typora/note/Go/数组切片.md
2024-12-12 10:48:55 +08:00

6.6 KiB
Raw Blame History

一、数组

1. 定义

  • 一个长度固定的、由同构类型(相同类型)元素组成的连续序列
  • var arr [N]T
  • 数组变量 arr类型为[N]T长度为N元素类型为T
  • 如果两个数组类型的元素类型 T 与数组长度 N 都是一样的,那么这两个数组类型是等价的
  • 如果有一个属性不同,它们就是两个不同的数组类型
  • 通过len方法获得数组长度
  • 通过unsafe.Pointer()可以获得数组总的占用空间
  • 数组做为函数参数,值拷贝,性能代价高

2. 初始化

  • 定义但不初始化
  • 定义且初始化并指定长度
  • 定义且初始化当不指定长度
  • 下标赋值的方式对它进行初始化

var arr1 [6]int // 没有显示初始化,数据默认为零值,[0 0 0 0 0 0]

var arr2 = [6]int {
    11, 12, 13, 14, 15, 16,
} // [11 12 13 14 15 16]

var arr3 = [...]int { 
    21, 22, 23,
} // 编译器自动推断数组的长度,[21 22 23]
fmt.Printf("%T\n", arr3) // [3]int

// 下标赋值的方式对它进行初始化
var arr4 = [...]int{
    99: 39, // 将第100个元素(下标值为99)的值赋值为39其余元素值均为0
}
fmt.Printf("%T\n", arr4) // [100]int

3. 底层方法

  • 初始化
// NewArray returns a new fixed-length array Type.
func NewArray(elem *Type, bound int64) *Type {
	if bound < 0 {
		Fatalf("NewArray: invalid bound %v", bound)
	}
	t := New(TARRAY)
	t.Extra = &Array{Elem: elem, Bound: bound}
	t.SetNotInHeap(elem.NotInHeap())
	return t
}
  • 长度推导
    • 定义时指定长度变量类型和长度会在编译中的类型检查阶段取出来直接调用NewArray方法创建一个Array
    • 未指定长度会在编译中的类型检查阶段调用typecheckcomplit阶段推导出长度
// The result of typecheckcomplit MUST be assigned back to n, e.g.
// 	n.Left = typecheckcomplit(n.Left)
func typecheckcomplit(n *Node) (res *Node) {
	// Need to handle [...]T arrays specially.
	if n.Right.Op == OTARRAY && n.Right.Left != nil && n.Right.Left.Op == ODDD {
		n.Right.Right = typecheck(n.Right.Right, ctxType)
		if n.Right.Right.Type == nil {
			n.Type = nil
			return n
		}
		elemType := n.Right.Right.Type // 元素类型

		length := typecheckarraylit(elemType, -1, n.List.Slice(), "array literal") // 数组长度

		n.Op = OARRAYLIT
		n.Type = types.NewArray(elemType, length)
		n.Right = nil
		return n
	}
  switch t.Etype {
	default:
		yyerror("invalid composite literal type %v", t)
		n.Type = nil

	case TARRAY:
		typecheckarraylit(t.Elem(), t.NumElem(), n.List.Slice(), "array literal") // 对数组进行元素和索引的检查
		n.Op = OARRAYLIT
		n.Right = nil
  // ... 省略代码
}
  • 数组越界检查
    • 使用常量或整数数值在静态编译期间可以验出数组越界风险
func typecheck1(n *Node, top int) (res *Node) {
	if enableTrace && trace {
		defer tracePrint("typecheck1", n)(&res)
	}

	switch n.Op {
    case OINDEX:
		ok |= ctxExpr
		n.Left = typecheck(n.Left, ctxExpr)
		n.Left = defaultlit(n.Left, nil)
		n.Left = implicitstar(n.Left)
		l := n.Left // 数组
		n.Right = typecheck(n.Right, ctxExpr) // 索引
		r := n.Right
    t := l.Type
    case TSTRING, TARRAY, TSLICE: // 判断是数组
			n.Right = indexlit(n.Right)
			if n.Right.Type != nil && !n.Right.Type.IsInteger() {
				yyerror("non-integer %s index %v", why, n.Right)
				break
			}

			if !n.Bounded() && Isconst(n.Right, CTINT) {
				x := n.Right.Int64()
				if x < 0 {
					yyerror("invalid %s index %v (index must be non-negative)", why, n.Right)
				} else if t.IsArray() && x >= t.NumElem() { // 数组越界
					yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, t.NumElem())
				}
			}
  • 运行时数组越界
    • runtime.panicindex
TEXT runtime·panicIndex(SB),NOSPLIT,$0-16
	MOVQ	AX, x+0(FP)
	MOVQ	CX, y+8(FP)
	JMP	runtime·goPanicIndex(SB)
  • runtime.goPanicIndex
func goPanicIndex(x int, y int) {
	panicCheck1(getcallerpc(), "index out of range")
	panic(boundsError{x: int64(x), signed: true, y: y, code: boundsIndex})
}

切片

  • 定义切片变量时,不像数组一样定义长度
  • 长度容量不固定
  • len 返回切片长度
  • append 向切片中追加元素
  • 切片传参数,相当于传递底层数组的描述符

切片结构

  • 底层运行时结构
  • array 底层数组的指针
  • len 切片长度
  • cap 切片容量

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

切片定义与初始化

  • 通过make创建切片可以指定长度与容量
    • var arr = make([]int, 3, 6) 类型,长度,容量
    • 不指定容量cap=len
  • 采用 array[low : high : max]语法基于一个已存在的数组创建切片。这种方式被称为数组的切片化
    • 从原数组起始位置low开始
    • 新数组长度 high - low
    • 数组容量是 max - low
    • 对新切片中元素的修改将直接影响原数组
    • 新数组的容量和长度不能大于原数组
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]

  • 基于切片创建切片

切片动态扩容

  • 翻倍扩容
  • 基于一个已有数组建立的切片,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”,后续对切片的任何修改都不会反映到原数组中

数组与切片互相转换go1.17后官方支持)

  • 数组转切片
func main() {
	arr := [3]int{1, 2, 3}
	sli := arr[:]
	fmt.Println(sli)
}
  • 切片转数组 unsafe 方法
func main() {
	b := []int{1, 2, 3}
	p := (*[3]int)(unsafe.Pointer(&b[0]))
	(*p)[1]+= 10
	fmt.Println(*p)
}
  • 切片转数组官方方法转换后的数组长度不能大于原切片的长度nil 切片或 cap 为 0 的 empty 切片都可以被转换为一个长度为 0 的数组指针
func main() {
	b := []int{1, 2, 3}
	p := (*[3]int)(b)
	(*p)[1] += 10
	fmt.Println(*p)
}

函数传参性能

  • 传参都是值拷贝
  • slice拷贝的是引用结构
  • array拷贝的是数组的值包含元素
a1 := [16]int{}
	d1 := func(arr [16]int) int {
		return len(arr)
	}

	f1 := func() int {
		return d1(a1)
	}

	a2 := [65535]int{}
	d2 := func(arr [65535]int) int {
		return len(arr)
	}
	f2 := func() int {
		return d2(a2)
	}

	for _, f := range []func() int{f1, f2} {
		start := time.Now()
		for i := 0; i < 10000; i++ {
			f()
		}
		fmt.Printf("time.Since(start).Milliseconds(): %v\n", time.Since(start).Microseconds())
	}
    // 88
    // 398362