结构体和方法

一、值,指针和引用

我们现在有一段程序:

  1. package main
  2. import "fmt"
  3. func main() {
  4. // a,b 是一个值
  5. a := 5
  6. b := 6
  7. fmt.Println("a的值:", a)
  8. // 指针变量 c 存储的是变量 a 的内存地址
  9. c := &a
  10. fmt.Println("a的内存地址:", c)
  11. // 指针变量不允许直接赋值,需要使用 * 获取引用
  12. //c = 4
  13. // 将指针变量 c 指向的内存里面的值设置为4
  14. *c = 4
  15. fmt.Println("a的值:", a)
  16. // 指针变量 c 现在存储的是变量 b 的内存地址
  17. c = &b
  18. fmt.Println("b的内存地址:", c)
  19. // 将指针变量 c 指向的内存里面的值设置为8
  20. *c = 8
  21. fmt.Println("a的值:", a)
  22. fmt.Println("b的值:", b)
  23. // 把指针变量 c 赋予 c1, c1 是一个引用变量,存的只是指针地址,他们两个现在是独立的了
  24. c1 := c
  25. fmt.Println("c的内存地址:", c)
  26. fmt.Println("c1的内存地址:", c1)
  27. // 将指针变量 c 指向的内存里面的值设置为9
  28. *c = 9
  29. fmt.Println("c指向的内存地址的值", *c)
  30. fmt.Println("c1指向的内存地址的值", *c1)
  31. // 指针变量 c 现在存储的是变量 a 的内存地址,但 c1 还是不变
  32. c = &a
  33. fmt.Println("c的内存地址:", c)
  34. fmt.Println("c1的内存地址:", c1)
  35. }

打印出:

  1. a的值: 5
  2. a的内存地址: 0xc000016070
  3. a的值: 4
  4. b的内存地址: 0xc000016078
  5. a的值: 4
  6. b的值: 8
  7. c的内存地址: 0xc000016078
  8. c1的内存地址: 0xc000016078
  9. c指向的内存地址的值 9
  10. c1指向的内存地址的值 9
  11. c的内存地址: 0xc000016070
  12. c1的内存地址: 0xc000016078

那么 a,b 是一个值变量,而 c 是指针变量,c1 是引用变量。

如果 & 加在变量 a 前:c := &a,表示取变量 a 的内存地址,c 指向了 a,它是一个指针变量。

当获取或设置指针指向的内存的值时,在指针变量前面加 *,然后赋值,如:*c = 4,指针指向的变量 a 将会变化。

如果将指针变量赋予另外一个变量:c1 := c,那另外一个变量 c1 可以叫做引用变量,它存的值也是内存地址,内存地址指向的也是变量 a,这时候,引用变量只是指针变量的拷贝,两个变量是互相独立的。

值变量可以称为值类型,引用变量和指针变量都可以叫做引用类型。

如何声明一个引用类型的变量(也就是指针变量)呢?

我们可以在数据类型前面加一个 * 来表示:

  1. var d *int

我们以后只会以值类型,和引用类型来区分变量。

二、结构体

有了基本的数据类型,还远远不够,所以 Golang 支持我们定义自己的数据类型,结构体:

  1. // 结构体
  2. type Diy struct {
  3. A int64 // 大写导出成员
  4. b float64 // 小写不可以导出
  5. }

结构体的名字为 Diy,使用 type 结构体名字 struct 来定义。

结构体里面有一些成员 Ab ,和变量定义一样,类型 int64float64 放在后面,不需要任何符号分隔,只需要换行即可。结构体里面小写的成员,在包外无法使用,也就是不可导出。

使用结构体时:

  1. // 新建结构体,值
  2. g := diy.Diy{
  3. A: 2,
  4. //b: 4.0, // 小写成员不能导出
  5. }
  6. // 新建结构体,引用
  7. k := &diy.Diy{
  8. A: 2,
  9. }
  10. // 新建结构体,引用
  11. m := new(diy.Diy)
  12. m.A = 2

可以按照基本数据类型的样子使用结构体,上述创立的:

  1. g := diy.Diy{
  2. A: 2,
  3. //b: 4.0, // 小写成员不能导出
  4. }

是一个值类型的结构体。

你也可以使用结构体值前面加一个 & 或者使用 new 来创建一个引用类型的结构体,如:

  1. // 新建结构体,引用
  2. k := &diy.Diy{
  3. A: 2,
  4. }
  5. // 新建结构体,引用
  6. m := new(diy.Diy)
  7. m.A = 2

引用和值类型的结构体有何区别的?

我们知道函数内和函数外的变量是独立的,当传参数进函数的时候,参数是值拷贝,函数里的变量被约束在函数体内,就算修改了函数里传入的变量的值,函数外也发现不了。

但引用类型的变量,传入函数时,虽然也是传值,但拷贝的是引用类型的内存地址,可以说拷贝了一个引用,这个引用指向了函数体外的某个结构体,使用这个引用在函数里修改结构体的值,外面函数也会发现。

如果传入的不是引用类型的结构体,而是值类型的结构体,那么会完整拷贝一份结构体,该结构体和原来的结构体就没有关系了。

内置的数据类型切片 slice 和字典 map 都是引用类型,不需要任何额外操作,所以传递这两种类型作为函数参数,是比较危险的,开发的时候需要谨慎操作。

三、方法

结构体可以和函数绑定,也就是说这个函数只能被该结构体使用,这种函数称为结构体方法:

  1. // 引用结构体的方法,引用传递,会改变原有结构体的值
  2. func (diy *Diy) Set(a int64, b float64) {
  3. diy.A = a
  4. diy.b = b
  5. return
  6. }
  7. // 值结构体的方法,值传递,不会改变原有结构体的值
  8. func (diy Diy) Set2(a int64, b float64) {
  9. diy.A = a
  10. diy.b = b
  11. return
  12. }

只不过在以前函数的基础上 func Set(a int64, b float64),变成了 func (diy *Diy) Set(a int64, b float64),只不过在函数里面,可以使用结构体变量 diy 里面的成员。

上面表示值类型的结构体 diy Diy 可以使用 Set2 方法,引用类型的结构体 diy *Diy 可以使用 Set 方法。

如果是这样的话,我们每次使用结构体方法时,都要注意结构体是值还是引用类型,幸运的是 Golang 操碎了心,每次使用一个结构体调用方法,都会自动将结构体进行类型转换,以适配方法。比如下面:

  1. // 新建结构体,值
  2. g := diy.Diy{
  3. A: 2,
  4. //b: 4.0, // 小写成员不能导出
  5. }
  6. g.Set(1, 1)
  7. fmt.Printf("type:%T:%v\n", g, g) // 结构体值变化
  8. g.Set2(3, 3)
  9. fmt.Printf("type:%T:%v\n", g, g) // 结构体值未变化
  10. // 新建结构体,引用
  11. k := &diy.Diy{
  12. A: 2,
  13. }
  14. k.Set(1, 1)
  15. fmt.Printf("type:%T:%v\n", k, k) // 结构体值变化
  16. k.Set2(3, 3)
  17. fmt.Printf("type:%T:%v\n", k, k) // 结构体值未变化

结构体 g 是值类型,本来不能调用 Set 方法,但是 Golang 帮忙转换了,我们毫无感知,然后值类型就变成了引用类型。同理,k 是引用类型,照样可以使用 Set2 方法。

前面我们也说过,函数传入引用,函数里修改该引用对应的值,函数外也会发现。

结构体的方法也是一样,不过范围扩散了结构体本身,方法里可以修改结构体本身,但是如果结构体是值,那么修改后,外面的世界是发现不了的。

四、关键字 new 和 make

关键字 new 主要用来创建一个引用类型的结构体,只有结构体可以用。

关键字 make 是用来创建和初始化一个切片或者字典。我们可以直接赋值来使用:

  1. e := []int64{1, 2, 3} // slice
  2. f := map[string]int64{"a": 3, "b": 4} // map

但是这种直接赋值相对粗暴,因为我们使用时可能不知道数据在哪里,数据有多少。

所以,我们在创建切片和字典时,可以指定容量大小。看示例:

  1. s := make([]int64, 5)
  2. s1 := make([]int64, 0, 5)
  3. m1 := make(map[string]int64, 5)
  4. m2 := make(map[string]int64)
  5. fmt.Printf("%#v,cap:%#v,len:%#v\n", s, cap(s), len(s))
  6. fmt.Printf("%#v,cap:%#v,len:%#v\n", s1, cap(s1), len(s1))
  7. fmt.Printf("%#v,len:%#v\n", m1, len(m1))
  8. fmt.Printf("%#v,len:%#v\n", m2, len(m2))

运行后:

  1. []int64{0, 0, 0, 0, 0},cap:5,len:5
  2. []int64{},cap:5,len:0
  3. map[string]int64{},len:0
  4. map[string]int64{},len:0

切片可以使用 make([],占用容量大小,全部容量大小) 来定义,你可以创建一个容量大小为 5,但是实际占用容量为 0 的切片,比如 make([]int64, 0, 5),你预留了 5 个空间,这样当你切片 append 时,不会因为容量不足而内部去分配空间,节省了时间。

如果你省略了后面的参数如 make([]int64, 5),那么其等于 make([]int64, 5,5),因为这时全部容量大小就等于占用容量大小。内置语言 caplen 可以查看全部容量大小,已经占用的容量大小。

同理,字典也可以指定容量,使用 make([],容量大小),但是它没有所谓的占用容量,它去掉了这个特征,因为我们使用切片,可能需要五个空白的初始值,但是字典没有键的情况下,预留初始值也没作用。省略容量大小,表示创建一个容量为 0 的键值结构,当赋值时会自动分配空间。

五、内置语法和函数,方法的区别

函数是代码片段的一个封装,方法是将函数和结构体绑定。

但是 Golang 里面有一些内置语法,不是函数,也不是方法,比如 appendcaplenmake,这是一种语法特征。

语法特征是高级语言提供的,内部帮你隐藏了如何分配内存等细节。