Golang中的指针、结构体、方法、方法集和接口
指针
- 很多人提起指针就害怕,这是C语言中的指针给人留下的影响,C语言中的指针基础概念很好懂,难就难在C语言中的指针实在是太灵活了,可以进行各种运算,所以也让C语言中的指针变得很难懂。Golang中保留了指针变量,但是对指针变量做了很大的限制,指针不允许进行运算,所以不会看到各种眼花缭乱难懂的指针运算技巧了,也使得Golang中的指针变得很好懂。
- 任何一个变量都由两部分组成,一部分是存储该变量的地址,一部分是该变量的值。对于非指针的普通变量来说,这两个部分很好理解,对于指针变量来说,指针变量的地址意义不变,跟普通变量一样,指针变量的值是其他变量的地址,也就是说指针变量是指向其他变量的,&符号是用来获取变量地址的,*号是用来获取指针变量的所指向地址的值的。看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package main import ( "fmt" "reflect" ) func main(){ var i int i = 123 var p *int p = &i fmt.Println(p) fmt.Println(&i) fmt.Println(*p) fmt.Println(&p) fmt.Println(reflect.TypeOf(p)) } # 结果如下: 0xc00000e0c8 0xc00000e0c8 123 0xc000006028 *int
|
结构体
- 结构体是一个复合数据类型,是由零个或多个任意类型的值聚合成的实体,每个值称为结构体的成员。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。
- 结构体变量的成员可以通过点操作符访问。因为是一个变量,它所有的成员也同样是变量,我们可以直接对每个成员赋值。或者是对成员取地址,然后通过指针访问。
- 需要注意的是,结构体有三种不同的初始化方式,不同的初始化方式返回的结果不一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package main import ( "fmt" "reflect" ) type Data struct { id int name string }
func main() { // 方式一:使用new函数进行初始化,此时d1是一个指针变量 d1 := new(Data) d1.id = 123 d1.name = "test" fmt.Println("d1 value:", d1) fmt.Println("d1 type:", reflect.TypeOf(d1)) // 方式二:直接初始化赋值,此时d2是个普通的结构体变量,不是指针变量 d2 := Data{123, "test"} fmt.Println("d2 value:", d2) fmt.Println("d2 type:", reflect.TypeOf(d2)) // 方式三:先声明再赋值使用,此时d2是个普通的结构体变量,不是指针变量 var d3 Data d3.id = 123 d3.name = "test" fmt.Println("d3 value:", d3) fmt.Println("d3 type:", reflect.TypeOf(d3)) } # 结果如下: d1 value: &{123 test} d1 type: *main.Data d2 value: {123 test} d2 type: main.Data d3 value: {123 test} d3 type: main.Data
|
类型的本质
- 。如果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值?如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。这个答案也会影响程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递。保持传递的一致性很重要。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。
- 内置类型:内置类型是由语言提供的一组类型。我们已经见过这些类型,分别是数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
- 引用类型:Go 语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。当声明上述类型的变量时,创建的变量被称作标头(header)值。从技术细节上说,字符串也是一种引用类型。每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。当引用类型传入函数时,在函数或者方法内修改该值会引起原来值的改变。
- 结构类型:结构类型可以用来描述一组数据值,这组值的本质即可以是原始的,也可以是非原始的。如果决定在某些东西需要删除或者添加某个结构类型的值时该结构类型的值不应该被更改,那么需要遵守之前提到的内置类型和引用类型的规范。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package main import "fmt" type IP []int func (s IP) change() { s[1] = 10 } func change(s IP) { s[2] = 20 } func main() { s := []int{1, 2, 3, 4, 5} var i IP i = s fmt.Println("Raw:", i) i.change() fmt.Println("method:", i) change(i) fmt.Println("function:", i) } # 结果为: Raw: [1 2 3 4 5] method: [1 10 3 4 5] function: [1 10 20 4 5]
|
方法
- 在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。方法能给用户定义的类型添加新的行为。方法实际上也是函数,只是在声明时,在关键字func 和方法名之间增加了一个参数。
- 关键字func 和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称为方法。
- 接收者可以是普通的值类型,也可以是指针类型,但是不管你的方法的接收者是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。但需要注意的是,值类型的方法传入只是值的拷贝,在方法内对接收者进行修改,不会影响到原来的值,指针类型传入的是变量地址,对接收者进行修改会修改原来的值。最终决定值是否改变的取决于方法是值类型方法还是指针类型方法,跟传入的类型无关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package main import ( "fmt" ) type Data struct { id int name string } func (d Data) method1() { d.id = 456 d.name = "method1" } func (d *Data) method2() { d.id = 789 d.name = "method2" } func main() { d1 := new(Data) d1.id = 123 d1.name = "test" d2 := Data{123, "test"} fmt.Println("Raw d1:", d1) d1.method1() fmt.Println("method1:", d1) d1.method2() fmt.Println("method2", d1) fmt.Println("Raw d2", d2) d2.method1() fmt.Println("method1:", d2) d2.method2() fmt.Println("method1:", d2) } # 结果如下: Raw d1: &{123 test} method1: &{123 test} method2 &{789 method2} Raw d2 {123 test} method1: {123 test} method1: {789 method2}
|
方法集和接口
- 方法集定义了接口的接受规则。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
- 从值的角度来看,T 类型的值的方法集只包含值接收者声明的方法。而指向T 类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。
Values |
Methods Receivers |
T |
func (t T)method() |
*T |
func (t T)method() 或func (t *T)method() |
- 接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会展示出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
- 接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口。
- 从接收者的角度来看,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。因为值类型接收者传入指针类型,变量可自动通过指针获取到值,但是指针接收者传入一个值类型,编译器并不是总能自动获得一个值的地址。
Methods Receivers |
Values |
func (t T)method() |
T或者*T |
func (t *T)method() |
*T |
- 这和上面方法集的规则并不矛盾,只是站的角度不同而已。
参考资料:
- 《Go语言实战》
- 《Go语言圣经》
- 《Go入门指南》