资讯专栏INFORMATION COLUMN

第十五章:指针类型

不知名网友 / 2687人阅读

摘要:指针类型的零值指针类型的零值指针类型的零值都是,也就是说,一个没有存储地址的指针等于解除引用解除引用一个指针变量持有另一个变量的地址。

image

本篇翻译自《Practical Go Lessons》 Chapter 15: Pointer type

1 你将在本章将学到什么?

  • 什么是指针?
  • 什么时指针类型?
  • 如何去创建并使用一个指针类型的变量。
  • 指正类型变量的零值是什么?
  • 什么是解除引用?
  • slices, maps, 和 channels 有什么特殊的地方?

2 涵盖的技术概念

  • 指针
  • 内存地址
  • 指针类型
  • 解除引用
  • 引用

3 什么是指针?

指针是“是一个数据项,它存储另外一个数据项的位置”。
在程序中,我们不断地存储和检索数据。例如,字符串、数字、复杂结构…。在物理层面,数据存储在内存中的特定地址,而指针存储的就是这些特定内存地址。

image

记住指针变量,就像其他变量一样,它也有一个内存地址。

4 指针类型

Go 中的指针类型不止一种,每一种普通类型就对应一个指针类型。相应地,指针类型也限定了它自己只能指向对应类型的普通变量(地址)。

指针类型的语法为:

*BaseType

BaseType指代的是任何普通类型。

我们来看一下例子:

  • *int 表示指向 int 类型的指针
  • *uint8 表示指向 uint8 类型的指针
type User struct {	ID string	Username string}
  • *User 表示指向 User 类型的指针

5 如何去创建一个指针类型变量?

下面的语法可以创建:

var p *int

这里我们创建了一个类型为 *int 的变量 p*int 是指针类型(基础类型是 int)。

让我们来创建一个名为 answer 的整型变量。

var answer int = 42

现在我们给变量 p 分配一个值了:

p = &answer

使用 & 符号我们就能得到变 answer地址。来打印出这个地址~

fmt.Println(p)// 0xc000012070

0xc000012070 是一个十六进制数字,因为它的以 0x 为前缀。内存地址通常是以十六进制格式表示。你也可以使用二进制(用 0 和 1)表示,但不易读。

6 指针类型的零值

指针类型的零值都是 nil,也就是说,一个没有存储地址的指针等于 nil

var q *intfmt.Println(q == nil)// true

7 解除引用

一个指针变量持有另一个变量的地址。如果你想通过指针去访问地址背后的变量值该怎么办?你可以使用解除引用操作符 *

来举个例子,我们定义一个结构体类型 Cart

type Cart struct {	ID string	Paid bool}

然后我们创建一个 Cart 类型的变量 cart,我们可以得到这个变量的地址,也可以通过地址找到这个变量:
image
image

  • 使用 * 操作符,你可以通过地址找到变量值
  • 使用 & 操作符,你可以得到变量的地址

7.1 空指针解引用:运行时 panic

每个 Go 程序员都会遇到这个 panic(报错):

panic: runtime error: invalid memory address or nil pointer dereference[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1091507]

为了更好地理解它,我们来复现一下:

package mainimport "fmt"func main() {    var myPointerVar *int    fmt.Println(*myPointerVar)}

在程序里,我们的定义了一个指针变量 myPointerVar,这个变量的类型是 *int(指向整型)。

然后我尝试对它进行解引用,myPointerVar 变量持有一个尚未初始化的指针,因此该指针的值为 nil。因为我们尝试去寻找一个不存在的地址,程序将会报错!我们尝试找到空地址,而空地址在内存中不存在。

8 Maps 和 channels

Maps 和 channels 变量里保存了对内部结构的指针。因此,即便向一个函数或方法传递的 map 或 channel 不是指针类型,也开始对这个 map 或 channel 进行修改。让我们看一个例子:

func addElement(cities map[string]string) {    cities["France"] = "Paris"}
  • 这个函数将一个 map 作为输入
  • 它向 map 中添加一项数据(key = "France", value = "Paris")
package mainimport "log"func main() {    cities := make(map[string]string)    addElement(cities)    log.Println(cities)}
  • 我们初始化一个名为 cities 的 map
  • 然后调用函数 addElement
  • 程序打印出:
map[France:Paris]

我们将在专门的部分中更广泛地介绍 channels 和 maps。

9 切片

9.1 切片定义

切片是相同类型元素的集合。在内部,切片是一个具有三个字段的结构:

  • length:长度
  • capacity:容量
  • pointer:执向内部数组的指针
    下面是一个关于切片 EUcountries 的例子:
package mainimport "log"func main() {    EUcountries := []string{"Austria", "Belgium", "Bulgaria"}    log.Println(EUcountries)}

9.2 函数或方法将切片作为参数或接收器:小心

9.2.0.1 Example1: 向切片添加元素

package mainimport "log"func main() {    EUcountries := []string{"Austria", "Belgium", "Bulgaria"}    addCountries(EUcountries)    log.Println(EUcountries)}func addCountries(countries []string) {    countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)}
  • 函数 addCountries 将一个字符串类型切片作为参数
  • 它通过内建函数 append 向切片添加字符串来修改切片
  • 它将缺失的欧盟国家附加到切片中
    问题:依你看,程序的输出将会是下面的哪个?
[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden][Austria Belgium Bulgaria]

答案:这个函数实际输出:

[Austria Belgium Bulgaria]

9.2.0.2 解释

  • 这个函数将[]string类型元素作为参数
  • 当函数被调用时,Go 会将切片 EUcountries 拷贝一份传进去
  • 函数将得到一个拷贝的切片数据:
    • 长度
    • 容量
    • 指向底层数据的指针
  • 在函数内部,缺失的国家被添加了进去
  • 切片的长度会增加
  • 运行时将分配一个新的内部数组

让我们在函数中添加一个日志来可视化它:

func addCountries(countries []string) {    countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)    log.Println(countries)}

日志打印出:

[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
  • 这里的改变只会影响拷贝的版本

9.2.0.3 Example2:更新元素

package mainimport (    "log"    "strings")func main() {    EUcountries := []string{"Austria", "Belgium", "Bulgaria"}    upper(EUcountries)    log.Println(EUcountries)}func upper(countries []string) {    for k, _ := range countries {        countries[k] = strings.ToUpper(countries[k])    }}
  • 我们添加新函数 upper,它将把一个字符串切片的每个元素都转换成大写

问题:依你看,程序将传输下面哪个?

[AUSTRIA BELGIUM BULGARIA][Austria Belgium Bulgaria]

答案:这个函数将返回:

[AUSTRIA BELGIUM BULGARIA]

9.2.0.4 解释

  • 函数 upper 获取切片 EUcountries 的副本(和上面一样)
  • 在函数内部,我们更改切片元素的值 countries[k] = strings.ToUpper(countries[k])
  • 切片副本仍然有对底层数组的引用
  • 我们可以修改!
  • .. 但只有已经在切片中的切片元素。

9.2.0.5 结论

  • 当你将切片传递给函数时,它会获取切片的副本。
  • 这并不意味着你不能修改切片。
  • 你只可以修改切片中已经存在的元素。

9.3 函数或方法将切片指针作为参数或接收器

如果使用切片指针,你就可以在函数中修改这个切片了:

package mainimport (    "log")func main() {    EUcountries := []string{"Austria", "Belgium", "Bulgaria"}    addCountries2(&EUcountries)    log.Println(EUcountries)}func addCountries2(countriesPtr *[]string) {    *countriesPtr = append(*countriesPtr, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)}

这个程序将输出:

[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
  • 函数 addCountries2 将字符串切片的指针([]string)作为参数
  • 函数 append 调用时的第一个参数是 *countriesPtr(即我们通过指针 countriesPtr 去找到原值)
  • append 的第二个参数没有改变
  • 函数 addCountries2 的结果会影响到外部的变量

10 指向结构体的指针

有一个快捷方式可以让你直接修改 struct 类型的变量而无需使用*运算符:

type Item struct {	SKU string	Quantity int}type Cart struct {	ID string	CreatedDate time.Time	Items Item}cart := Cart{    ID:          "115552221",    CreatedDate: time.Now(),}cartPtr := &cartcartPtr.Items = []Item{    {SKU: "154550", Quantity: 12},    {SKU: "DTY8755", Quantity: 1},}log.Println(cart.Items)// [{154550 12} {DTY8755 1}]
  • cart 是一个 Cart 类型变量
  • cartPtr := &cart 会获取变量 cart 的地址然后将其存储到 cartPtr
  • 使用变量 cartPtr,我们可以直接修改变量 cartItem 字段
  • 这是因为运行时自动通过结构体指针找到了原值进行了修改,以下是等价的写法
(*carPtr).Items = []Item{    {SKU: "154550", Quantity: 12},    {SKU: "DTY8755", Quantity: 1},}

(这也有效,但更冗长)

11 使用指针作为方法的接收器

指针通常用作方法的接收器,让我们以 Cat 类型为例:

type Cat struct {  Color string  Age uint8  Name string}

你可以定义一个方法,使用指向 Cat 的指针作为方法的接收器(*Cat):

func (cat *Cat) Meow(){  fmt.Println("Meooooow")}

Meow 方法没有做任何有实际意义的事吗;它只是打印了字符串"Meooooow"。我们没有修改比变量的值。我们来看另一个方法,它修改了 cat 的 Name

func (cat *Cat) Rename(newName string){  cat.Name = newName}

此方法将更改猫的名称。通过指针,我们修改了 Cat 结构体的一个字段。

当然,如果你不想使用指针作为接收器,你也可以:

func (cat Cat) RenameV2(newName string){  cat.Name = newName}

在这个例子中,变量 cat 是一个副本。接收器被命名为“值接收器”。因此,你对 cat 变量所做的任何修改都将在 cat 副本上完成:

package mainimport "fmt"type Cat struct {    Color string    Age   uint8    Name  string}func (cat *Cat) Meow() {    fmt.Println("Meooooow")}func (cat *Cat) Rename(newName string) {    cat.Name = newName}func (cat Cat) RenameV2(newName string) {    cat.Name = newName}func main() {    cat := Cat{Color: "blue", Age: 8, Name: "Milow"}    cat.Rename("Bob")    fmt.Println(cat.Name)    // Bob    cat.RenameV2("Ben")    fmt.Println(cat.Name)    // Bob}

在主函数的第一行,我们创建了一个 Cat 类型的变量 cat,它的 Name 是 "Millow"
当我们调用具有值接收器RenameV2 方法时,函数外部变量 cat 的 Name 没有发生改变。
当我们调用 Rename 方法时,cat 的 Name 字段值会发生变化。
image

11.1 何时使用指针接收器,何时使用值接收器

  • 以下情况使用指针接收器:
    • 你的结构体很大(如果使用值接收器,Go 会复制它)
    • 你想修改接收器(例如,你想更改结构变量的名称字段)
    • 你的结构包含一个同步原语(如sync.Mutex)字段。如果你使用值接收器,它还会复制互斥锁,使其无用并导致同步错误。
    • 当接收器是一个 map、func、chan、slice、string 或 interface值时(因为在内部它已经是一个指针)
    • 当你的接收器是持有指针时

12 随堂测试

12.1 问题

  1. 如何去表示一个持有指向 Product 指针的变量?
  2. 指针类型的零值是多少?
  3. "解引用(dereferencing)" 是什么意思?
  4. 如何解引用一个指针?
  5. 填空: ____ 在内部是一个指向 ____ 的指针。
  6. 判断正误:当我想函数中修改 map 时,我的函数需要接收一个指向 map 的指针作为参数,我还需要返回修改后的 map?

12.2 答案

  1. 如何去表示一个持有指向 Product 指针的变量?
    *Product
  2. 指针类型的零值是多少?
    nil
  3. "解引用(dereferencing)" 是什么意思?
    • 指针是指向存储数据的内存位置的地址。
    • 当我们解引用一个指针时,我们可以访问存储在该地址的内存中的数据。
  4. 如何解引用一个指针?
    使用解引用操作符 *
  5. 填空: ____ 在内部是一个指向 ____ 的指针。
    slice 在内部是一个指向 array 的指针。
  6. 判断正误:当我想函数中修改 map 时,我的函数需要接收一个指向 map 的指针作为参数,我还需要返回修改后的 map
    错, 函数中只要接收一个 map 类型参数就行,也不需要返回更改后的map,因为 map 变量内部存储了指向底层数据的指针

关键要点

  • 指针是指向数据的地址
  • 类型 *T 表示所有指向 T 类型变量的指针集合
  • 创建指针变量,可以使用运算符&。它将获取一个变量的地址
userId := 12546584p := &userId
`userId` 是 `int` 类型的变量`p` 是 `*int` 类型变量`*int` 表示所有指向 `int` 类型变量的指针
  • 具有指针类型的参数/接收器的函数可以修改指针指向的值。
  • map 和 channel 是“引用类型”
  • 接收 map 或 channel 的函数/方法可以修改内部存储在这两个数据结构中的值(无需传递指向 map 的指针或指向 channel 的指针)
  • 切片在内部保存对数组的引用;任何接收切片的函数/方法都可以修改切片元素。
  • 当你想在函数中修改切片长度和容量时,你应该向该函数传递一个指向切片的指针 (*[]string)
  • 解引用允许你访问和修改存储在指针地址处的值。
  • 要对指针进行解引用操作,请使用运算符 *
userId := 12546584p := &userId*p = 4log.Println(userId)

p 是一个指针

  • 我们使用 *p 来对指针 p 进行解引用
  • 我们用指令 *p = 4 修改 userId 的值
  • 在代码片段的末尾,userId 的值为 4(不再是 12546584)
  • 当你有一个指向结构的指针时,你可以直接使用你的指针变量访问一个字段(不需要使用解引用运算符)
    • 例子:
type Cart struct {    ID string}var cart CartcartPtr := &cart
  • 不需要这样写:(*cartPtr).ID = "1234"
  • 你可直接这样写:cartPtr.Items = "1234"
  • 变量 cart 就会被修改

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/125672.html

相关文章

  • 第十五章 输入输出系统

    摘要:在包下主要包括输入输出两种流,每种输入输出流又可分为字节流和字符流两大类。输入输出是从程序运行所在的内存的角度而言的。的输入流主要由和作为基类,而输出流主要由和作为基类。 本章主要参考和摘自疯狂java讲义上面的(java编程思想的后面看过后有新的内容再补充进去吧)。  输入输出是所有程序都必需的部分————使用输入机制允许程序读取外部数据(包括磁盘、光盘等存储设备上的数据和用户输入的...

    hankkin 评论0 收藏0
  • 《On Java 8》中文版,又名《Java 编程思想》中文第五版

    摘要:基于版本基于版本。由于中英行文差异,完全的逐字逐句翻译会很冗余啰嗦。译者在翻译中同时参考了谷歌百度有道翻译的译文以及编程思想第四版中文版的部分内容对其翻译死板,生造名词,语言精炼度差问题进行规避和改正。 来源:LingCoder/OnJava8 主译: LingCoder 参译: LortSir 校对:nickChenyx E-mail: 本书原作者为 [美] Bru...

    BingqiChen 评论0 收藏0
  • 新书《AngularJS半知半解》预热!

    摘要:是目前最热门的一种前端开发框架。对于前端工程师来说,掌握这门炙手可热的技术是完全有必要的。虽然目前已出,但是官方并不会放弃版本,还会持续维护更新,而且掌握的基本知识能更快的帮助我们迈入。 AngularJS是目前最热门的一种前端开发框架。对于前端工程师来说,掌握这门炙手可热的技术是完全有必要的。本书会将作者掌握的AngularJS知识倾囊相授,并从学以致用的角度出发,用实例详细地讲解各...

    ymyang 评论0 收藏0

发表评论

0条评论

不知名网友

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<