与 C# 的类型相比, go 语言的类型有两点很大的差异, 1是没有值类型和引用类型之分, 2是对象在栈上还是在堆上由运行时决定(C++中是由类的使用者决定, C#是由类的定义者决定, go与Java类似,由编译器(java 是JVM)决定分析决定)
1. go 语言中的所有类型都是值类型, 没有引用类型, 变量之间赋值与函数传参全部都是值传递
示例:
type Persion struct { age int }
func main(){
p1 := Person{age:33}
p2 := p1 // 注: p2 将获得一片新分配的内存, 并将p1中的所有内容都copy了一份到其中
p2.age = 34 // 此时的 p1.age 仍然是33
}
2. 关于数组(array), 同样是值类型的
示例:
a1 := [3]int{1, 2, 3}
a2 := a1 // 注: a1 将被整个拷贝一份
a2[1] = 4 // 此时 a1[1] 仍然是 2
3. 关于切片(slice), 同样是值类型, 只不过内部有一根指向用于具体存放数据的array的指针
切片的内部结构类似于
type Slice struct {
pArray *[]int
nLen int
nCap int // 当nLen增长到了nCap, pArray 会扩容(即:重新创建个更大的数组,并把数据拷贝过去)
}
所以 当两个切片变量间赋值后, 会指向同一个数组, 但仅仅只是这样而已, 如果其中一个切片因为append发生了扩容, 将不会影响另一个
示例:
s1 := make([]int, 4, 4)
s1[3] = 5
s2 := s1 // slice的内容被拷贝, 但s1 和 s2 中的指针 pArray 指向的是同一个 数组
s2[3] = 6 // 这时 s1[3] 也变成了 6
fmt.Println(s1[3])
fmt.Println(s2[3])
s2 = append(s2, 44) // 因为 len将大于cap, 所以 s2 要扩容, 这将导致 s2 的 pArray 会指向一个新的 数组
s2[3] = 7 // 这时 s1[3] 仍然是 6
fmt.Println(s1[3])
fmt.Println(s2[3])
4. 但是像 slice 和 map, chan 由其特殊的地方, 首先他们是内建类型, 要正确的创建一个slice,map或chan 只能使用make
如果单独定义 slice(包括map和chan)的变量是可以的, 但是只会对slice中的变量作零值初始化, 而且很要注意的是, 这个变量与nil比较时的结果是 true, 我的分析是, 不使用make而单独定义一个slice变量 没有什么实质性的作用, 不能对它做append等操作, 从使用的角度看与nil值没什么区别, 但len和cap函数还是能用的, 结果都是0
示例:
var s1 []int // 注: 这个时候的确是分配了 slice 的 pArray, nLen, nCap
fmt.Println(len(s1)) // 结果是0
fmt.Println(s1 = = nil) // 结果是 true, 因为 runtime 对 slice 类型的变量 与 nil 值比较时 做了特殊处理
我们可以尝试强行分配一个 为 NULL 的 slice 指针
var s2 *[]int // s2 的值是 nil
fmt.Println(len(s2)) // 编译不过, len 不接受 指针
fmt.Println(len(*s2)) // 编译通过, 但运行报错, 因为 s2 这地址是NULL, 无法取值
再看另外一种现象, 使用new来创建 slice
s1 := new([]int)
fmt.Println(*s1 = = nil) // 结果是 true, 原因就是 虽然 slice 创建了, 但是 内容(pArray, nCap, nLen)都是零值,
// 没有什么意义, runtime 对其做了特殊处理, 认为 这样的 slice 就是 nil
可以总结一下:
1. 对于 slice, map, chan 要正确使用的话, 必须使用 make 来创建它们, 直接定义变量或使用new都不行
2. 其实直接定义变量和使用new, 在内存分配上没有任何区别, 只不过 一个返回的是 类型的变量, 一个返回的是类型指针变量, 置于是在栈上还是在堆上, 不是 使不使用new 来定的, 而是 go语言的编译器根据上下文场景定的