diff --git a/chap-15-Pointer-type/15.md b/chap-15-Pointer-type/15.md new file mode 100644 index 0000000000000000000000000000000000000000..4bcb0d634636293ae84502b617733281b8b8c2e0 --- /dev/null +++ b/chap-15-Pointer-type/15.md @@ -0,0 +1,594 @@ +# Chapter 15 : Pointer type +![](imgs/pointer.5749c110.jpg) + +## 1 在本章中你将学到什么? + +- 什么是指针? + +- 什么是指针类型? + +- 如何创建和使用指针类型的变量? + +- 什么是一个指针类型变量的零值? + +- 什么是解引用? + +- slices, maps, and channels 的特殊性是什么? + +## 2 涵盖的技术概念 + +- 指针 + +- 内存地址 + +- 指针类型 + +- 解引用 + +- 引用 + +## 3 什么是指针? +指针是“指定另一个数据项所在的位置”的数据项。 + +在一个程序中,我们不断的存储和检索数据。例如:字符串,数字,复杂的结构体…… 在物理层面,数据被存储在内存中指定的地址。指针包含这些地址。 + +![](imgs/pointer_schema.c52bfd6e.png) +指针变量 vs “经典”变量 + +请记住一个指针变量像其他任何变量一样也有一个内存地址。 + +## 4 指针类型 +指针类型不是唯一的,指针类型与类型一样多。指针类型表示指向给定类型变量的所有指针的集合。 + +指针类型语法如下: +``` +*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`. + +## 7 解引用 +一个指针变量保存另一个变量的地址。 如果你想要检索地址上的变量怎么做?你可以使用解引用操作符 `*`。 + +让我们举个例子。我们定义了一个结构体类型 `Cart` : +``` +type Cart struct { + ID string + Paid bool +} +``` +然后我们创建一个 `Cart` 类型的变量 `cart` 。我们可以得到这个变量的地址,也可以跟踪地址得到变量:   +![](imgs/dereferencing.857e2c0e.png) +解引用 / 引用   + +![](imgs/take_follow_address.cd62e917.png) +解引用 / 引用   + +* 使用操作符 `*` 你可以跟踪地址得到变量 + +* 使用操作符 `&` 你可以获取变量的地址 + +### 7.1 警告!混淆的风险 + +解引用操作符 `*` 与被用来表示指针类型的符号相同。 `*card` 可以表示一个指针类型,也可以表示一个解引用的指针变量。结合使用上下文仔细分析,你可以很容易区分两者。 + +### 7.2 空指针解引用: runtime panic + +每一个 Go 程序员都会遇到这个 panic : +``` +panic: runtime error: invalid memory address or nil pointer dereference +[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1091507] +``` +为了更好的理解,我们尝试来重现它: +``` +// pointer/nil-pointer-dereference/main.go +package main + +import "fmt" + +func main() { + var myPointerVar *int + fmt.Println(*myPointerVar) +} +``` +在这个程序中,我们定义了一个指针变量 `myPointerVar` 。这是一个 `*int` 的变量(整数指针)。 + +然后我们尝试解引用它。 `myPointerVar` 变量保存一个尚未初始化的指针,因此这个指针的值是 `nil` 。因为我们试图去跟踪一个不存在的地址,这个程序将会 panic !我们试图跳转到 nil 地址。 nil 地址表示根本不存在。 + +## 8 Maps and channels +Maps 和 channels 已经是内部数据结构的引用了。所以,即使参数不是指针类型,接受 map 或 channel 的函数/方法可以修改它。让我们举个例子: +``` +// pointer/maps-channels/main.go +// ... + +func addElement(cities map[string]string) { + cities["France"] = "Paris" +} +``` +* 这个函数以一个 map 为输入 + +* 它增加一个条目到 map (key = “France”, value = “Paris”) + +``` +// pointer/maps-channels/main.go +package main + +import "log" + +func main() { + cities := make(map[string]string) + addElement(cities) + log.Println(cities) +} +``` + +* 我们初始化一个叫 `cities` 的 map + +* 然后调用函数 `addElement` + +* 程序打印如下日志 : +``` +map[France:Paris] +``` + +我们将在其它地方更详细的介绍 map 和 channel。 + +## 9 Slices + +### 9.1 Slice 定义 +切片是相同元素的集合。 + +切片在内部是一个有三个字段的结构体: +* 长度 + +* 容量 + +* 指向内部数组的指针 + +这是一个 slice 例子 `EUcountries`: +``` +package main + +import "log" + +func main() { + EUcountries := []string{"Austria", "Belgium", "Bulgaria"} + log.Println(EUcountries) +} +``` + +### 9.2 以 slice 为参数/接收者的函数/方法: watch your steps. + +#### 9.2.0.1 例 1: 添加元素 + +``` +// pointer/slices-add-elements/main.go +package main + +import "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` 以一个字符串类型的 slice 为参数 + +* 它通过使用內建函数 `append` 追加字符串到参数 slice。 + +* 它追加缺失的欧盟国家到 slice 。 + +**Question** : 你认为程序会输出什么? +``` +[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] +``` +**Answer** : 函数实际上输出: +``` +[Austria Belgium Bulgaria] +``` + +#### 9.2.0.2 说明 + +* 函数以 `[]string` 类型为参数 + +* 当调用函数的时候,Go 会复制 `EUcountries` slice + +* 函数会获得一份 slice 的拷贝 : + + * 长度 + + * 容量 + + * 指向内部数据结构的指针 + +* 在函数内部,国家被有效的增加了 + +* slice 的长度会增大 + +* 运行时会重新分配一个新的内部数组 + +为了更直观让我们在函数中添加日志: +``` +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 Example 2: 更新元素 + +``` +// pointer/slices-update-elements/main.go +package main + +import ( + "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`, 使用 `strings.ToUpper` 将字符串 slice 中的每个元素转换为大写。 + +**Question** : 你认为程序会输出以下哪个? +``` +[AUSTRIA BELGIUM BULGARIA] + +[Austria Belgium Bulgaria] +``` +**Answer** : 这是程序实际输出 : +``` +[AUSTRIA BELGIUM BULGARIA] +``` + +#### 9.2.0.4 说明 +* 函数 `upper` 获取一份 `EUcountries` slice 的拷贝(与之前相同) + +* 在函数中我们修改 slice 元素的值 `countries[k] = strings.ToUpper(countries[k])` + +* slice 的拷贝仍然有一个底层数组的引用 + +* 我们可以修改它! + +* ... 但是只有已经存在 slice 中的元素可以被我们修改 + +#### 9.2.0.5 结论 + +* 当你传递 slice 给一个函数时,函数获得一份 slice 的**拷贝** + +* 这并不意味着你不可以修改 slice + +* 你可以只修改 slice 中已经存在的元素 + +### 9.3 以一个指向 slice 的指针为参数/接收者的函数/方法 + +通过使用指针类型,你可以按照预期那样修改 slice +``` +// pointer/slices-add-elements-pointer/main.go +package main + +import ( + "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` 以指向一个字符串类型的 slice 的指针 (`*[]string`) 做为参数 + +* `*countriesPtr` 做为第一个参数来调用函数 `append` (我们解引用了指针 `countriesPtr`) + +* `append` 的第二个参数没有变化 + +* 结果最后赋值给 `*countriesPtr` + +## 10 指向结构体的指针 + +有一个快捷方式可以让您直接修改 struct 类型的变量而无需取消引用运算符: +``` +cart := Cart{ + ID: "115552221", + CreatedDate: time.Now(), +} +cartPtr := &cart +cartPtr.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` 我们可以直接修改变量 `cart` 的字段`Items` + +* “自动解引用”比等效的解引用更容易编写 + +``` +(*cartPtr).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"。 这里我们没有更改我们的接收者:例如,我们不更改 `Name` 的值。这是另一个将会修改一个 `cat` 的属性的方法: +``` +func (cat *Cat) Rename(newName string){ + cat.Name = newName +} +``` +这个方法会修改猫的名字。因此,指针很有用因为我们会修改结构体 `Cat` 的一个字段。 + +当然,如果你不想使用一个指针接收者,你可以: +``` +func (cat Cat) RenameV2(newName string){ + cat.Name = newName +} +``` + +在这个例子中,变量 `cat` 是一份**拷贝**。接收者被称为“**值接收者**”.因此,你对变量 `cat` 做的任何修改都会发生在 `cat` 拷贝上: +``` +// pointer/methods-pointer-receivers/main.go +package main + +import "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 +} +``` +`main` 函数的第一行,我们创建了一个新的 `cat`,它的名字叫 `"Milow"`。 + +当我们调用**以值为接收者的** `RenameV2` 方法,源头的 `cat` 的名字将不会改变;它将会保持不变。 + +当我们调用 `Rename` 方法, 猫的 `Name` 字段的值将会改变。 + +![](imgs/pointer_value_reciever.8bb9a1c4.png) + +指针接收者 vs. 值接收者 + +#### 11.0.0.1 什么时候使用指针接收者,什么时候使用值接收者 + +* 当以下情况使用指针接收者 : + + * 你的结构体占用内存很多(否则, Go 会拷贝一份它) + + * 你想要修改接收者(例如,你想要修改一个结构体变量的 `name` 字段) + + * 你的结构体包含一个同步原语(如 sync.Mutex)字段。 如果你使用一个值接收者,它也将拷贝 mutex ,使其无用并且引发一个同步错误。 + +* 使用值接收者 + + * 你的结构体比较小 + + * 当你不想要修改接收者时 + + * 当接收者的类型是一个 map, 一个 func, 一个 chan, 一个 slice, 一个 string, 或者 一个 interface (因为内部类型已经是一个指针了) + + * 当你的其它接收者是指针时 + +## 12 自我测试 + +### 12.1 问题 + +1. 如何表示一个持有指向 `Product` 的指针的变量的类型? + +2. 一个指针的零值是什么? + +3. 解引用是什么意思? + +4. 如何解引用一个指针? + +5. 填空。 \_\___ 是一个内部指向 ____ 的指针。 + +6. 判断题。当我想要在一个函数中修改 map,我的函数需要接受一个指向 map 的指针做为参数。我也需要去返回修改后的 map。 + +### 12.2 Answers +1. 如何表示一个持有指向 `Product` 的指针的变量的类型? + + 1. *Product + +2. 一个指针的零值是什么? + + 1. nil + +3. 解引用是什么意思? + + 1. 指针是一块存储数据的内存的地址。 + + 2. 当我们解引用一个指针,我们可以访问存在这个内存地址的数据。 + +4. 如何解引用一个指针? + + 1. 使用解引用操作符 `*` + +5. 填空。 \_\___ 内部是一个指向 ____ 的指针。 + + 1. slice 内部是一个指向 array 的指针 + +6. 判断题。当我想要在一个函数中修改 map,我的函数需要接受一个指向 map 的指针做为参数。我也需要去返回修改后的 map。 + + 1. 错误。 + + 2. 你可以仅接收一个 map (不是一个指向 map 的指针) + + 3. 也不需要返回修改后的 map + +## 13 关键要点 + +* 指针是数据的地址 + +* 类型 `*T` 表示所有指向类型 `T` 的指针的集合。 + +* 你可以使用操作符 `&` 去创建一个指针类型。它将获得一个变量的地址 +``` +userId := 12546584 +p := &userId +``` +`userId` 是一个 `int` 类型的变量 + + * p 是一个 `*int` 类型的变量 + + `*int` 表示所有指向类型 `int` 的指针的结合 + +* 一个有这指针类型的参数/接收者的函数可以修改指针指向的值。 + +* Maps and channels 是“引用类型” + +* 接受 maps 或 channels 的函数/方法可以修改存储在这两个数据结构内部的值(不需要传递指向一个 map 或 channel 的指针) + +* slice 内部持有一个数组的引用; 任何接受一个 slice 的函数/方法可以修改 slice 的元素。 + +* 当你想要在一个函数中修改一个 slice 的长度和容量,你应该传递指向 slice 的指针给函数 (`*[]string`) + +* 解引用允许你访问和修改存储在指针指向的地址中的数据。 + +* 使用操作符 `*` 去解引用一个指针 + + ``` + userId := 12546584 + p := &userId + *p = 4 + log.Println(userId) + ``` +`p` 是一个指针。 + + * 使用 `*p` 我们解引用了指针 `p` + + * 通过使用 `*p = 4` 我们修改了 `userId` 的值 + + * 代码段的最后,`userId` 的值是4 (不再是12546584) + +* 当你有一个指向结构体的指针,你可以使用你的指针变量直接访问一个字段(不需要使用解引用操作符) + + * 例子: +``` +type Cart struct { + ID string +} +var cart Cart +cartPtr := &cart +``` + * 为了替代写法: `(*cartPtr).ID = "1234"` + + * 你可以直接写: `cartPtr.Items = "1234"` + + * `cart` 变量实际上已经被修改了 + +# 参考书目 + * [institute1990ieee] Electrical, Institute of, and Electronics Engineers. 1990. “IEEE Standard Glossary of Software Engineering Terminology: Approved September 28, 1990, IEEE Standards Board.” In. Inst. of Electrical; Electronics Engineers. diff --git a/chap-15-Pointer-type/imgs/dereferencing.857e2c0e.png b/chap-15-Pointer-type/imgs/dereferencing.857e2c0e.png new file mode 100644 index 0000000000000000000000000000000000000000..acbab552769bac37509c711084cf6b34ee6d55d0 Binary files /dev/null and b/chap-15-Pointer-type/imgs/dereferencing.857e2c0e.png differ diff --git a/chap-15-Pointer-type/imgs/pointer.5749c110.jpg b/chap-15-Pointer-type/imgs/pointer.5749c110.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d7b7b5c67ba3fc391441bb6b49512e78a2ea7ef7 Binary files /dev/null and b/chap-15-Pointer-type/imgs/pointer.5749c110.jpg differ diff --git a/chap-15-Pointer-type/imgs/pointer_schema.c52bfd6e.png b/chap-15-Pointer-type/imgs/pointer_schema.c52bfd6e.png new file mode 100644 index 0000000000000000000000000000000000000000..3037f1748bf144d55c7cc27a2d5d00a3e66a9feb Binary files /dev/null and b/chap-15-Pointer-type/imgs/pointer_schema.c52bfd6e.png differ diff --git a/chap-15-Pointer-type/imgs/pointer_value_reciever.8bb9a1c4.png b/chap-15-Pointer-type/imgs/pointer_value_reciever.8bb9a1c4.png new file mode 100644 index 0000000000000000000000000000000000000000..12a591adb66ded81a7479655dc6fa0f35f810942 Binary files /dev/null and b/chap-15-Pointer-type/imgs/pointer_value_reciever.8bb9a1c4.png differ diff --git a/chap-15-Pointer-type/imgs/take_follow_address.cd62e917.png b/chap-15-Pointer-type/imgs/take_follow_address.cd62e917.png new file mode 100644 index 0000000000000000000000000000000000000000..05cc13755b9b5c381aa9321703504a1862053c01 Binary files /dev/null and b/chap-15-Pointer-type/imgs/take_follow_address.cd62e917.png differ