关于golang指针的理解与使用

栏目: IT技术 · 发布时间: 4年前

内容简介:Go 有指针,但是没有指针运算。你不能用指针变量遍历字符串的各个字节。在 Go 中调用函数的时候,得记得变量是值传递的。通过类型作为前缀来定义一个指针’ * ’:var p * int。现在 p 是一个指向整数值的指针。所有新定义的变量都被赋值为其类型的零值,而指针也一样。一个新定义的或者没有任何指向的指针,有值 nil。在其他语言中,这经常被叫做空(NULL)指针,在 Go 中就是 nil 。让指针指向某些内容,可以使用取址操作符 ( & ),像这样:前面已经说了,没有指针运算,所以如果这样写:

Go指针理解

Go 有指针,但是没有指针运算。你不能用指针变量遍历字符串的各个字节。在 Go 中调用函数的时候,得记得变量是值传递的。

通过类型作为前缀来定义一个指针’ * ’:var p * int。现在 p 是一个指向整数值的指针。所有新定义的变量都被赋值为其类型的零值,而指针也一样。一个新定义的或者没有任何指向的指针,有值 nil。在其他语言中,这经常被叫做空(NULL)指针,在 Go 中就是 nil 。让指针指向某些内容,可以使用取址操作符 ( & ),像这样:

package main 
 
import "fmt"
 
func main() {
    var p *int 
    fmt.Printf("%v\n",p) //← 打印 nil
 
    var i int //← 定义一个整形变量 i
    p = &i    //← 使得 p 指向 i, 获取 i 的地址
    fmt.Printf("%v\n",p) //打印内存地址
 
    *p = 6
    fmt.Printf("%v\n",*p) //打印6
    fmt.Printf("%v\n",i) //打印6
 
}

前面已经说了,没有指针运算,所以如果这样写: p++ ,它表示 ( p)++ :首先获取指针指向的值,然后对这个值加一。这里注意与 C语言 的区别。

对于Go语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。

当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。当这样理解的时候,我们就可以理解成Go总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。

指针与普通变量区别

刚学习golang的人可能不是很明白普通变量和指针的区别,看到有人说其实普通变量是程序创造出来的,比如说 c 中

a int

那么在编译时 就会有 [a 地址 int] 这样来标识内存

普通变量a其实是语言本身创造了,是为了更方便的表示内存。我们对a进行访问其实就是直接对内存进行访问。至于a表示的内存的地址是多少,程序员一般不用关心。编译器会自动分配地址,也就是常说的为a分配一个地址。如果想知道a的地址也可以通过&a得知。

打个比方来理解:普通变量就像是房间(内存)外面的门牌号(总经理室),指针就是这个房间的地址(9#905)

变量是运行时系统给这个内存起的别名,内存地址是唯一的,程序中当我想拿到这个内存的值的时候,因为知道它的别名,所以直接用别名访问就可以得到值,又或者我能知道它唯一的地址我也能得到它的值,其实是两种不同的内存访问方式,但是变量是会变的,地址是不会变的。比如在go中

package main
 
func main(){
    a := 10   //此时有一块内存存放了10,它的地址由系统自动分配,别名是a
    a = 20   //内存存放的10变成了20
    var p *int
    p = &a   //或者直接写 p := &a
    //上面的p是一个指针,通过 *p 的方式同样可以访问 变量a指向 的内存
 
    /*当你动态申请内存的时候,指针的存在意义之一就被体现出来了*/ 
    ptr := new(int)   
    //申请了一块内存空间,没有办法指定别名,new()返回内存地址,用指针接收
    //此时并没有变量能直接指向这块内存,所以只能通过内存地址来访问
}

看到有个解释还可以,是说指针和普通变量区别的

1+2这个表达式永远得值3

a+b这个表达式只依赖于a,b的值—–按名字访问称为直接访问

*p + *q 这个表达式的值随着p,q指向的变量不同而不同—-按指针访问称为间接访问

指针的使用

方法中的指针

方法即为有接受者的函数,接受者可以是类型的实例变量或者是类型的实例指针变量。但两种效果不同。

1、类型的实例变量

func main(){
    person := Person{"TigerwolfC", 25}
    fmt.Printf("person<%s:%d>\n", person.name, person.age)
    person.sayHi()
    person.ModifyAge(28)
    person.sayHi()
}
type Person struct {
    name string
    age int
}
func (p Person) sayHi() {
    fmt.Printf("SayHi -- This is %s, my age is %d\n",p.name, p.age)
}
func (p Person) ModifyAge(age int) {
    fmt.Printf("ModifyAge")
    p.age = age
}

输出结果

person<TigerwolfC:25>
SayHi -- This is TigerwolfC, my age is 25
ModifyAgeSayHi -- This is TigerwolfC, my age is 25

尽管 ModifyAge 方法修改了其age字段,可是方法里的p是person变量的一个副本,修改的只是副本的值。下一次调用sayHi方法的时候,还是person的副本,因此修改方法并不会生效。即实例变量的方式并不会改变接受者本身的值。

2、类型的实例指针变量

package main
 
import "fmt"
 
func main(){
    person := Person{"TigerwolfC", 25}
    fmt.Printf("person<%s:%d>\n", person.name, person.age)
    person.sayHi()
    person.ModifyAge(28)
    person.sayHi()
}
 
type Person struct {
    name string
    age int
}
 
func (p *Person) sayHi() {
    fmt.Printf("SayHi -- This is %s, my age is %d\n",p.name, p.age)
}
 
func (p *Person) ModifyAge(age int) {
    fmt.Printf("ModifyAge")
    p.age = age
}

输出结果

person<TigerwolfC:25>
SayHi -- This is TigerwolfC, my age is 25
ModifyAgeSayHi -- This is TigerwolfC, my age is 28

Go会根据Person的示例类型,转换成指针类型再拷贝,即 person.ModifyAge会变成 (&person).ModifyAge。

指针类型的接受者,如果实例对象是值,那么go会转换成指针,然后再拷贝,如果本身就是指针对象,那么就直接拷贝指针实例。因为指针都指向一处值,就能修改对象了。

3.如何选择 P 和 *P

在定义函数和方法的时候,需要对函数的参数和返回值定义成P和

P深思熟虑,有些情况下可能还会有些困惑。

那么什么时候才应该把参数定义成类型P,什么情况下定义成类型

P呢。

一般的判断标准是看副本创建的成本和需求。

不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型P。相反,如果想修改原始的变量,则选择

P

P,只创建新的指针,这个区别是巨大的

(不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成P,Go编译器尽量将对象分配到栈上,而*P很可能会分配到对象上,这对垃圾回收会有影响

4. 零值与nil(空指针)

变量声明而没有赋值,默认为零值,不同类型零值不同,例如字符串零值为空字符串;

指针声明而没有赋值,默认为nil,即该指针没有任何指向。当指针没有指向的时候,不能对(*point)进行操作包括读取,否则会报空指针异常。

func main(){
    // 声明一个指针变量 aPot 其类型也是 string
    var aPot *string
    fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)
    *aPot = "This is a Pointer"  // 报错: panic: runtime error: invalid memory address or nil pointer dereference
}

解决方法即给该指针分配一个指向,即初始化一个内存,并把该内存地址赋予指针变量,例如:// 声明一个指针变量 aPot 其类型也是 string

var aPot *string
  fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)
  aPot = &aVar
  *aPot = "This is a Pointer"
  fmt.Printf("aVar: %p %#v \n", &aVar, aVar) // 输出 aVar: 0xc42000e240 "This is a Pointer"
  fmt.Printf("aPot: %p %#v %#v \n", &aPot, aPot, *aPot) // 输出 aPot: 0xc42000c030 (*string)(0xc42000e240) "This is a Pointer"

或者通过new开辟一个内存,并返回这个内存的地址。

var aNewPot *int
aNewPot = new(int)
*aNewPot = 666
fmt.Printf("aNewPot: %p %#v %#v \n", &aNewPot, aNewPot, *aNewPot) // 输出 aNewPot: 0xc42007a028 (*int)(0xc42006e1f0) 666

总结

Golang提供了指针用于操作数据内存,并通过引用来修改变量。

只声明未赋值的变量,golang都会自动为其初始化为零值,基础数据类型的零值比较简单,引用类型和指针的零值都为nil,nil类型不能直接赋值,因此需要通过new开辟一个内存,或者通过make初始化数据类型,或者两者配合,然后才能赋值。

指针也是一种类型,不同于一般类型,指针的值是地址,这个地址指向其他的内存,通过指针可以读取其所指向的地址所存储的值。

函数方法的接受者,也可以是指针变量。无论普通接受者还是指针接受者都会被拷贝传入方法中,不同在于拷贝的指针,其指向的地方都一样,只是其自身的地址不一样。

欢迎关注我们的微信公众号,每天学习Go知识

关于golang指针的理解与使用

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员代码面试指南:IT名企算法与数据结构题目最优解

程序员代码面试指南:IT名企算法与数据结构题目最优解

左程云 / 电子工业出版社 / 2015-9 / 79.00元

这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮助广大程序员的面试准备做到万无一失。“刷”完本书后,你就是“题王”!__eol__本书采用题目+解答的方式组织内容,并把面试题类型相近或者解法相近的题目尽量放在一起,读者在学习本书时很容易看出面试题解法之间的联......一起来看看 《程序员代码面试指南:IT名企算法与数据结构题目最优解》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试