指针

指针是一个简单的变量,它保存一个值在内存中的存储位置。每个变量都被存储在一个或多个连续的内存位置,称为地址。

1
2
3
4
5
var x int32 = 10
var y bool = true
pointerX := &x
pointerY := &y
var pointerZ *string

pointer

指针的零值是 nil。nil 也是slice、map、函数、channel、interface的零值。这些类型都是指针实现的。nil是一个未定型的标识符,表示某些类型没有值。

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int*int64*string等。

Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。

  • &是地址运算符。它位于值类型之前,并返回存储该值的内存位置的地址。
  • *是间接寻址运算符。它位于指针类型的变量之前,并返回所指向的值。这称为解引用。
1
2
3
4
5
6
7
func main() {
	a := 10
	b := &a
	fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
	fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
	fmt.Println(&b)                    // 0xc00000e018
}

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

指针传值示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func modify1(x int) {
	x = 100
}

func modify2(x *int) {
	*x = 100
}

func main() {
	a := 10
	modify1(a)
	fmt.Println(a) // 10
	modify2(&a)
	fmt.Println(a) // 100
}

new和make

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
	var a *int
	*a = 100
	fmt.Println(*a)

	var b map[string]int
	b["Mike"] = 100
	fmt.Println(b)
}

// panic: runtime error: invalid memory address or nil pointer dereference

执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存。

new

new是一个内置的函数,它的函数签名如下:

1
func new(Type) *Type

其中,

  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:

1
2
3
4
5
6
7
8
func main() {
	a := new(int)
	b := new(bool)
	fmt.Printf("%T\n", a) // *int
	fmt.Printf("%T\n", b) // *bool
	fmt.Println(*a)       // 0
	fmt.Println(*b)       // false
}	

make

make也是用于内存分配的,区别于new,它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

new与make的区别

  1. 二者都是用来做内存分配的。
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

Go是一种传值调用的语言,传递给函数的值是副本。对于基本类型、结构体和数组等非指针类型,这意味着被调用的函数不能修改原始值。

尽管当一个指针被传递给一个函数时,该函数会得到该指针的一个副本。但由于指针仍然指向原始数据,所以原始数据可以被调用的函数修改。

这其中有几个相关的含义:

第一个含义当把一个nil指针传递给一个函数时,不能将这个值改变为非nil的。如果已经对该指针赋值,只能重新赋值。虽然一开始让人困惑,但这是有道理的。因为内存位置是通过传值调用传递给函数的,我们不能改变内存地址,就像我们不能改变int参数的值一样。我们可以用下面的程序来证明这一点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func failedUpdate(g *int) {
    x := 10
    g = &x
}

func main() {
    var f * int // f is nil
    failedUpdate(f)
    fmt.Println(f) // print nil
}

我们在main中以一个nil变量f开始。当调用failedUpdate时,将f的值(即nil)复制到名为g的参数中,这意味着g也被设置为nil。然后在failedUpdate中声明一个新的变量x,值为10。接下来将failedUpdate中的g改为指向x。这并没有改变main中的f,当我们退出failedUpdate并返回main时,f仍然是nil。

第二个含义是,如果你希望对指针参数进行的修改在退出函数时依然有效(即函数修改了指针的值),则必须对指针解引用并设置该值。如果仅仅改变了指针(对指针进行赋值),那么只是改变了指针副本,而不是原指针。解引用将新的值放在原始指针和副本指针都指向的内存位置上。下面是一个简短的程序,展示了它是如何工作的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func failedUpdate(px *int) {
    x2 := 20
    px = &x2
}
func update(px *int) {
    *px = 20
}
func main() {
    x := 10
    failedUpdate(&x)
    fmt.Println(x) // 10
    update(&x)
    fmt.Println(x) // 20
}

💡 指针也是一个变量

如果结构体足够大,使用结构体的指针作为输入参数或返回值可以提高性能。对于所有数据大小来说,将指针传入函数的时间是恒定的,大约是1ns。这不难理解,因为指针的大小对所有数据类型都是一样的。当数据变大时,将值传入函数需要更长的时间。一旦数据达到10MB左右,就需要大约1ms。

在绝大多数情况下,使用指针和数值之间的差异不会大幅影响程序的性能。但是如果你要在函数之间传递兆字节甚至更大的数据,还是考虑使用指针,即使这些数据是不可改变的。