GoLang 指针初探


1. 内置类型和引用类型

Go 中内置类型包括数值类型,字符串类型和布尔类型。引用类型包括切片,映射,通道,接口和函数类型。其中,引用类型表示创建的变量包含一个指向底层数据结构的指针和一组管理底层数据结构的字段。

1.1 内置类型:数值类型

以数值类型举例,查看数值类型的指针变化情况。

var x int = 1
y := x
fmt.Printf("x: %d, &x: %p, y: %d, &y: %p\n", x, &x, y, &y)
y = 0
fmt.Printf("x: %d, &x: %p, y: %d, &y: %p\n", x, &x, y, &y)

var yp *int = &y
fmt.Printf("\ny: %d, &y: %p, yp: %p, &yp: %p, *yp: %d\n", y, &y, yp, &yp, *yp)
*yp = 1
fmt.Printf("y: %d, &y: %p, yp: %p, &yp: %p, *yp: %d\n", y, &y, yp, &yp, *yp)

var ypn *int = new(int)
fmt.Printf("\nypn: %p, &ypn: %p, *ypn: %v\n", ypn, &ypn, *ypn)
*ypn = 1
fmt.Printf("ypn: %p, &ypn: %p, *ypn: %v\n", ypn, &ypn, *ypn)

执行结果:

x: 1, &x: 0xc000014088, y: 1, &y: 0xc0000140a0
x: 1, &x: 0xc000014088, y: 0, &y: 0xc0000140a0

y: 0, &y: 0xc0000140a0, yp: 0xc0000140a0, &yp: 0xc000006030, *yp: 0
y: 1, &y: 0xc0000140a0, yp: 0xc0000140a0, &yp: 0xc000006030, *yp: 1

ypn: 0xc0000140d0, &ypn: 0xc000006038, *ypn: 0
ypn: 0xc0000140d0, &ypn: 0xc000006038, *ypn: 1

可以看出:

  1. 内置类型变量的赋值完成的是变量值的拷贝,对拷贝的副本的改动不影响原变量。
  2. 内置类型变量指针的赋值完成的是变量地址的拷贝,对地址所指向的值的改动会影响原变量。

上述“规则”也适用于内置类型在函数间的传递。

1.2 引用类型:切片

切片创建的变量包含一个指向底层数组的指针。切片的结构是指向底层数组的指针,切片长度和容量的数组。

创建切片并查看其指针变化情况:

s := []int{1, 2, 3}
for index, value := range s {
    fmt.Printf("s index %d address: %p, the copy address: %p, value:%d\n", index, &s[index], &value, value)
}
fmt.Printf("s address: %p, s length: %d\n\n", &s, unsafe.Sizeof(s))
// fmt.Printf("s len: %d, cap: %d\n", len(s), cap(s))

sc := s
for index, value := range sc {
    fmt.Printf("sc index %d address: %p, the copy address: %p, value:%d\n", index, &sc[index], &value, value)
}
fmt.Printf("sc address: %p, sc length: %d\n\n", &sc, unsafe.Sizeof(sc))
// fmt.Printf("sc len: %d, cap: %d\n", len(sc), cap(sc))

sp := &s
for index, value := range *sp {
    fmt.Printf("sp index %d address: %p, the copy address: %p, value:%d\n", index, &(*sp)[index], &value, value)
}
fmt.Printf("sp address: %p, sp length: %d\n\n", &sp, unsafe.Sizeof(sp))

执行结果:

s index 0 address: 0xc00000a198, the copy address: 0xc000014088, value:1
s index 1 address: 0xc00000a1a0, the copy address: 0xc000014088, value:2
s index 2 address: 0xc00000a1a8, the copy address: 0xc000014088, value:3
s address: 0xc000004078, s length: 24

sc index 0 address: 0xc00000a198, the copy address: 0xc0000140a8, value:1
sc index 1 address: 0xc00000a1a0, the copy address: 0xc0000140a8, value:2
sc index 2 address: 0xc00000a1a8, the copy address: 0xc0000140a8, value:3
sc address: 0xc000004090, sc length: 24

sp index 0 address: 0xc00000a198, the copy address: 0xc0000140d0, value:1
sp index 1 address: 0xc00000a1a0, the copy address: 0xc0000140d0, value:2
sp index 2 address: 0xc00000a1a8, the copy address: 0xc0000140d0, value:3
sp address: 0xc000006030, sp length: 8

可以看出:

  1. 切片的大小为 24Bytes(其中,地址指针,长度整型值,容量整型值分别占 8Bytes)。
  2. 对切片赋值,实际上赋值的切片的副本,切片地址指针指向的底层数组并没有动。
  3. 赋切片地址,实际上做的是开辟了内存空间,在内存空间中存储了切片的地址值,这里开辟的内存空间大小为 8Bytes。

上述“规则”在函数的传递中也适用。

1.3 结构类型和方法

介绍引用类型之前有必要介绍下结构类型。结构类型用来描述一组数据值,数据值可以是原始,也可以是非原始的。
创建结构类型和方法并查看指针变化情况:

func (p person) eat() {
	fmt.Printf("eating %s\n", p.food)
	// fmt.Printf("p: %p, &p: %p, p.sex: %p, p.age: %p, p.food: %p\n", p, &p, &(*p).sex, &(*p).age, &(*p).food)
	fmt.Printf("p: %v, &p: %p, p.sex: %p, p.age: %p, p.food: %p\n", p, &p, &p.sex, &p.age, &p.food)
}

func (sp *super_person) eat() {
	fmt.Printf("%s is eating %s\n", sp.name, sp.food)
	fmt.Printf("sp: %p, &sp: %p, sp.sex: %p, sp.age: %p, sp.food: %p\n", sp, &sp, &(*sp).sex, &(*sp).age, &(*sp).food)
}

chunqiu := person{"male", 27, "dogLiang"}
fmt.Printf("chunqiu address: %p, chunqiu.sex: %p, chunqiu.age: %p, chunqiu.food: %p\n", &chunqiu, &chunqiu.sex, &chunqiu.age, &chunqiu.food)
chunqiu.eat()
(&chunqiu).eat()

p := super_person{
    person: person{
        sex:  "male",
        age:  27,
        food: "catLiang",
    },
    name: "chunqiu",
}

fmt.Printf("\nchunqiu address: %p, chunqiu.sex: %p, chunqiu.age: %p, chunqiu.food: %p, chunqiu.name: %p\n", &p, &p.sex, &p.age, &p.food, &p.name)
p.eat()
(&p).eat()

执行结果:

chunqiu address: 0xc0000723c0, chunqiu.sex: 0xc0000723c0, chunqiu.age: 0xc0000723d0, chunqiu.food: 0xc0000723d8
eating dogLiang
p: {male 27 dogLiang}, &p: 0xc0000723f0, p.sex: 0xc0000723f0, p.age: 0xc000072400, p.food: 0xc000072408
eating dogLiang
p: {male 27 dogLiang}, &p: 0xc000072450, p.sex: 0xc000072450, p.age: 0xc000072460, p.food: 0xc000072468

chunqiu address: 0xc000046040, chunqiu.sex: 0xc000046040, chunqiu.age: 0xc000046050, chunqiu.food: 0xc000046058, chunqiu.name: 0xc000046068
chunqiu is eating catLiang
sp: 0xc000046040, &sp: 0xc000006030, sp.sex: 0xc000046040, sp.age: 0xc000046050, sp.food: 0xc000046058
chunqiu is eating catLiang
sp: 0xc000046040, &sp: 0xc000006038, sp.sex: 0xc000046040, sp.age: 0xc000046050, sp.food: 0xc000046058

可以看出:

  1. 值接收者和指针接收者方法都可以接收值和指针类型的结构体变量,这是因为 Go 编译器在背后做了“类型转换”。
  2. 值接收者方法接收到的是结构体的副本,指针接收者方法接收到的是结构体的地址。前者不会改变原结构体的数据,后者则会改变。使用值接收者方法和指针接收者方法主要取决于传递类型的本质。

1.4 引用类型:接口

Go 语言中的接口是引用类型,具体的实现由方法定义。
给 eat() 方法添加 eater 接口:

type eater interface {
	eat()
}

func NewEater(e eater) {
	fmt.Printf("e: %p, &e: %p\n", e, &e)
	e.eat()
}

var handsome_boy eater = chunqiu
fmt.Printf("chunqiu address: %p, chunqiu.sex: %p, chunqiu.age: %p, chunqiu.food: %p\n", &chunqiu, &chunqiu.sex, &chunqiu.age, &chunqiu.food)
fmt.Printf("handsome_boy address: %p, handsome value: %v, handsome length: %d\n", &handsome_boy, handsome_boy, unsafe.Sizeof(handsome_boy))
NewEater(handsome_boy)

var bold_boy eater = &p
fmt.Printf("\nchunqiu address: %p, chunqiu.sex: %p, chunqiu.age: %p, chunqiu.food: %p, chunqiu.name: %p\n", &p, &p.sex, &p.age, &p.food, &p.name)
fmt.Printf("bold_boy address: %p, bold_boy value: %p, bold length: %d\n", &bold_boy, bold_boy, unsafe.Sizeof(bold_boy))
NewEater(bold_boy)

执行结果:

chunqiu address: 0xc0000723c0, chunqiu.sex: 0xc0000723c0, chunqiu.age: 0xc0000723d0, chunqiu.food: 0xc0000723d8
handsome_boy address: 0xc00003a240, handsome value: {male 27 dogLiang}, handsome length: 16
e: %!p(main.person={male 27 dogLiang}), &e: 0xc00003a250
eating dogLiang
p: {male 27 dogLiang}, &p: 0xc000072420, p.sex: 0xc000072420, p.age: 0xc000072430, p.food: 0xc000072438

chunqiu address: 0xc000046040, chunqiu.sex: 0xc000046040, chunqiu.age: 0xc000046050, chunqiu.food: 0xc000046058, chunqiu.name: 0xc000046068
bold_boy address: 0xc00003a270, bold_boy value: 0xc000046040, bold length: 16
e: 0xc000046040, &e: 0xc00003a280
chunqiu is eating catLiang
sp: 0xc000046040, &sp: 0xc000006030, sp.sex: 0xc000046040, sp.age: 0xc000046050, sp.food: 0xc000046058

可以看出:

  1. 接口类型是引用类型。
  2. 接口方法传值传递的是引用类型的副本,传指针传递的是引用类型的地址。
  3. 内嵌结构体提升的数据,可被外部结构体直接访问,通过访问接口方法可实现继承和多态。

进一步的,将地址和值分别赋给接口 handsome_boy 和 bold_boy 如下:

var handsome_boy eater = &chunqiu
var bold_boy eater = p

执行结果:

src\Blog\go_variable.go:163:6: cannot use p (type super_person) as type eater in assignment:
    super_person does not implement eater (eat method has pointer receiver)

报错了,从报错信息可以看到不能将值赋值给实现接口指针接收者的方法,这和前面的指针接收者接收的参数是不一样的。列出接口方法和方法的接收参数如下:

接口方法 类型
值接收者 T / *T
指针接收者 *T
方法 类型
值接收者 T / *T
指针接收者 T / *T

更新:函数返回指针类型。

package main

import "fmt"

func translet_string(sp *string, st *string) *string {
	fmt.Printf("\n&sp: %v, sp: %v\n", &sp, sp)
	fmt.Printf("&st: %v, st: %v, *st: %v\n", &st, st, *st)
	
	if sp == nil {
		sp = st
	}
	return sp
}

func main() {
	var s *string
	fmt.Printf("&s: %v, s: %v\n", &s, s)
	
	var str string = "hxia"
	fmt.Printf("&str: %v, str: %v\n", &str, str)
	
	st := translet_string(s, &str)
	fmt.Printf("\n&st: %v, st: %v, *st: %v\n", &st, st, *st)
}
&s: 0xc00000e028, s: <nil>
&str: 0xc000010230, str: hxia

&sp: 0xc00000e040, sp: <nil>
&st: 0xc00000e048, st: 0xc000010230, *st: hxia

&st: 0xc00000e038, st: 0xc000010230, *st: hxia
posted @ 2021-03-30 14:27  hxia043  阅读(130)  评论(0编辑  收藏  举报