【Golang】指针(Pointers)

Posted by 西维蜀黍 on 2020-01-30, Last Modified on 2021-09-21

Background

Although Go absorbs many features from all kinds of other languages, Go is mainly viewed as a C family language. One evidence is Go also supports pointers. Go pointers and C pointers are much similar in many aspects, but there are also some differences between Go pointers and C pointers. This article will list all kinds of concepts and details related to pointers in Go.

Memory Addresses

A memory address means a specific memory location in programming.

Generally, a memory address is stored as an unsigned native (integer) word. The size of a native word is 4 (bytes) on 32-bit architectures and 8 (bytes) on 64-bit architectures. So the theoretical maximum memory space size is 232 bytes, a.k.a. 4GB (1GB == 230 bytes), on 32-bit architectures, and is 264 bytes a.k.a 16EB (1EB == 1024PB, 1PB == 1024TB, 1TB == 1024GB), on 64-bit architectures.

Memory addresses are often represented with hex integer literals, such as 0x1234CDEF.

指针(Pointers)

我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。

Go 语言的取地址符是 &,放到一个变量前使用就会返回这个变量当前存储在的内存地址。

  • Dereference operator: 在一个指针类型的变量前面加上 * 号(前缀)来获取指针所指向的内存地址中存储的内容
  • 在一个非指针类型的变量前面加上 & 来获取这个变量所存储的内存地址的值

以下实例演示了变量在内存中地址:

package main

import "fmt"

func main() {
	var a int = 10 //声明非指针变量

	fmt.Printf("变量 a 的内存地址: %p\n", &a)
  fmt.Printf("变量 a 的内存地址: %v\n", &a)

	var b *int //声明指针变量
	b = &a     
  
  fmt.Printf("指针变量 b 所指向的内存地址的地址值: %v\n", ip)
  fmt.Printf("指针变量 b 所指向的内存地址的地址值: %p\n", ip)
	fmt.Printf("指针变量 b 所指向的地址存储的值: %v\n", *ip)
  fmt.Printf("指针变量 b 本身当前所在的内存地址的值: %v\n", &ip)
  fmt.Printf("指针变量 b 本身当前所在的内存地址的值: %p\n", &ip)  
  // -> 对一个地址才使用 %p
}

执行以上代码输出结果为:

变量 a 的内存地址: 0xc0000ae000
变量 a 的内存地址: 0xc0000ae000
指针变量 b 所指向的内存地址的地址值: 0xc0000ae000
指针变量 b 所指向的内存地址的地址值: 0xc0000ae000
指针变量 b 所指向的地址存储的值: 10
指针变量 b 本身当前所在的内存地址的值: 0xc0000b6000
指针变量 b 本身当前所在的内存地址的值: 0xc0000b6000

什么是指针(指针变量)

一个指针变量保存了一个值的内存地址。

类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:

var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

本例中这是一个指向 int 和 float32 的指针。

Go 空指针( nil)- 指针的默认值

当一个指针变量被定义后,没有将其指向任何变量时,它的值为 nil。

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

一个指针变量通常缩写为 ptr。

查看以下实例:

实例

package main

import "fmt"

func main() {
	var ptr *int

	fmt.Printf("ptr 的值为 : %x\n", ptr)
	fmt.Printf("ptr 的值为 : %v\n", ptr == nil)
}

以上实例输出结果为:

ptr 的值为 : 0
ptr 的值为 : true

Go 语言指针作为函数参数

Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。

以下实例演示了如何向函数传递指针,并在函数调用后修改函数内的值,:

实例

package main

import "fmt"

func main() {
  */\* 定义局部变量 \*/*
  var a int = 100
  var b int= 200

  fmt.Printf("交换前 a 的值 : %d\n", a )
  fmt.Printf("交换前 b 的值 : %d\n", b )

  */\* 调用函数用于交换值
  \* &a 指向 a 变量的地址
  \* &b 指向 b 变量的地址
  \*/*
  swap(&a, &b);

  fmt.Printf("交换后 a 的值 : %d\n", a )
  fmt.Printf("交换后 b 的值 : %d\n", b )
}

func swap(x *int, y *int) {
  var temp int
  temp = *x   */\* 保存 x 地址的值 \*/*
  *x = *y    */\*  y 赋值给 x \*/*
  *y = temp   */\*  temp 赋值给 y \*/*
}

以上实例允许输出结果为:

交换前 a 的值 : 100
交换前 b 的值 : 200
交换后 a 的值 : 200
交换后 b 的值 : 100

& - 取地址符

我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。

Go 语言的取地址符是 &,放到一个变量前使用就会返回当前变量的内存地址。

以下实例演示了变量在内存中地址:

package main

import "fmt"

func main() {
  var a int = 10 

  fmt.Printf("变量的地址: %x\n", &a  )
}

执行以上代码输出结果为:

变量的地址: 20818a220

指针变量

一个指针变量保存了一个值的内存地址,也就是说,指针变量保存的是一个内存地址

与使用变量和常量类似,在使用指针前,你需要声明指针。指针声明格式如下:

var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定该变量是作为一个指针变量。

以下均是正确的指针变量声明方式:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

本例中这是一个指向 int 和 float32 的指针。

A value of a pointer type whose base type is T can only store the addresses of values of type T.

How to Get a Pointer Value and What Are Addressable Values?

There are two ways to get a non-nil pointer value.

  1. The built-in new function can be used to allocate memory for a value of any type. new(T) will allocate memory for a T value (an anonymous variable) and return the address of the T value. The allocated value is a zero value of type T. The returned address is viewed as a pointer value of type *T.
  2. We can also take the addresses of values which are addressable in Go. For an addressable value t of type T, we can use the expression &t to take the address of t, where & is the operator to take value addresses. The type of &t is viewed as *T.

Pointer Dereference

Given a pointer value p of a pointer type whose base type is T, how can you get the value at the address stored in the pointer (a.k.a., the value being referenced by the pointer)? Just use the expression *p, where * is called dereference operator. *p is called the dereference of pointer p. Pointer dereference is the inverse process of address taking. The result of *p is a value of type T (the base type of the type of p).

Dereferencing a nil pointer causes a runtime panic.

Return Pointers of Local Variables Is Safe in Go

Unlike C language, Go is a language supporting garbage collection, so return the address of a local variable is absolutely safe in Go.

func newInt() *int {
	a := 3
	return &a
}

Example

Example 1

package main

import "fmt"

func main() {
	var p *int
	p = new(int)
	*p = 1
	fmt.Println(p, &p, *p)
}

//输出
0xc04204a080  0xc042068018  1
  • 在 Go 中 * 代表取指针变量中存储的内存地址中存储的值,& 代表取一个变量的内存地址
  • 对于指针,我们一定要记住:指针储存的是一个内存地址值,但本身这个指针本身也需要地址来储存
    • 如上 p 是一个指针(指针变量),它存储的值为 0xc04204a080(是一个内存地址),或者说它指向了 0xc04204a080 内存地址
    • 而这个指针变量 p 是一个变量,因此存储该变量本身自然也需要占用内存,而变量存储在 0xc042068018(内存地址)
    • 内存地址 0xc04204a080 储存的值为 1
地址 0xc042068018 0xc04204a080
0xc04204a080 1

Example 2 - 错误实例

如果我们在声明一个指针变量时,像普通变量那样给他赋值:

package main

import "fmt"
func main() {
	var i *int
	*i = 1
  fmt.Println(i, &i, *i)
}

就会报以下错误:

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x498025]
  • 报这个错的原因是 go 在初始化指针的时候,会将指针变量值赋为 nil ;
  • 由于指针变量 i 还没指向一个有效的内存空间,就执行了 *i = 1(这行代码的含义是:将指针变量 i 指向的内存空间位置的值设为 1);
  • 因此,go 的执行器就不知道要将哪个内存空间位置的值设为 1了;
  • 解决这个问题非常简单,在给指针赋值前可以先创建一块内存空间,并让指针对象指向这块内存空间:
package main

import "fmt"
func main() {
	var i *int
	i = new(int)
	*i = 1
	fmt.Println(i, &i, *i)
}

Example 3 - 指向对象的指针变量

package main

import "fmt"

type User struct {
	UserName string
}

func main() {
	user := User{UserName: "user1"}
	fmt.Println(user)
	fmt.Printf("1 %p\n", &user)

	tryToModify(user)

	fmt.Println(user)
	fmt.Printf("3 %p\n", &user)
}

func tryToModify(u User) {
	fmt.Println(u)
	fmt.Printf("2 %p\n", &u)

	u.UserName = "user2"
}

//output
{user1}
1 0xc000010060
{user1}
2 0xc000010080
{user1}
3 0xc000010060

我们来分析一下:

  • 首先,user 是一个 User 对象(注意,user 不是指针变量),它,这个对象的空间地址为 0xc000010060;
  • 当调用 tryToModify(user)时,会隐式执行 u = user ,这意味着:我们把之前的这个 User 对象复制一份(其实复制到了 0xc000010080),新的这个 User 对象叫 u,它位于 0xc000010080;
  • 显然,修改叫 u 的这个新的 User 对象的 UserName 属性(u.UserName = "user2")并不能影响之前那个叫 user 的 User 对象的值。

自然地,你想知道,如何在一个函数中修改对象的属性:

package main

import "fmt"

type User struct {
	UserName string
}

func main() {
	user := User{UserName: "user1"}
	fmt.Println(user)
	fmt.Printf("1 %p\n", &user)

	tryToModify(&user)

	fmt.Println(user)
	fmt.Printf("3 %p\n", &user)
}

func tryToModify(u *User) {
	fmt.Println(u)
	fmt.Printf("2 %p\n", u)

	u.UserName = "user2"
}

//output
{user1}
1 0xc000010200
&{user1}
2 0xc000010200
{user2}
3 0xc000010200
  • 我们需要将 u 声明为一个指针变量(即 func tryToModify(u *User) ) ;
  • 同时,在调用 tryToModify 时,需要提供 user 对象的地址(而不是提供 user 对象,因为并不能将一个对象赋值给一个指针变量,而只能将一个空间地址赋值给一个指针变量),因此改为 tryToModify(&user)

这样,我们就实现了在一个函数内修改对象的属性。

Restrictions on Pointers in Go

For safety reasons, Go makes some restrictions to pointers (comparing to pointers in C language). By applying these restrictions, Go keeps the benefits of pointers, and avoids the dangerousness of pointers at the same time.

Go pointer values don’t support arithmetic operations

In Go, pointers can’t do arithmetic operations. For a pointer p, p++ and p-2 are both illegal.

If p is a pointer to a numeric value, compilers will view *p++ is a legal statement and treat it as (*p)++. In other words, the precedence of the pointer dereference operator * is higher than the increment operator ++ and decrement operator --.

A pointer value can’t be converted to an arbitrary pointer type

In Go, a pointer value of pointer type T1 can be directly and explicitly converted to another pointer type T2 only if either of the following two conditions is get satisfied.

The underlying types of type T1 and T2 are identical (ignoring struct tags), in particular if either T1 and T2 is a non-defined type and their underlying types are identical (considering struct tags), then the conversion can be implicit.

// In the following example. type alias C and type literal []string both represent the same non-defined types, but type A and type alias B both represent the same defined type.
type A []string
type B = A
type C = []string

Type T1 and T2 are both non-defined pointer types and the underlying types of their base types are identical (ignoring struct tags).

For example, for the below shown pointer types:

type MyInt int64
type Ta    *int64
type Tb    *MyInt

the following facts exist:

  1. values of type *int64 can be implicitly converted to type Ta, and vice versa, for their underlying types are both *int64.
  2. values of type *MyInt can be implicitly converted to type Tb, and vice versa, for their underlying types are both *MyInt.
  3. values of type *MyInt can be explicitly converted to type *int64, and vice versa, for they are both non-defined and the underlying types of their base types are both int64.
  4. values of type Ta can’t be directly converted to type Tb, even if explicitly. However, by the just listed first three facts, a value pa of type Ta can be indirectly converted to type Tb by nesting three explicit conversions, Tb((*MyInt)((*int64)(pa))).

None values of these pointer types can be converted to type *uint64, in any safe ways.

A pointer value can’t be compared with values of an arbitrary pointer type

In Go, pointers can be compared with == and != operators. Two Go pointer values can only be compared if either of the following three conditions are satisfied.

  1. The types of the two Go pointers are identical.
  2. One pointer value can be implicitly converted to the pointer type of the other. In other words, the underlying types of the two types must be identical and either of the two types of the two Go pointers must be an undefined type.
  3. One and only one of the two pointers is represented with the bare (untyped) nil identifier.

Example:

package main

func main() {
	type MyInt int64
	type Ta    *int64
	type Tb    *MyInt

	// 4 nil pointers of different types.
	var pa0 Ta
	var pa1 *int64
	var pb0 Tb
	var pb1 *MyInt

	// The following 6 lines all compile okay.
	// The comparison results are all true.
	_ = pa0 == pa1
	_ = pb0 == pb1
	_ = pa0 == nil
	_ = pa1 == nil
	_ = pb0 == nil
	_ = pb1 == nil

	// None of the following 3 lines compile ok.
	/*
	_ = pa0 == pb0
	_ = pa1 == pb1
	_ = pa0 == Tb(nil)
	*/
}

A pointer value can’t be assigned to pointer values of other pointer types

The conditions to assign a pointer value to another pointer value are the same as the conditions to compare a pointer value to another pointer value, which are listed above.

map 和 slice 均为指针类型

在 Go 中,没有值传递和引用传递的区别。或者说,如果没有显式地进行引用传递,那所有的赋值操作都是值传递(即使是对于复杂数据类型,但是除了 map 和 slice)。

“所有的赋值操作都是值传递”,这意味着,对于复杂数据类型,如果在将其传递到函数内部时,没有使用指针,则该复杂数据类型会被完全复制一份(在上面的例子中就体现出来了),因而在函数内部对该复杂数据类型的修改,并不会影响到函数外部(因为其实存在两个该复杂数据类型实例)。

package main

import "fmt"

func main() {
  // int
	a := 1
	b := &a
	fmt.Printf("a: %p, b: %p\n", &a, b) // a: 0xc0000b0000, b: 0xc0000b0000
	fmt.Printf("a: %p, b: %p", &a, &b)  // a: 0xc0000b0000, b: 0xc0000b2000

  // array
	array1 := [3]int{1, 2, 3}
	array2 := array1

	fmt.Printf("array1: %p, array2: %p\n", &array1, &array2)   // array1: 0xc0000ac000, array2: 0xc0000ac020
	array1[0] = 0
	fmt.Printf("array1: %v, array2: %v", array1, array2)    // array1: [0 2 3], array2: [1 2 3]
  
  // slice case 1
	sliceA := []int{1, 2, 3}
	sliceA[0] = 0
	sliceA[1] = 1

	sliceB := sliceA
	fmt.Printf("sliceA: %p, sliceB: %p\n", sliceA, sliceB)  // sliceA: 0xc0000aa000, sliceB: 0xc0000aa000
	sliceB[0] = 100
	fmt.Printf("sliceA: %v, sliceB: %v\n", sliceA, sliceB)  // sliceA: [100 1 3], sliceB: [100 1 3]
	fmt.Printf("sliceA: %p, sliceB: %p", &sliceA, &sliceB)  // sliceA: 0xc0000a8000, sliceB: 0xc0000a8020
  
  // slice case 2  
  sliceC :=  make([]int, 3)
	sliceC[0] = 100
	sliceC[1] = 200

	sliceD := sliceC
	fmt.Printf("sliceC: %p, sliceD: %p\n", sliceC, sliceD)   // sliceC: 0xc00001a180, sliceD: 0xc00001a180
	sliceD[0] = 400
	fmt.Printf("sliceC: %v, sliceD: %v\n", sliceC, sliceD)   // sliceC: [400 200 0], sliceD: [400 200 0]
	fmt.Printf("sliceC: %p, sliceD: %p", &sliceC, &sliceD)   // sliceC: 0xc00000c040, sliceD: 0xc00000c060
  
  // map
	m1 := make(map[int]int)
	m1[0] = 100
	m1[1] = 200

	m2 := m1
	fmt.Printf("m1: %p, m2: %p\n", m1, m2) // m1: 0xc0000a0030, m2: 0xc0000a0030
	m2[0] = 400
	fmt.Printf("m1: %v, m2: %v\n", m1, m2) // m1: map[0:400 1:200], m2: map[0:400 1:200]
	fmt.Printf("m1: %p, m2: %p", &m1, &m2) // m1: 0xc00000e018, m2: 0xc00000e020
}

奇怪的报错

package main

import "fmt"

func main() {
	a := A{}
	var b []int32 = a.GetB()
	doSomething(&b)           // 正确
	doSomething(&(a.GetB())   // 报错
	doSomething(&a.GetB())	  // 报错
}

type A struct {
}

func (a *A) GetB() []int32 {
	return []int32{1, 2}
}

func doSomething(tmp *[]int32) {
	fmt.Println(tmp)
}

区别 - var res []interface{}res = make([]interface{}, 0)

	a := make([]int, 0)
	var b []int

	fmt.Printf("a: %v\n", a == nil)
	fmt.Printf("b: %v\n", b == nil)

Reference