数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
1:从两个维度描述数组 数组中存储的元素类型和数组最大能存储的元素个数
cmd/compile/internal/types.Array
// Array contains Type fields specific to array types.
type Array struct {
Elem *Type // element type
Bound int64 // number of elements; <0 if unknown yet
}
Go 语言数组在初始化之后大小就无法改变,存储元素类型相同、但是大小不同的数组类型在 Go 语言看来也是完全不同的,只有两个条件都相同才是同一类型。:
2:数组生成
cmd/compile/internal/types.NewArray
// 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
}
元素类型Elem 和 数组的大小Bound 这两个字段共同构成了数组类型
当前数组是否应该在堆栈中初始化也在编译期就确定了。
3:Go 语言的数组有两种不同的创建方式
arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}
种声明方式在运行期间得到的结果是完全相同的,后一种声明方式在编译期间就会被转换成前一种,这也就是编译器对数组大小的推导。
4:两种不同的声明方式会导致编译器做出完全不同的处理
a:[10]T 的方式声明数组时
变量的类型在编译进行到类型检查阶段就会被提取出来,随后使用 cmd/compile/internal/types.NewArray
创建包含数组大小的 cmd/compile/internal/types.Array
结构体
b:[...]T
的方式声明数组时
cmd/compile/internal/gc.typecheckcomplit
函数中对该数组的大小进行推导。
所以[...]T{1, 2, 3}
和 [3]T{1, 2, 3}
在运行时是完全等价的,[...]T
这种初始化方式也只是 Go 语言为我们提供的一种语法糖,当我们不想计算数组中的元素个数时可以通过这种方法减少一些工作量。
5:语句转换
根据数组数量的不同,编译器会在负责初始化字面量的 cmd/compile/internal/gc.anylit
函数中做两种不同的优化:
a.当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
[3]int{1, 2, 3}会被分成一个声明变量的表达式和几个赋值表达式,这些表达式会完成对数组的初始化:
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
b.当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;
[5]int{1, 2, 3, 4, 5}会被分成一个声明变量的表达式和几个赋值表达式 + 赋值:
var arr [5]int
statictmp_0[0] = 1
statictmp_0[1] = 2
statictmp_0[2] = 3
statictmp_0[3] = 4
statictmp_0[4] = 5
arr = statictmp_0
总结起来,在不考虑逃逸分析的情况下,如果数组中元素的个数小于或者等于 4 个,那么所有的变量会直接在栈上初始化,如果数组元素大于 4 个,变量就会在静态存储区初始化然后拷贝到栈上,这些转换后的代码才会继续进入中间代码生成和机器码生成两个阶段,最后生成可以执行的二进制文件
6:访问和赋值
无论是在栈上还是静态存储区,数组在内存中都是一连串的内存空间。
数组开头的指针+元素的数量 + 元素类型占的空间大小表示数组
如果我们不知道数组中元素的数量,访问时可能发生越界
如果我们不知道数组中元素类型的大小,就没有办法知道应该一次取出多少字节的数据
func newArray() *[4]int {
a := [4]int{1, 2, 3, 4}
return &a
}
func main() {
a := newArray()
fmt.Println(a)
}
/*
go build -gcflags="-m -m" hello.go
# command-line-arguments
./hello.go:6:6: cannot inline newArray: marked go:noinline
./hello.go:11:6: cannot inline main: function too complex: cost 139 exceeds budget 80
./hello.go:13:13: inlining call to fmt.Println func(...interface {}) (int, error) { var fmt..autotmp_3 int; fmt..autotmp_3 = ; var fmt..autotmp_4 error; fmt..autotmp_4 = ; fmt..autotmp_3, fmt..autotmp_4 = fmt.Fprintln(io.Writer(os.Stdout), fmt.a...); return fmt..autotmp_3, fmt..autotmp_4 }
./hello.go:7:2: a escapes to heap:
./hello.go:7:2: flow: ~r0 = &a:
./hello.go:7:2: from &a (address-of) at ./hello.go:8:9
./hello.go:7:2: from return &a (return) at ./hello.go:8:2
./hello.go:7:2: moved to heap: a
./hello.go:13:13: []interface {} literal does not escape
:1: .this does not escape
*/
这儿变量 a 发生 escape 的原因是,因为编译器发现在后续代码中存在对此局部变量的引用,因此将变量 a 移动到了 heap 中。
这个问题在 Go doc 的 FAQ https://golang.org/doc/faq#stack_or_heap中说得比较清楚:在可能的情况下,编译器会在当前函数的栈中为局部变量分配内存。 一旦当编译器发现后续代码中存在对局部变量的引用,就会将局部变量从栈移动到堆,以此来避免 C/C++ 中常出现的所谓“悬空指针”的现象。
另外当局部变量非常大的时候,编译器也会考虑在堆上创建局部变量。
所以说,到底是在栈还是在堆上分配内存,并没有一个一成不变的规则,编译器会根据具体的情况做出最优选择。
------------------------------------------------------------2021-06-08-------------------------------------------------------
数组的定义必须指定类型和大小 数组的类型包括长度和类型 数组的长度是数组类型的一部分
数组初始化
var testArray [3]int //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化 不足的为零值
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
//自行推断数组的长度
var testArray [3]int //[0,0,0]
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
//指定索引值的方式来初始化数组
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
fmt.Printf("type of a:%T\n", a) //type of a:[4]int
数组遍历
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}
多维数组只有第一层可以使用...
来让编译器推导数组长度
a := [3][2]int{
{1,2},
{2,3},
{3,4},
}
b := [...][2]int{
[2]int{1,2},
[2]int{2,3},
[2]int{3,4},
}
fmt.Println(a,b)
[[1 2] [2 3] [3 4]] [[1 2] [2 3] [3 4]]
数组支持 “==“、”!=” 操作符,因为数组是值类型,内存总是被初始化过的
[n]*T
表示指针数组,*[n]T
表示数组指针
数组常见的坑。。。。
package main
import "fmt"
func testArray(){
a := [3]int{1, 2, 3}
for k, v := range a {
if k == 0 {
a[0], a[1] = 100, 200
fmt.Println(a)
}
a[k] = 100 + v
}
fmt.Println(a)
}
/**
range a的时候 a已经拷贝了一份,所以v的时候也是确定的。。。。
[100 200 3]
[101 102 103]
**/
func testSlicess(){
a := []int{1, 2, 3}
for k, v := range a {
if k == 0 {
a[0], a[1] = 100, 200
fmt.Println(a)
}
a[k] = 100 + v
}
fmt.Println(a)
}
/***
range a的时候 虽然a拷贝了一份,拷贝的是指向数组的指针,所以a[0], a[1] = 100, 200 改变了底层数组的值,但是k=0的时候 v已经取出来了所以。。。。。
[100 200 3]
[101 300 103]
**/
array := [3]int {10, 20, 30}
for _, num := range array {
num++
}
fmt.Println(array)
for i := 0; i < len(array); i++ {
array[i]++
}
fmt.Println(array)
//不管是[3]int 还是[]int 结果都是一样的。。。
[10 20 30]
[11 21 31]