Go 18.0 专业五


指针

<h1>Go 数据类型 指针详解:在什么情况下应该使用指针?</h1> <h2>什么是指针</h2> <hr /> <p>我们都知道<strong>程序运行时的数据是存放在内存中的</strong>,而内存会被抽象为一系列具有连续编号的存储空间,<strong>那么每一个存储在内存中的数据都会有一个编号,这个编号就是内存地址</strong>。<strong>有了这个内存地址就可以找到这个内存中存储的数据</strong>,而内存地址可以被赋值给一个指针。</p> <p>&gt; 小提示:<strong>内存地址通常为 16 进制的数字表示</strong>,比如 0x45b876。 </p> <p>可以总结为:在编程语言中,<strong>指针是一种数据类型,用来存储一个内存地址</strong>,<strong>该地址指向存储在该内存中的对象</strong>。这个<strong>对象可以是字符串、整数、函数或者你自定义的结构体</strong>。 </p> <p>&gt; 小技巧:你也可以简单地把指针理解为内存地址。 </p> <p>举个通俗的例子,每本书中都有目录,目录上会有相应章节的页码,你可以把页码理解为一系列的内存地址,通过页码你可以快速地定位到具体的章节(也就是说,通过内存地址可以快速地找到存储的数据)。</p> <h2>指针的声明和定义</h2> <hr /> <p>在 Go 语言中,获取一个变量的指针非常容易,使用取地址符 &amp; 就可以,比如下面的例子:</p> <pre><code>func main() { name:=&amp;quot;飞雪无情&amp;quot; nameP:=&amp;amp;name//取地址 fmt.Println(&amp;quot;name变量的值为:&amp;quot;,name) fmt.Println(&amp;quot;name变量的内存地址为:&amp;quot;,nameP) }</code></pre> <p>我在示例中定义了一个 string 类型的变量 name,它的值为&quot;飞雪无情&quot;,然后通过<strong>取地址符 &amp; 获取变量 name 的内存地址</strong>,<strong>并赋值给指针变量 nameP,该指针变量的类型为 *string</strong>。运行以上示例你可以看到如下打印结果:</p> <pre><code>name变量的值为: 飞雪无情 name变量的内存地址为: 0xc000010200</code></pre> <p>这一串 0xc000010200 就是内存地址,这个内存地址可以赋值给<a href="https://so.csdn.net/so/search?q=%E6%8C%87%E9%92%88%E5%8F%98%E9%87%8F&amp;amp;spm=1001.2101.3001.7020">指针变量</a> nameP。</p> <p>&gt; <strong>指针类型非常廉价,只占用 4 个或者 8 个字节的内存大小。</strong> </p> <p>以上示例中 nameP 指针的类型是 *string,用于指向 string 类型的数据。<strong>在 Go 语言中使用类型名称前加 * 的方式,即可表示一个对应的指针类型</strong>。比如 int 类型的指针类型是 *int,<a href="https://so.csdn.net/so/search?q=float64&amp;amp;spm=1001.2101.3001.7020">float64</a> 类型的指针类型是 *float64,<strong>自定义结构体 A 的指针类型是 *A</strong>。总之,指针类型就是在对应的类型前加 * 号。</p> <p>下面我通过一个图让你更好地理解普通类型变量、指针类型变量、内存地址、内存等之间的关系。</p> <p><img src="https://img-blog.csdnimg.cn/6a66d525816443c8ad22d0f26b655ed2.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a-M5aOr5bq36LSo5qOA5ZGY5byg5YWo6JuL,size_20,color_FFFFFF,t_70,g_se,x_16" alt="" /></p> <p>                                              (指针变量、内存地址指向示意图)</p> <pre><code>var a intvar p *intp = &amp;amp;afmt.Printf(&amp;quot;%p\n&amp;quot;,&amp;amp;a)fmt.Printf(&amp;quot;%p\n&amp;quot;,p)fmt.Println(a,*p) 0xc0000180900xc0000180900 0</code></pre> <p>上图就是我刚举的例子所对应的示意图,从图中可以看到普通变量 name 的值“飞雪无情”被放到内存地址为 0xc000010200 的内存块中。<strong>指针类型变量也是变量,它也需要一块内存用来存储值</strong>,这块内存对应的地址就是 0xc00000e028,存储的值是 0xc000010200。相信你已经看到关键点了,<strong>指针变量 nameP 的值正好是普通变量 name 的内存地址,所以就建立指向关系</strong>。</p> <p>&gt; 小提示:<strong>指针变量的值就是它所指向数据的内存地址,普通变量的值就是我们具体存放的数据。</strong></p> <p><strong>不同的指针类型是无法相互赋值的</strong>,比如你不能对一个 string 类型的变量取地址然后赋值给 *int指针类型,编译器会提示你 Cannot use '&amp;name' (type *string) as type *int in assignment。 </p> <p>此外,除了可以通过简短声明的方式声明一个指针类型的变量外,也可以使用 var 关键字声明,如下面示例中的 var intP *int 就声明了一个 *int 类型的变量 intP。</p> <pre><code>var intP *int intP = &amp;amp;name //指针类型不同,无法赋值</code></pre> <p>可以看到指针变量也和普通的变量一样,既可以通过 var 关键字定义,也可以通过简短声明定义。</p> <p>&gt; 小提示:<strong>通过 var 声明的指针变量是不能直接赋值和取值的,因为这时候它仅仅是个变量,还没有对应的内存地址,它的值是 nil。</strong></p> <p>和普通类型不一样的是,<strong>指针类型还可以通过内置的 new 函数来声明</strong>,如下所示:</p> <pre><code>intP1:=new(int)</code></pre> <p><strong>内置的 new 函数有一个参数,可以传递类型给它。它会返回对应的指针类型</strong>,比如上述示例中会返回一个 *int 类型的 intP1。</p> <h2>指针的操作</h2> <hr /> <p>在 Go 语言中指针的操作无非是两种:一种是获取指针指向的值,一种是修改指针指向的值。</p> <p>首先介绍如何获取,我用下面的代码进行演示:</p> <pre><code>nameV:=*nameP fmt.Println(&amp;quot;nameP指针指向的值为:&amp;quot;,nameV)</code></pre> <p> 可以看到,<strong>要获取指针指向的值,只需要在指针变量前加 * 号即可</strong>,获得的变量 nameV 的值就是“飞雪无情”,方法比较简单。</p> <p>修改指针指向的值也非常简单,比如下面的例子:</p> <pre><code>*nameP = &amp;quot;公众号:飞雪无情&amp;quot; //修改指针指向的值 fmt.Println(&amp;quot;nameP指针指向的值为:&amp;quot;,*nameP) fmt.Println(&amp;quot;name变量的值为:&amp;quot;,name)</code></pre> <p>对 *nameP 赋值等于修改了指针 nameP 指向的值。运行程序你将看到如下打印输出:</p> <pre><code>nameP指针指向的值为: 公众号:飞雪无情 name变量的值为: 公众号:飞雪无情</code></pre> <p>通过打印结果可以看到,不光 nameP 指针指向的值被改变了,变量 name 的值也被改变了,这就是指针的作用。<strong>因为变量 name 存储数据的内存就是指针 nameP 指向的内存,这块内存被 nameP 修改后,变量 name 的值也被修改了。</strong></p> <p>我们已经知道,<strong>通过 var 关键字直接定义的指针变量是不能进行赋值操作的,因为它的值为 nil,也就是还没有指向的内存地址</strong>。比如下面的示例:</p> <pre><code>var intP *int *intP =10</code></pre> <p>运行的时候会提示 invalid memory address or nil pointer dereference。这时候该怎么办呢?其实只需要通过 new 函数给它分配一块内存就可以了,如下所示:</p> <pre><code>var intP *int = new(int) //更推荐简短声明法,这里是为了演示 //intP:=new(int)</code></pre> <h2>指针参数</h2> <hr /> <p>假如有一个函数 modifyAge,想要用来修改年龄,如下面的代码所示。但运行它,你会看到 age 的值并没有被修改,还是 18,并没有变成 20。 </p> <pre><code>age:=18 modifyAge(age) fmt.Println(&amp;quot;age的值为:&amp;quot;,age) func modifyAge(age int) { age = 20 }</code></pre> <p>导致这种结果的原因是 <strong>modifyAge 中的 age 只是实参 age 的一份拷贝,所以修改它不会改变实参 age 的值。</strong></p> <p>如果要达到修改年龄的目的,就需要使用指针,现在对刚刚的示例进行改造,如下所示:</p> <pre><code>age:=18 modifyAge(&amp;amp;age) fmt.Println(&amp;quot;age的值为:&amp;quot;,age) func modifyAge(age *int) { *age = 20}</code></pre> <p>也就是说,<strong>当你需要在函数中通过形参改变实参的值时,需要使用指针类型的参数。</strong> </p> <h2>指针接收者</h2> <hr /> <p>指针的接收者在[“第 6 讲| struct 和 interface:结构体与接口都实现了哪些功能?”](<a href="https://kaiwu.lagou.com/course/courseInfo.htm?courseId=536#/detail/pc?id=5232">https://kaiwu.lagou.com/course/courseInfo.htm?courseId=536#/detail/pc?id=5232</a> &quot;“第 6 讲| struct 和 interface:结构体与接口都实现了哪些功能?”&quot;)中有详细介绍,你可以再复习一下。对于是否使用指针类型作为接收者,有以下几点参考:</p> <ol> <li> <p><strong>如果接收者类型是 map、slice、channel 这类引用类型,不使用指针</strong>;</p> </li> <li> <p>如果需要修改接收者,那么需要使用指针;</p> </li> <li>如果接收者是比较大的类型,可以考虑使用指针,因为内存拷贝廉价,所以效率高。</li> </ol> <p>所以对于是否使用指针类型作为接收者,还需要你根据实际情况考虑。</p> <h3>什么情况下使用指针</h3> <p>从以上指针的详细分析中,我们可以总结出指针的两大好处:</p> <ol> <li> <p>可以修改指向数据的值;</p> </li> <li>在变量赋值,参数传值的时候可以节省内存。</li> </ol> <p>不过 Go 语言作为一种高级语言,在指针的使用上还是比较克制的。它在设计的时候就对指针进行了诸多限制,比如指针不能进行运行,也不能获取常量的指针。所以在思考是否使用时,我们也要保持克制的心态。</p> <p>我根据实战经验总结了以下几点使用指针的建议,供你参考:</p> <ol> <li> <p><strong>不要对 map、slice、channel 这类引用类型使用指针;</strong></p> </li> <li> <p>如果需要修改方法接收者内部的数据或者状态时,需要使用指针;</p> </li> <li> <p>如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;</p> </li> <li> <p>如果是<strong>比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针</strong>;</p> </li> <li> <p>像 int、bool 这样的<strong>小数据类型没必要使用指针</strong>;</p> </li> <li> <p>如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;</p> </li> <li><strong>指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。</strong></li> </ol> <h3>总结</h3> <p>为了使编程变得更简单,指针在高级的语言中被逐渐淡化,但是它也的确有自己的优势:修改数据的值和节省内存。所以在 Go 语言的开发中我们要尽可能地使用值类型,而不是指针类型,因为值类型可以使你的开发变得更简单,并且也是并发安全的。如果你想使用指针类型,就要参考我上面讲到的使用指针的条件,看是否满足,要在满足和必须的情况下才使用指针。</p> <p>这节课到这里就要结束了,在这节课的最后同样给你留个思考题:指向接口的指针是否实现了该接口?为什么?思考后可以自己写代码验证下。</p> <p>下节课将为你深入讲解值类型,引用类型,指针类型之间的关系和区别。</p> <h1>Go指针</h1> <hr /> <p>虽然Go吸收融合了很多其语言中的各种特性,但是Go主要被归入C语言家族。其中一个重要的原因就是Go和C一样,也支持指针。 当然Go中的指针相比C指针有很多限制。本篇将介绍指针相关的各种概念和Go指针相关的各种细节</p> <h2>指针与变量</h2> <hr /> <p>变量是一种使用方便的占位符,用于引用计算机内存地址。可以通过&amp;运算符获取, 指针是用来存储变量地址的变量</p> <pre><code>a := &amp;quot;string a&amp;quot;fmt.Println(&amp;amp;a) // 0xc000044770</code></pre> <p><img src="https://img-blog.csdnimg.cn/img_convert/77e3ed07865fc4f7cf2dc432a93f1ed3.png" alt="pointer_addr_var" /></p> <h2>声明初始化与赋值</h2> <hr /> <p>1.指针声明需要指定存储地址中对应数据的类型,并使用*作为类型前缀。</p> <pre><code>var ip *int /* 指向整型*/var fp *float32 /* 指向浮点型 */</code></pre> <p><img src="https://img-blog.csdnimg.cn/img_convert/eb240629bf705b2e5867daa889c72ca0.png" alt="pointer_del" /></p> <p>2.指针<strong>变量声明后会被初始化为 nil,表示空指针</strong></p> <pre><code>var a *intfmt.Println(a) // nil</code></pre> <p>3.使用 new 函数初始化:new 函数根据数据类型申请内存空间并使用零值填充,并返回申请空间地址</p> <pre><code>var a *int = new(int)fmt.Println(a) // 0xc000014330fmt.Println(*a) // 0</code></pre> <p><img src="https://img-blog.csdnimg.cn/img_convert/5afdf54d7ab9e3e39d9392a7e81e5beb.png" alt="pointer_del" /></p> <p>4.指针赋值</p> <pre><code>var a *int = new(int)*a = 10fmt.Println(a, *a)</code></pre> <p><img src="https://img-blog.csdnimg.cn/img_convert/8e381dcb938c156bbf8d0a90d9cc5dd9.png" alt="pointer_del" /></p> <h2>指针运算</h2> <hr /> <ul> <li>&amp;: 获取变量的指针</li> <li>*: 获取指针指向的值</li> </ul>

页面列表

ITEM_HTML