提交 3e003ab1 编写于 作者: 写代码的明哥's avatar 写代码的明哥

update

上级 7862c4b4
文件模式从 100755 更改为 100644
...@@ -24,7 +24,7 @@ func main() { ...@@ -24,7 +24,7 @@ func main() {
} }
``` ```
> 明哥注:fmt.Printf 中的 %c 表示输入为单个字符,详情请查看:[5.1 fmt.Printf 方法详解](http://golang.iswbm.com/en/latest/c05/c05_01.html) > 明哥注:fmt.Printf 中的 %c 表示输入为单个字符,详情请查看:[5.1 fmt.Printf 方法详解](http://golang.iswbm.com/c05/c05_01.html)
在 ASCII 表中,由于字母 A 的ASCII 的编号为 65 ,字母 B 的ASCII 编号为 66,所以上面的代码也可以写成这样 在 ASCII 表中,由于字母 A 的ASCII 的编号为 65 ,字母 B 的ASCII 编号为 66,所以上面的代码也可以写成这样
......
...@@ -31,8 +31,7 @@ ...@@ -31,8 +31,7 @@
.. ..
明哥注:fmt.Printf 中的 %c 表示输入为单个字符,详情请查看:\ `5.1 明哥注:fmt.Printf 中的 %c 表示输入为单个字符,详情请查看:\ `5.1
fmt.Printf fmt.Printf 方法详解 <http://golang.iswbm.com/c05/c05_01.html>`__
方法详解 <http://golang.iswbm.com/en/latest/c05/c05_01.html>`__
在 ASCII 表中,由于字母 A 的ASCII 的编号为 65 ,字母 B 的ASCII 编号为 在 ASCII 表中,由于字母 A 的ASCII 的编号为 65 ,字母 B 的ASCII 编号为
66,所以上面的代码也可以写成这样 66,所以上面的代码也可以写成这样
......
...@@ -4,11 +4,11 @@ ...@@ -4,11 +4,11 @@
前面写过两节关于 `switch-case` 的文章,分别是: 前面写过两节关于 `switch-case` 的文章,分别是:
[流程控制:switch-case](http://golang.iswbm.com/en/latest/c01/c01_09.html) [流程控制:switch-case](http://golang.iswbm.com/c01/c01_09.html)
[Go 语言中的类型断言](http://golang.iswbm.com/en/latest/c01/c01_14.html) [Go 语言中的类型断言](http://golang.iswbm.com/c01/c01_14.html)
今天要学习一个跟 `switch-case` 很像,但还有点`个人特色``select-case`,这一节本应该放在 [学习 Go 协程:详解信道/通道](http://golang.iswbm.com/en/latest/c04/c04_03.html) 里一起讲的,但是当时漏了,直到有读者给我提出,才注意到,今天就用这篇文章补充一下。 今天要学习一个跟 `switch-case` 很像,但还有点`个人特色``select-case`,这一节本应该放在 [学习 Go 协程:详解信道/通道](http://golang.iswbm.com/c04/c04_03.html) 里一起讲的,但是当时漏了,直到有读者给我提出,才注意到,今天就用这篇文章补充一下。
跟 switch-case 相比,select-case 用法比较单一,它仅能用于 信道/通道 的相关操作。 跟 switch-case 相比,select-case 用法比较单一,它仅能用于 信道/通道 的相关操作。
......
...@@ -5,14 +5,13 @@ ...@@ -5,14 +5,13 @@
前面写过两节关于 ``switch-case`` 的文章,分别是: 前面写过两节关于 ``switch-case`` 的文章,分别是:
`流程控制:switch-case <http://golang.iswbm.com/en/latest/c01/c01_09.html>`__ `流程控制:switch-case <http://golang.iswbm.com/c01/c01_09.html>`__
`Go `Go 语言中的类型断言 <http://golang.iswbm.com/c01/c01_14.html>`__
语言中的类型断言 <http://golang.iswbm.com/en/latest/c01/c01_14.html>`__
今天要学习一个跟 ``switch-case`` 很像,但还有点\ ``个人特色`` 今天要学习一个跟 ``switch-case`` 很像,但还有点\ ``个人特色``
``select-case``\ ,这一节本应该放在 `学习 Go ``select-case``\ ,这一节本应该放在 `学习 Go
协程:详解信道/通道 <http://golang.iswbm.com/en/latest/c04/c04_03.html>`__ 协程:详解信道/通道 <http://golang.iswbm.com/c04/c04_03.html>`__
里一起讲的,但是当时漏了,直到有读者给我提出,才注意到,今天就用这篇文章补充一下。 里一起讲的,但是当时漏了,直到有读者给我提出,才注意到,今天就用这篇文章补充一下。
switch-case 相比,select-case 用法比较单一,它仅能用于 信道/通道 switch-case 相比,select-case 用法比较单一,它仅能用于 信道/通道
......
...@@ -185,7 +185,7 @@ hello is string ...@@ -185,7 +185,7 @@ hello is string
此外,还有两点需要你格外注意 此外,还有两点需要你格外注意
1. 类型断言,仅能对静态类型为空接口(interface{})的对象进行断言,否则会抛出错误,具体内容可以参考:[关于接口的三个"潜规则"](http://golang.iswbm.com/en/latest/c02/c02_07.html) 1. 类型断言,仅能对静态类型为空接口(interface{})的对象进行断言,否则会抛出错误,具体内容可以参考:[关于接口的三个"潜规则"](http://golang.iswbm.com/c02/c02_07.html)
2. 类型断言完成后,实际上会返回静态类型为你断言的类型的对象,而要清楚原来的静态类型为空接口类型(interface{}),这是 Go 的隐式转换。 2. 类型断言完成后,实际上会返回静态类型为你断言的类型的对象,而要清楚原来的静态类型为空接口类型(interface{}),这是 Go 的隐式转换。
## 参考文章 ## 参考文章
......
...@@ -194,7 +194,7 @@ Type Switch ...@@ -194,7 +194,7 @@ Type Switch
此外,还有两点需要你格外注意 此外,还有两点需要你格外注意
1. 类型断言,仅能对静态类型为空接口(interface{})的对象进行断言,否则会抛出错误,具体内容可以参考:\ `关于接口的三个“潜规则” <http://golang.iswbm.com/en/latest/c02/c02_07.html>`__ 1. 类型断言,仅能对静态类型为空接口(interface{})的对象进行断言,否则会抛出错误,具体内容可以参考:\ `关于接口的三个“潜规则” <http://golang.iswbm.com/c02/c02_07.html>`__
2. 类型断言完成后,实际上会返回静态类型为你断言的类型的对象,而要清楚原来的静态类型为空接口类型(interface{}),这是 2. 类型断言完成后,实际上会返回静态类型为你断言的类型的对象,而要清楚原来的静态类型为空接口类型(interface{}),这是
Go 的隐式转换。 Go 的隐式转换。
......
...@@ -151,7 +151,7 @@ Convert ...@@ -151,7 +151,7 @@ Convert
通过上一节的内容([关于接口的三个 『潜规则』](http://golang.iswbm.com/en/latest/c02/c02_07.html)),我们知道了一个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。也就是说在真实世界里,type 和 value 是合并在一起组成 接口变量的。 通过上一节的内容([关于接口的三个 『潜规则』](http://golang.iswbm.com/c02/c02_07.html)),我们知道了一个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。也就是说在真实世界里,type 和 value 是合并在一起组成 接口变量的。
而在反射的世界里,type 和 data 却是分开的,他们分别由 reflect.Type 和 reflect.Value 来表现。 而在反射的世界里,type 和 data 却是分开的,他们分别由 reflect.Type 和 reflect.Value 来表现。
...@@ -219,7 +219,7 @@ func main() { ...@@ -219,7 +219,7 @@ func main() {
等等,上面我们定义的 age 不是 int 类型的吗?第一法则里怎么会说是接口类型的呢? 等等,上面我们定义的 age 不是 int 类型的吗?第一法则里怎么会说是接口类型的呢?
关于这点,其实在上一节([关于接口的三个 『潜规则』](http://golang.iswbm.com/en/latest/c02/c02_07.html))已经提到过了,由于 TypeOf 和 ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go 语言函数都是值传递,因此Go语言会将我们的类型隐式地转换成接口类型。 关于这点,其实在上一节([关于接口的三个 『潜规则』](http://golang.iswbm.com/c02/c02_07.html))已经提到过了,由于 TypeOf 和 ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go 语言函数都是值传递,因此Go语言会将我们的类型隐式地转换成接口类型。
```go ```go
// TypeOf returns the reflection Type of the value in the interface{}.TypeOf returns nil. // TypeOf returns the reflection Type of the value in the interface{}.TypeOf returns nil.
...@@ -292,7 +292,7 @@ func main() { ...@@ -292,7 +292,7 @@ func main() {
从反射对象到接口变量新对象的类型为 int 值为 25 从反射对象到接口变量新对象的类型为 int 值为 25
``` ```
当然了,最后转换后的对象,静态类型为 `interface{}` ,如果要转成最初的原始类型,需要再类型断言转换一下,关于这点,我已经在上一节里讲解过了,你可以点此前往复习:([关于接口的三个 『潜规则』](http://golang.iswbm.com/en/latest/c02/c02_07.html))。 当然了,最后转换后的对象,静态类型为 `interface{}` ,如果要转成最初的原始类型,需要再类型断言转换一下,关于这点,我已经在上一节里讲解过了,你可以点此前往复习:([关于接口的三个 『潜规则』](http://golang.iswbm.com/c02/c02_07.html))。
```go ```go
i := v.Interface().(int) i := v.Interface().(int)
......
...@@ -158,7 +158,7 @@ value,只不过在反射对象里,它们拥有更多的内容。 ...@@ -158,7 +158,7 @@ value,只不过在反射对象里,它们拥有更多的内容。
Convert Convert
通过上一节的内容(\ `关于接口的三个 通过上一节的内容(\ `关于接口的三个
『潜规则』 <http://golang.iswbm.com/en/latest/c02/c02_07.html>`__\ ),我们知道了一个接口变量,实际上都是由一 『潜规则』 <http://golang.iswbm.com/c02/c02_07.html>`__\ ),我们知道了一个接口变量,实际上都是由一
pair 对(type data)组合而成,pair pair 对(type data)组合而成,pair
对中记录着实际变量的值和类型。也就是说在真实世界里,type value 对中记录着实际变量的值和类型。也就是说在真实世界里,type value
是合并在一起组成 接口变量的。 是合并在一起组成 接口变量的。
...@@ -238,7 +238,7 @@ Go 语言里有个反射三定律,是你在学习反射时,很重要的参 ...@@ -238,7 +238,7 @@ Go 语言里有个反射三定律,是你在学习反射时,很重要的参
类型的吗?第一法则里怎么会说是接口类型的呢? 类型的吗?第一法则里怎么会说是接口类型的呢?
关于这点,其实在上一节(\ `关于接口的三个 关于这点,其实在上一节(\ `关于接口的三个
『潜规则』 <http://golang.iswbm.com/en/latest/c02/c02_07.html>`__\ )已经提到过了,由于 『潜规则』 <http://golang.iswbm.com/c02/c02_07.html>`__\ )已经提到过了,由于
TypeOf ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go TypeOf ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go
语言函数都是值传递,因此Go语言会将我们的类型隐式地转换成接口类型。 语言函数都是值传递,因此Go语言会将我们的类型隐式地转换成接口类型。
...@@ -320,7 +320,7 @@ TypeOf 和 ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go ...@@ -320,7 +320,7 @@ TypeOf 和 ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go
当然了,最后转换后的对象,静态类型为 ``interface{}`` 当然了,最后转换后的对象,静态类型为 ``interface{}``
,如果要转成最初的原始类型,需要再类型断言转换一下,关于这点,我已经在上一节里讲解过了,你可以点此前往复习:(\ `关于接口的三个 ,如果要转成最初的原始类型,需要再类型断言转换一下,关于这点,我已经在上一节里讲解过了,你可以点此前往复习:(\ `关于接口的三个
『潜规则』 <http://golang.iswbm.com/en/latest/c02/c02_07.html>`__\ )。 『潜规则』 <http://golang.iswbm.com/c02/c02_07.html>`__\ )。
.. code:: go .. code:: go
......
...@@ -223,8 +223,8 @@ empty = tty ...@@ -223,8 +223,8 @@ empty = tty
关于 反射 的内容有点多,我将其安排在另外两篇文章:。 关于 反射 的内容有点多,我将其安排在另外两篇文章:。
- [学习反射:反射三定律](http://golang.iswbm.com/en/latest/c02/c02_07.html) - [学习反射:反射三定律](http://golang.iswbm.com/c02/c02_07.html)
- [学习反射:全面学习反射的函数](http://golang.iswbm.com/en/latest/c02/c02_08.html) - [学习反射:全面学习反射的函数](http://golang.iswbm.com/c02/c02_08.html)
## 参考文章 ## 参考文章
......
...@@ -251,8 +251,8 @@ eface,其只有一个 ``_type`` 可以存放变量类型,此时 empty ...@@ -251,8 +251,8 @@ eface,其只有一个 ``_type`` 可以存放变量类型,此时 empty
关于 反射 的内容有点多,我将其安排在另外两篇文章:。 关于 反射 的内容有点多,我将其安排在另外两篇文章:。
- `学习反射:反射三定律 <http://golang.iswbm.com/en/latest/c02/c02_07.html>`__ - `学习反射:反射三定律 <http://golang.iswbm.com/c02/c02_07.html>`__
- `学习反射:全面学习反射的函数 <http://golang.iswbm.com/en/latest/c02/c02_08.html>`__ - `学习反射:全面学习反射的函数 <http://golang.iswbm.com/c02/c02_08.html>`__
参考文章 参考文章
-------- --------
......
...@@ -81,7 +81,7 @@ func main() { ...@@ -81,7 +81,7 @@ func main() {
### 多个类型不一致的参数 ### 多个类型不一致的参数
上面那个例子中,我们的参数类型都是 int,如果你希望传多个参数且这些参数的类型都不一样,可以指定类型为 `...interface{}` (你可能会问 interface{} 是什么类型,它是空接口,也是一个很重要的知识点,可以点 [这篇文章](http://golang.iswbm.com/en/latest/c02/c02_05.html)查看相关内容),然后再遍历。 上面那个例子中,我们的参数类型都是 int,如果你希望传多个参数且这些参数的类型都不一样,可以指定类型为 `...interface{}` (你可能会问 interface{} 是什么类型,它是空接口,也是一个很重要的知识点,可以点 [这篇文章](http://golang.iswbm.com/c02/c02_05.html)查看相关内容),然后再遍历。
比如下面这段代码,是Go语言标准库中 fmt.Printf() 的函数原型: 比如下面这段代码,是Go语言标准库中 fmt.Printf() 的函数原型:
...@@ -192,7 +192,7 @@ Go语言中的函数,在你定义的时候,就规定了此函数 ...@@ -192,7 +192,7 @@ Go语言中的函数,在你定义的时候,就规定了此函数
## 6. 方法与函数 ## 6. 方法与函数
方法,在之前的《[2.1 面向对象:结构体与继承](http://golang.iswbm.com/en/latest/c02/c02_01.html)》里已经介绍过了,它的定义与函数有些不同,你可以点击前面的标题进行交叉学习。 方法,在之前的《[2.1 面向对象:结构体与继承](http://golang.iswbm.com/c02/c02_01.html)》里已经介绍过了,它的定义与函数有些不同,你可以点击前面的标题进行交叉学习。
**方法和函数有什么区别?** 为防会有朋友第一次接触面向对象,这里多嘴一句。 **方法和函数有什么区别?** 为防会有朋友第一次接触面向对象,这里多嘴一句。
......
...@@ -97,7 +97,7 @@ a,b,并规定必须返回一个int类型的值 。 ...@@ -97,7 +97,7 @@ a,b,并规定必须返回一个int类型的值 。
int,如果你希望传多个参数且这些参数的类型都不一样,可以指定类型为 int,如果你希望传多个参数且这些参数的类型都不一样,可以指定类型为
``...interface{}`` (你可能会问 interface{} ``...interface{}`` (你可能会问 interface{}
是什么类型,它是空接口,也是一个很重要的知识点,可以点 是什么类型,它是空接口,也是一个很重要的知识点,可以点
`这篇文章 <http://golang.iswbm.com/en/latest/c02/c02_05.html>`__\ 查看相关内容),然后再遍历。 `这篇文章 <http://golang.iswbm.com/c02/c02_05.html>`__\ 查看相关内容),然后再遍历。
比如下面这段代码,是Go语言标准库中 fmt.Printf() 的函数原型: 比如下面这段代码,是Go语言标准库中 fmt.Printf() 的函数原型:
...@@ -205,7 +205,7 @@ Go语言中的函数,在你定义的时候,就规定了此函数 ...@@ -205,7 +205,7 @@ Go语言中的函数,在你定义的时候,就规定了此函数
------------- -------------
方法,在之前的《\ `2.1 方法,在之前的《\ `2.1
面向对象:结构体与继承 <http://golang.iswbm.com/en/latest/c02/c02_01.html>`__\ 》里已经介绍过了,它的定义与函数有些不同,你可以点击前面的标题进行交叉学习。 面向对象:结构体与继承 <http://golang.iswbm.com/c02/c02_01.html>`__\ 》里已经介绍过了,它的定义与函数有些不同,你可以点击前面的标题进行交叉学习。
那 **方法和函数有什么区别?** 那 **方法和函数有什么区别?**
为防会有朋友第一次接触面向对象,这里多嘴一句。 为防会有朋友第一次接触面向对象,这里多嘴一句。
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
![](http://image.iswbm.com/20200607145423.png) ![](http://image.iswbm.com/20200607145423.png)
在 「[4.3 学习 Go 协程:详解信道/通道](http://golang.iswbm.com/en/latest/c04/c04_03.html)」这一节里我详细地介绍信道的一些用法,要知道的是在 Go 语言中,信道的地位非常高,它是 first class 级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。 在 「[4.3 学习 Go 协程:详解信道/通道](http://golang.iswbm.com/c04/c04_03.html)」这一节里我详细地介绍信道的一些用法,要知道的是在 Go 语言中,信道的地位非常高,它是 first class 级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。
今天就来讲一讲 Golang 中的锁机制。 今天就来讲一讲 Golang 中的锁机制。
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
|image0| |image0|
\ `4.3 学习 Go \ `4.3 学习 Go
协程:详解信道/通道 <http://golang.iswbm.com/en/latest/c04/c04_03.html>`__\ 」这一节里我详细地介绍信道的一些用法,要知道的是在 协程:详解信道/通道 <http://golang.iswbm.com/c04/c04_03.html>`__\ 」这一节里我详细地介绍信道的一些用法,要知道的是在
Go 语言中,信道的地位非常高,它是 first class Go 语言中,信道的地位非常高,它是 first class
级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那 级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那
Golang 中的锁机制,就是你绕不过的知识点了。 Golang 中的锁机制,就是你绕不过的知识点了。
......
...@@ -24,9 +24,11 @@ func english(name string) string { ...@@ -24,9 +24,11 @@ func english(name string) string {
// 转换成 Greeting 类型的函数对象 // 转换成 Greeting 类型的函数对象
greet := Greeting(english) greet := Greeting(english)
// 或者
var greet Greeting = english
``` ```
greet 做为 Greeting 类型的对象,自然也拥有 Greeting 类型的所有方法,比如下面的 say 方法 greet 做为 Greeting 类型的对象,也拥有 Greeting 类型的所有方法,比如下面的 say 方法
```go ```go
func (g Greeting) say(n string) { func (g Greeting) say(n string) {
......
...@@ -28,8 +28,10 @@ Greeting 类型的函数对象(也即 greet) ...@@ -28,8 +28,10 @@ Greeting 类型的函数对象(也即 greet)
// 转换成 Greeting 类型的函数对象 // 转换成 Greeting 类型的函数对象
greet := Greeting(english) greet := Greeting(english)
// 或者
var greet Greeting = english
greet 做为 Greeting 类型的对象,自然也拥有 Greeting greet 做为 Greeting 类型的对象,也拥有 Greeting
类型的所有方法,比如下面的 say 方法 类型的所有方法,比如下面的 say 方法
.. code:: go .. code:: go
......
...@@ -327,7 +327,7 @@ func main(){ ...@@ -327,7 +327,7 @@ func main(){
有的朋友 可能会对 `(*sliceValue)(p)` 这行代码有所疑问,这是什么意思呢? 有的朋友 可能会对 `(*sliceValue)(p)` 这行代码有所疑问,这是什么意思呢?
关于这个,其实之前在 【[2.9 详细图解:静态类型与动态类型](http://golang.iswbm.com/en/latest/c02/c02_09.html#id2)】有讲过,忘记了可以前往复习。 关于这个,其实之前在 【[2.9 详细图解:静态类型与动态类型](http://golang.iswbm.com/c02/c02_09.html#id2)】有讲过,忘记了可以前往复习。
## 6. 长短选项 ## 6. 长短选项
......
...@@ -355,7 +355,7 @@ flag.Value接口即可 ...@@ -355,7 +355,7 @@ flag.Value接口即可
这行代码有所疑问,这是什么意思呢? 这行代码有所疑问,这是什么意思呢?
关于这个,其实之前在 \ `2.9 关于这个,其实之前在 \ `2.9
详细图解:静态类型与动态类型 <http://golang.iswbm.com/en/latest/c02/c02_09.html#id2>`__\ 】有讲过,忘记了可以前往复习。 详细图解:静态类型与动态类型 <http://golang.iswbm.com/c02/c02_09.html#id2>`__\ 】有讲过,忘记了可以前往复习。
6. 长短选项 6. 长短选项
----------- -----------
......
...@@ -21,3 +21,4 @@ age := 18 ...@@ -21,3 +21,4 @@ age := 18
- `=` 是赋值 - `=` 是赋值
- `:=` 是声明并赋值 - `:=` 是声明并赋值
一个变量只能声明一次,使用多次 `:=` 是不允许的,而当你声明一次后,却可以赋值多次,没有限制。
...@@ -21,3 +21,6 @@ ...@@ -21,3 +21,6 @@
- ``=`` 是赋值 - ``=`` 是赋值
- ``:=`` 是声明并赋值 - ``:=`` 是声明并赋值
一个变量只能声明一次,使用多次 ``:=``
是不允许的,而当你声明一次后,却可以赋值多次,没有限制。
# 7.2 Go 中的指针有什么作用? # 7.2 Go 中的指针的意义是什么?
## 什么是指针和指针变量
普通的变量,存储的是数据,而指针变量,存储的是数据的内存地址。 普通的变量,存储的是数据,而指针变量,存储的是数据的内存地址。
...@@ -22,3 +24,6 @@ myage := *ptr ...@@ -22,3 +24,6 @@ myage := *ptr
fmt.Println(myage) //output: 18 fmt.Println(myage) //output: 18
``` ```
## 指针的意义是什么?
当你往一个函数传递参数时,若该参数是一个值类型的变量,则在调用函数时,会将原来的变量的值拷贝一遍
7.2 Go 中的指针有什么作用? 7.2 Go 中的指针的意义是什么?
=========================== =============================
什么是指针和指针变量
--------------------
普通的变量,存储的是数据,而指针变量,存储的是数据的内存地址。 普通的变量,存储的是数据,而指针变量,存储的是数据的内存地址。
...@@ -22,3 +25,8 @@ ...@@ -22,3 +25,8 @@
myage := *ptr myage := *ptr
fmt.Println(myage) //output: 18 fmt.Println(myage) //output: 18
指针的意义是什么?
------------------
当你往一个函数传递参数时,若该参数是一个值类型的变量,则在调用函数时,会将原来的变量的值拷贝一遍
...@@ -41,14 +41,39 @@ func main() { ...@@ -41,14 +41,39 @@ func main() {
} }
``` ```
它比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现 什么问题 它比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现什么问题呢
1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,如果能寻址问题就大了。 字典的使用无非两种情况,我们分别来探讨一下
2. 而如果字典的元素存在,考虑到 Go 中 map 实现中元素的地址是变化的,这意味着寻址的结果也是无意义的。
1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,这种情况要可寻址就有问题了
2. 而如果字典的元素存在,考虑到 Go 中 map 实现中元素的地址是变化的,前一秒 key1 对应 value1 ,下一秒可能就对应 value2 了,你取其地址,不一定能对应更改到 map 中 key1 的值,这意味着寻址的结果也是无意义的。
基于这两点,Map 中的元素不可寻址,符合常理。 基于这两点,Map 中的元素不可寻址,符合常理。
## 字符串 ## 字符串中的字节或者字符
字符串是不可变的,你想要修改其中某个字符,是做不到的。
```go
func main() {
name := "iswbm"
name[0] = 'x' // can not assign to name[0]
}
```
因此字符串的字节或者字符不可寻址,没有问题。
注意区分如下这种写法
`name = "golang"` 只是将 name 指向了一个新的内存地址,这个新的内存地址存的是值是 `golang`,实际上你并没有改变原字符串 `iswbm` 的内存地址里存储的值。
```go
func main() {
name := "iswbm"
name = "golang"
fmt.Println(name)
}
```
字符串是不可变的,因此不可寻址,没有问题
...@@ -45,16 +45,43 @@ ...@@ -45,16 +45,43 @@
// cannot take the address of p["name"] // cannot take the address of p["name"]
} }
它比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现 它比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现什么问题呢?
什么问题?
1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,如果能寻址问题就大了。 字典的使用无非两种情况,我们分别来探讨一下
1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,这种情况要可寻址就有问题了
2. 而如果字典的元素存在,考虑到 Go 中 map 2. 而如果字典的元素存在,考虑到 Go 中 map
实现中元素的地址是变化的,这意味着寻址的结果也是无意义的。 实现中元素的地址是变化的,前一秒 key1 对应 value1 ,下一秒可能就对应
value2 了,你取其地址,不一定能对应更改到 map 中 key1
的值,这意味着寻址的结果也是无意义的。
基于这两点,Map 中的元素不可寻址,符合常理。 基于这两点,Map 中的元素不可寻址,符合常理。
字符串 字符串中的字节或者字符
------ ----------------------
字符串是不可变的,你想要修改其中某个字符,是做不到的。
.. code:: go
func main() {
name := "iswbm"
name[0] = 'x' // can not assign to name[0]
}
因此字符串的字节或者字符不可寻址,没有问题。
注意区分如下这种写法
字符串是不可变的,因此不可寻址,没有问题 ``name = "golang"`` 只是将 name
指向了一个新的内存地址,这个新的内存地址存的是值是
``golang``\ ,实际上你并没有改变原字符串 ``iswbm``
的内存地址里存储的值。
.. code:: go
func main() {
name := "iswbm"
name = "golang"
fmt.Println(name)
}
...@@ -30,3 +30,27 @@ func main() { ...@@ -30,3 +30,27 @@ func main() {
} }
``` ```
也正是这个原因,因此在给你一个方法指定定一个接收者的时候,访问接收者的对象时,不需要像下面这样显示的解引用
```go
type Person struct {
name string
}
func (p *Person) Say() {
fmt.Println((*p).name)
}
```
而可以直接这样写
```go
type Person struct {
name string
}
func (p *Person) Say() {
fmt.Println(p.name)
}
```
...@@ -32,3 +32,27 @@ ...@@ -32,3 +32,27 @@
p1 := &Profile{"iswbm"} p1 := &Profile{"iswbm"}
fmt.Println(p1.Name) // output: iswbm fmt.Println(p1.Name) // output: iswbm
} }
也正是这个原因,因此在给你一个方法指定定一个接收者的时候,访问接收者的对象时,不需要像下面这样显示的解引用
.. code:: go
type Person struct {
name string
}
func (p *Person) Say() {
fmt.Println((*p).name)
}
而可以直接这样写
.. code:: go
type Person struct {
name string
}
func (p *Person) Say() {
fmt.Println(p.name)
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
## 1. 什么是闭包? ## 1. 什么是闭包?
一个函数内引用了外部的局部变量,这个函数内的值,就称之为闭包。 一个函数内引用了外部的局部变量,这种现象,就称之为闭包。
例如下面的这段代码中,adder 函数返回了一个匿名函数,而该匿名函数中引用了 adder 函数中的局部变量 `sum` ,那这个函数就是一个闭包。 例如下面的这段代码中,adder 函数返回了一个匿名函数,而该匿名函数中引用了 adder 函数中的局部变量 `sum` ,那这个函数就是一个闭包。
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
1. 什么是闭包? 1. 什么是闭包?
--------------- ---------------
一个函数内引用了外部的局部变量,这个函数内的值,就称之为闭包。 一个函数内引用了外部的局部变量,这种现象,就称之为闭包。
例如下面的这段代码中,adder 函数返回了一个匿名函数,而该匿名函数中引用了 例如下面的这段代码中,adder 函数返回了一个匿名函数,而该匿名函数中引用了
adder 函数中的局部变量 ``sum`` ,那这个函数就是一个闭包。 adder 函数中的局部变量 ``sum`` ,那这个函数就是一个闭包。
......
# 7.16 说说你对 Go 里的抢占式调度的理解
Go 从 v1.1 发现展到目前的 v1.16,协程调度策略也在不断的完善优化。
下面我将从 v.1.1 开始讲讲 协程调度策略中抢占式调度的发展历程。
## v1.1 的非抢占式调用
在最初的 v1.1 版本中,只有当一个协程主动让出 CPU 资源(可以是运行结束,也可以是发生了系统调用或其他阻塞性操作),才能触发调度,进行下一个协程。
而如果一个协程运行了很久,也没有主动让出的动作发生,就会自私的一个人占用整个线程,该线程无法再去运行其他的 goroutine 了。
这种策略会让 Go 的并发性大打折扣,名不符实。
## v1.2 基于协作的抢占式调用
由于 v1.1 的非抢占式调用,以程序的并发效率影响实在太大。因为在下一个版本 v1.2 就紧急地对调度策略进行了临时的优化。经过优化后,go 从 v1.2 开始支持抢占式的调用:
1. 如果 sysmon 监控线程发现有个协程 A 执行之间太长了(或者 gc 场景,或者 stw 场景),那么会友好的在这个 A 协程的某个字段设置一个抢占标记 ;
2. 协程 A 在 call 一个函数的时候,会复用到扩容栈(morestack)的部分逻辑,检查到抢占标记之后,让出 cpu,切到调度主协程里;
之所以说 v1.2 的抢占式调用是临时的优化方案,是因为这种抢占式调度是基于协作的。在一些的边缘场景下,协程还是在会独自占用整个线程无法让出。
从上面的流程中,你应该可以注意到,A 调度权被抢占有个前提:A 必须主动 call 函数,这样才能有走到 morestack 的机会。
反面案例可以看下面这个程序,当运行到 `time.Sleep` 后,线程上的 goroutine 会从 main 切换到前面的匿名函数协程,而这个匿名函数协程并是在作for 死循环,并没有任何可以让出 cpu 运行权的操作,因为该程序在 go 1.14 之前的 go版本中,运行后会一直卡住,而不会打印 `I got scheduled!`
```go
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
fmt.Println("The program starts ...")
go func() {
for {
}
}()
time.Sleep(time.Second)
fmt.Println("I got scheduled!")
}
```
## v1.14 基于信号的抢占式调用
基于协作的抢占式调用,伴随着 Go 走过了12个版本,终于在 v1.14 迎来了真正的抢占式调用。
为什么说是真正的抢占式调用呢?
因为 v1.14 的这种抢占式调用是基于信号的,不管你的协程有没有意愿主动让出 cpu 运行权,只要你这个协程超过某个时间,就会发送信号强行夺取 cpu 运行权。
那么这个时间具体是多少呢? 20ms
## 延伸阅读
- [go trace 剖析 go1.14 异步抢占式调度](https://jishuin.proginn.com/p/763bfbd2a2b6)
- [Go语言设计与实现 - 抢占式调度器](https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/#%E6%8A%A2%E5%8D%A0%E5%BC%8F%E8%B0%83%E5%BA%A6%E5%99%A8)
7.16 说说你对 Go 里的抢占式调度的理解
=====================================
Go v1.1 发现展到目前的 v1.16,协程调度策略也在不断的完善优化。
下面我将从 v.1.1 开始讲讲 协程调度策略中抢占式调度的发展历程。
v1.1 的非抢占式调用
-------------------
在最初的 v1.1 版本中,只有当一个协程主动让出 CPU
资源(可以是运行结束,也可以是发生了系统调用或其他阻塞性操作),才能触发调度,进行下一个协程。
而如果一个协程运行了很久,也没有主动让出的动作发生,就会自私的一个人占用整个线程,该线程无法再去运行其他的
goroutine 了。
这种策略会让 Go 的并发性大打折扣,名不符实。
v1.2 基于协作的抢占式调用
-------------------------
由于 v1.1 的非抢占式调用,以程序的并发效率影响实在太大。因为在下一个版本
v1.2 就紧急地对调度策略进行了临时的优化。经过优化后,go v1.2
开始支持抢占式的调用:
1. 如果 sysmon 监控线程发现有个协程 A 执行之间太长了(或者 gc 场景,或者
stw 场景),那么会友好的在这个 A 协程的某个字段设置一个抢占标记
2. 协程 A call
一个函数的时候,会复用到扩容栈(morestack)的部分逻辑,检查到抢占标记之后,让出
cpu,切到调度主协程里;
之所以说 v1.2
的抢占式调用是临时的优化方案,是因为这种抢占式调度是基于协作的。在一些的边缘场景下,协程还是在会独自占用整个线程无法让出。
从上面的流程中,你应该可以注意到,A 调度权被抢占有个前提:A 必须主动
call 函数,这样才能有走到 morestack 的机会。
反面案例可以看下面这个程序,当运行到 ``time.Sleep`` 后,线程上的
goroutine 会从 main
切换到前面的匿名函数协程,而这个匿名函数协程并是在作for
死循环,并没有任何可以让出 cpu 运行权的操作,因为该程序在 go 1.14 之前的
go版本中,运行后会一直卡住,而不会打印 ``I got scheduled!``
.. code:: go
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
fmt.Println("The program starts ...")
go func() {
for {
}
}()
time.Sleep(time.Second)
fmt.Println("I got scheduled!")
}
v1.14 基于信号的抢占式调用
--------------------------
基于协作的抢占式调用,伴随着 Go 走过了12个版本,终于在 v1.14
迎来了真正的抢占式调用。
为什么说是真正的抢占式调用呢?
因为 v1.14 的这种抢占式调用是基于信号的,不管你的协程有没有意愿主动让出
cpu 运行权,只要你这个协程超过某个时间,就会发送信号强行夺取 cpu
运行权。
那么这个时间具体是多少呢? 20ms
延伸阅读
--------
- `go trace 剖析 go1.14
异步抢占式调度 <https://jishuin.proginn.com/p/763bfbd2a2b6>`__
- `Go语言设计与实现 -
抢占式调度器 <https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/#%E6%8A%A2%E5%8D%A0%E5%BC%8F%E8%B0%83%E5%BA%A6%E5%99%A8>`__
# 7.17 说一下 GMP 模型的原理
## 1. 什么是 GMP ?
- `G`:Goroutine,也就是 go 里的协程,是用户态的轻量级线程,具体可以创建多个 goroutine ,取决你的内存有多大,一个 goroutine 大概需要 4k 内存,只要你不是在 32 位的机器上,那么创建个几百万个的 goroutine 应该没有问题。
- `M`:Thread,也就是操作系统线程,go runtime 最多允许创建 10000 个操作系统线程,超过了就会抛出异常
- `P`:Processor,处理器,数量默认等于开机器的cpu核心数,若想调小,可以通过 GOMAXPROCS 这个环境变量设置。
## 2. GMP 核心
### 两个队列
在整个 Go 调度器的生命周期中,存在着两个非常重要的队列:
- 全局队列(Global Queue):全局只有一个
- 本地队列(Local Queue):每个 P 都会维护一个本地队列
当你执行 `go func()` 创建一个 goroutine 时,会优选将该协程放入到当前 P 的本地队列中等待被 P 选中执行。
但若当前 P 的本地队列任务太多了,已经存放不下了,那么这个 goroutine 就只能放入到全局队列中。
### 两种调度
一个协程得以运行,需要同时满足以下两个条件:
1. P 已经和某个线程进行绑定,这样才能参考操作系统的调度获得 CPU 时间
2. P 已经从队列中(可以是本地队列,也可以是全局队列,甚至是从其他 P 的队列)取到该协程
第一个条件就是 **操作系统调度**,而第二个其实就是 **Go 里的调度器**
#### 操作系统调度
假设一台机器上有两个 CPU 核心,意味着,同时在同一时间里,只能有两个线程运行着。
可如果该机器上实际开启了 4 个线程,要是先执行一个线程完再执行另一个线程,那么当某一个线程因为一些阻塞性的系统调用而阻塞时,CPU 的时间就会因此而白白浪费掉了。
更合适的做法是,使用 **操作系统调度策略**,设定一个调度周期,假设是 10ms (毫秒),那在一个周期里,每个线程都平均分,都只能得到 2.5ms 的CPU 运行时间。
可如果机器上有 1000 个线程呢?难道每个线程都分个 0.01 ms (也就是 10 微秒)吗?
要知道,CPU 从 A 线程切换到 B 线程,是有巨大的时间浪费在线程上下文的切换,如果切换得太频繁,就会有大量的 CPU 时间白白浪费。
![](http://image.iswbm.com/20210904140447.png)
因此,通常会限制最小的时间片的长度,假设为 2ms,受此调整,现在调度周期就会变成 2*1000 = 2s 。
#### Go调度器
在 Go 中需要用到调度的,无非是如下几种:
**将 P 绑定到一个合适的 M **
P 本身不能直接运行 G,只将 P 跟 M 绑定后,才能执行 G。
假设 P1 当前正绑定在 M1 上运行 G1,此时 G1 内部发生了一次系统调度后,P1 就会与 M1 进行解绑,然后再从空闲的线程队列中再寻找一个来绑定,假设绑定的是 M2,可如果没有空闲的线程呢?那没办法,只能创建一个新的线程再进行绑定。
绑定后,就会再从本地的队列中寻找 G 来执行(如果没找到,就会去其他队列找,上面已经讲过,不再赘述)。
过了一段时间后,之前 M1 上 G1 发生的系统调用结束后,M1 会去找原先自己的搭档 P1(它自己会记录),如果自己的老搭档也刚好空闲着,就可以再次合作进行绑定,接着运行 G1 未完成的工作。
可不幸的是,P1 已经找到了新的合作伙伴 M2,暂时没空搭理 M1 。
M1 联系不上 P1,只能去寻找有没有其他空闲的 P ,如果所有的 P 都被绑定着,说明现在任务非常繁重,想完成任务只能排队慢慢等。
于是,M1 上的 G1 就会被标记为 Runable ,放到全局队列中,而 M1 自身也会因为没有 P 可以绑定而进入休眠状态,如果长时间休眠等待 则会 GC 回收销毁
**为 P 选中一个 G 来执行**
P 就像是一个流水线工人,而 P 的本地队列就是流水线,G 是流水线上的零件。而 Go 调度器就是流水线组长,负责监督工人的是不是有在努力的工作。
完成一个 G 后,P 就得立马接着从队列中拿到下一个 G,继续干活。
遇到手脚麻利的 P ,干完了自己的活,本想着可以偷懒一会,没想到却被组长发现了,立马就从全局队列中拿了些新的 G 交到 P 的手里。
天真的 P 以为只要把 全局队列中的 G 的也干完了,就肯定 能休息了吧?
当 P 又快手快脚的把全局队列中的 G 也都干完的时候,P 非常得意,心想:终于可以休息会了。
没想到又被眼尖的组长察觉到了:不错啊,小 P,手脚挺麻利的。看看其他人,还那么多活没干完。真是拖后腿。可谁让咱是一个团队的呢,要有集体荣誉感,你能者多劳。
说完,就把其他人的 G 放到了我的工作上。。。
## 3. 调度器的设计策略
### 复用线程
避免频繁的创建、销毁线程,而是对线程的复用。
**1)work stealing 机制**
当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
**2)hand off 机制**
当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
### 利用并行
GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。
### 抢占调度
在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这是协作式抢占调度。
而在 go 1.14+ ,Go 开始支持基于信号的真抢占调度了。
### 全局 G 队列
在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
## 延伸阅读
- [[典藏版] Golang 调度器 GMP 原理与调度全分析](https://learnku.com/articles/41728)
7.17 说一下 GMP 模型的原理
==========================
1. 什么是 GMP ?
----------------
- ``G``\ :Goroutine,也就是 go
里的协程,是用户态的轻量级线程,具体可以创建多个 goroutine
,取决你的内存有多大,一个 goroutine 大概需要 4k 内存,只要你不是在
32 位的机器上,那么创建个几百万个的 goroutine 应该没有问题。
- ``M``\ :Thread,也就是操作系统线程,go runtime 最多允许创建 10000
个操作系统线程,超过了就会抛出异常
- ``P``\ :Processor,处理器,数量默认等于开机器的cpu核心数,若想调小,可以通过
GOMAXPROCS 这个环境变量设置。
2. GMP 核心
-----------
两个队列
~~~~~~~~
在整个 Go 调度器的生命周期中,存在着两个非常重要的队列:
- 全局队列(Global Queue):全局只有一个
- 本地队列(Local Queue):每个 P 都会维护一个本地队列
当你执行 ``go func()`` 创建一个 goroutine 时,会优选将该协程放入到当前 P
的本地队列中等待被 P 选中执行。
但若当前 P 的本地队列任务太多了,已经存放不下了,那么这个 goroutine
就只能放入到全局队列中。
两种调度
~~~~~~~~
一个协程得以运行,需要同时满足以下两个条件:
1. P 已经和某个线程进行绑定,这样才能参考操作系统的调度获得 CPU 时间
2. P 已经从队列中(可以是本地队列,也可以是全局队列,甚至是从其他 P
的队列)取到该协程
第一个条件就是 **操作系统调度**\ ,而第二个其实就是 **Go
里的调度器**\ 。
操作系统调度
^^^^^^^^^^^^
假设一台机器上有两个 CPU
核心,意味着,同时在同一时间里,只能有两个线程运行着。
可如果该机器上实际开启了 4
个线程,要是先执行一个线程完再执行另一个线程,那么当某一个线程因为一些阻塞性的系统调用而阻塞时,CPU
的时间就会因此而白白浪费掉了。
更合适的做法是,使用 **操作系统调度策略**\ ,设定一个调度周期,假设是
10ms (毫秒),那在一个周期里,每个线程都平均分,都只能得到 2.5ms 的CPU
运行时间。
可如果机器上有 1000 个线程呢?难道每个线程都分个 0.01 ms (也就是 10
微秒)吗?
要知道,CPU 从 A 线程切换到 B
线程,是有巨大的时间浪费在线程上下文的切换,如果切换得太频繁,就会有大量的
CPU 时间白白浪费。
|image0|
因此,通常会限制最小的时间片的长度,假设为
2ms,受此调整,现在调度周期就会变成 2*1000 = 2s 。
Go调度器
^^^^^^^^
在 Go 中需要用到调度的,无非是如下几种:
**将 P 绑定到一个合适的 M**
P 本身不能直接运行 G,只将 P 跟 M 绑定后,才能执行 G。
假设 P1 当前正绑定在 M1 上运行 G1,此时 G1 内部发生了一次系统调度后,P1
就会与 M1
进行解绑,然后再从空闲的线程队列中再寻找一个来绑定,假设绑定的是
M2,可如果没有空闲的线程呢?那没办法,只能创建一个新的线程再进行绑定。
绑定后,就会再从本地的队列中寻找 G
来执行(如果没找到,就会去其他队列找,上面已经讲过,不再赘述)。
过了一段时间后,之前 M1 上 G1 发生的系统调用结束后,M1
会去找原先自己的搭档
P1(它自己会记录),如果自己的老搭档也刚好空闲着,就可以再次合作进行绑定,接着运行
G1 未完成的工作。
可不幸的是,P1 已经找到了新的合作伙伴 M2,暂时没空搭理 M1 。
M1 联系不上 P1,只能去寻找有没有其他空闲的 P ,如果所有的 P
都被绑定着,说明现在任务非常繁重,想完成任务只能排队慢慢等。
于是,M1 上的 G1 就会被标记为 Runable ,放到全局队列中,而 M1
自身也会因为没有 P 可以绑定而进入休眠状态,如果长时间休眠等待 则会 GC
回收销毁
**为 P 选中一个 G 来执行**
P 就像是一个流水线工人,而 P 的本地队列就是流水线,G
是流水线上的零件。而 Go
调度器就是流水线组长,负责监督工人的是不是有在努力的工作。
完成一个 G 后,P 就得立马接着从队列中拿到下一个 G,继续干活。
遇到手脚麻利的 P
,干完了自己的活,本想着可以偷懒一会,没想到却被组长发现了,立马就从全局队列中拿了些新的
G 交到 P 的手里。
天真的 P 以为只要把 全局队列中的 G 的也干完了,就肯定 能休息了吧?
当 P 又快手快脚的把全局队列中的 G 也都干完的时候,P
非常得意,心想:终于可以休息会了。
没想到又被眼尖的组长察觉到了:不错啊,小
P,手脚挺麻利的。看看其他人,还那么多活没干完。真是拖后腿。可谁让咱是一个团队的呢,要有集体荣誉感,你能者多劳。
说完,就把其他人的 G 放到了我的工作上。。。
3. 调度器的设计策略
-------------------
复用线程
~~~~~~~~
避免频繁的创建、销毁线程,而是对线程的复用。
**1)work stealing 机制**
当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
**2)hand off 机制**
当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P
转移给其他空闲的线程执行。
利用并行
~~~~~~~~
GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU
上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS =
核数/2,则最多利用了一半的 CPU 核进行并行。
抢占调度
~~~~~~~~
在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine
被饿死,这是协作式抢占调度。
而在 go 1.14+ ,Go 开始支持基于信号的真抢占调度了。
全局 G 队列
~~~~~~~~~~~
在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work
stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
延伸阅读
--------
- `[典藏版] Golang 调度器 GMP
原理与调度全分析 <https://learnku.com/articles/41728>`__
.. |image0| image:: http://image.iswbm.com/20210904140447.png
# 7.18 Go 的默认栈大小是多少?最大值多少?
Go 语言使用用户态线程 Goroutine 作为执行上下文,它的额外开销和默认栈大小都比线程小很多,然而 Goroutine 的栈内存空间和栈结构也在早期几个版本中发生过一些变化:
- v1.0 ~ v1.1 — 最小栈内存空间为 4KB;
- v1.2 — 将最小栈内存提升到了 8KB;
- v1.3 — 使用连续栈替换之前版本的分段栈;
- v1.4 — 将最小栈内存降低到了 2KB;
Goroutine 的初始栈内存在最初的几个版本中多次修改,从 4KB 提升到 8KB 是临时的解决方案,其目的是为了减轻分段栈的栈热分裂问题对程序造成的性能影响;
在 v1.3 版本引入连续栈之后,Goroutine 的初始栈大小降低到了 2KB,进一步减少了 Goroutine 占用的内存空间。
这个栈比 x86_64 构架下线程的默认栈 2M 要小很多,真的是轻量级的用户态线程。
关于这个初始值和最大值嘛,在 Go 的源码 `runtime/stack.go` 里其实都可以找到
```go
// rumtime.stack.go
// The minimum size of stack used by Go code
_StackMin = 2048
var maxstacksize uintptr = 1 << 20 // enough until runtime.main sets it for real
```
那么这个 `1<<20` 代表多大呢?使用 Python 计算一下是 1G
```python
>>> 1<<20
1048576
>>> 1048576/1024/1024
1.0
```
7.18 Go 的默认栈大小是多少?最大值多少?
========================================
Go 语言使用用户态线程 Goroutine
作为执行上下文,它的额外开销和默认栈大小都比线程小很多,然而 Goroutine
的栈内存空间和栈结构也在早期几个版本中发生过一些变化:
- v1.0 ~ v1.1 — 最小栈内存空间为 4KB;
- v1.2 — 将最小栈内存提升到了 8KB;
- v1.3 — 使用连续栈替换之前版本的分段栈;
- v1.4 — 将最小栈内存降低到了 2KB;
Goroutine 的初始栈内存在最初的几个版本中多次修改,从 4KB 提升到 8KB
是临时的解决方案,其目的是为了减轻分段栈的栈热分裂问题对程序造成的性能影响;
在 v1.3 版本引入连续栈之后,Goroutine 的初始栈大小降低到了
2KB,进一步减少了 Goroutine 占用的内存空间。
这个栈比 x86_64 构架下线程的默认栈 2M
要小很多,真的是轻量级的用户态线程。
关于这个初始值和最大值嘛,在 Go 的源码 ``runtime/stack.go``
里其实都可以找到
.. code:: go
// rumtime.stack.go
// The minimum size of stack used by Go code
_StackMin = 2048
var maxstacksize uintptr = 1 << 20 // enough until runtime.main sets it for real
那么这个 ``1<<20`` 代表多大呢?使用 Python 计算一下是 1G
.. code:: python
>>> 1<<20
1048576
>>> 1048576/1024/1024
1.0
# 7.19 Go 中的分段栈和连续栈的区别?
## 分段栈
在 Go 1.3 版本之前 ,使用的栈结构是分段栈,随着`goroutine` 调用的函数层级的深入或者局部变量需要的越来越多时,运行时会调用 `runtime.morestack``runtime.newstack`创建一个新的栈空间,这些栈空间是不连续的,但是当前 `goroutine` 的多个栈空间会以双向链表的形式串联起来,运行时会通过指针找到连续的栈片段。
分段栈虽然能够按需为当前 `goroutine` 分配内存并且及时减少内存的占用,但是它也存在一个比较大的问题:
如果当前 `goroutine` 的栈几乎充满,那么任意的函数调用都会触发栈的扩容,当函数返回后又会触发栈的收缩,如果在一个循环中调用函数,栈的分配和释放就会造成巨大的额外开销,这被称为热分裂问题(Hot split)。
为了解决这个问题,Go 在 1.2 版本的时候不得不将栈的初始化内存从 4KB 增大到了 8KB。后来把采用连续栈结构后,又把初始栈大小减小到了 2KB。
## 连续栈
连续栈可以解决分段栈中存在的两个问题,其核心原理就是每当程序的栈空间不足时,初始化一片比旧栈大两倍的新栈并将原栈中的所有值都迁移到新的栈中,新的局部变量或者函数调用就有了充足的内存空间。使用连续栈机制时,栈空间不足导致的扩容会经历以下几个步骤:
1. 调用用`runtime.newstack`在内存空间中分配更大的栈内存空间;
2. 使用`runtime.copystack`将旧栈中的所有内容复制到新的栈中;
3. 将指向旧栈对应变量的指针重新指向新栈;
4. 调用`runtime.stackfree`销毁并回收旧栈的内存空间;
`copystack`会把旧栈里的所有内容拷贝到新栈里然后调整所有指向旧栈的变量的指针指向到新栈, 我们可以用下面这个程序验证下,栈扩容后同一个变量的内存地址会发生变化。
```go
package main
func main() {
var x [10]int
println(&x)
a(x)
println(&x)
}
//go:noinline
func a(x [10]int) {
println(`func a`)
var y [100]int
b(y)
}
//go:noinline
func b(x [100]int) {
println(`func b`)
var y [1000]int
c(y)
}
//go:noinline
func c(x [1000]int) {
println(`func c`)
}
```
程序的输出可以看到在栈扩容前后,变量`x`的内存地址的变化:
```go
0xc000030738
...
...
0xc000081f38
```
7.19 Go 中的分段栈和连续栈的区别?
==================================
分段栈
------
Go 1.3 版本之前 ,使用的栈结构是分段栈,随着\ ``goroutine``
调用的函数层级的深入或者局部变量需要的越来越多时,运行时会调用
``runtime.morestack``
``runtime.newstack``\ 创建一个新的栈空间,这些栈空间是不连续的,但是当前
``goroutine``
的多个栈空间会以双向链表的形式串联起来,运行时会通过指针找到连续的栈片段。
分段栈虽然能够按需为当前 ``goroutine``
分配内存并且及时减少内存的占用,但是它也存在一个比较大的问题:
如果当前 ``goroutine``
的栈几乎充满,那么任意的函数调用都会触发栈的扩容,当函数返回后又会触发栈的收缩,如果在一个循环中调用函数,栈的分配和释放就会造成巨大的额外开销,这被称为热分裂问题(Hot
split)。
为了解决这个问题,Go 1.2 版本的时候不得不将栈的初始化内存从 4KB
增大到了 8KB。后来把采用连续栈结构后,又把初始栈大小减小到了 2KB
连续栈
------
连续栈可以解决分段栈中存在的两个问题,其核心原理就是每当程序的栈空间不足时,初始化一片比旧栈大两倍的新栈并将原栈中的所有值都迁移到新的栈中,新的局部变量或者函数调用就有了充足的内存空间。使用连续栈机制时,栈空间不足导致的扩容会经历以下几个步骤:
1. 调用用\ ``runtime.newstack``\ 在内存空间中分配更大的栈内存空间;
2. 使用\ ``runtime.copystack``\ 将旧栈中的所有内容复制到新的栈中;
3. 将指向旧栈对应变量的指针重新指向新栈;
4. 调用\ ``runtime.stackfree``\ 销毁并回收旧栈的内存空间;
``copystack``\ 会把旧栈里的所有内容拷贝到新栈里然后调整所有指向旧栈的变量的指针指向到新栈,
我们可以用下面这个程序验证下,栈扩容后同一个变量的内存地址会发生变化。
.. code:: go
package main
func main() {
var x [10]int
println(&x)
a(x)
println(&x)
}
//go:noinline
func a(x [10]int) {
println(`func a`)
var y [100]int
b(y)
}
//go:noinline
func b(x [100]int) {
println(`func b`)
var y [1000]int
c(y)
}
//go:noinline
func c(x [1000]int) {
println(`func c`)
}
程序的输出可以看到在栈扩容前后,变量\ ``x``\ 的内存地址的变化:
.. code:: go
0xc000030738
...
...
0xc000081f38
# 7.20 简述一下 Go 栈空间的扩容/缩容过程?
## 扩容流程
**为啥会有栈空间扩容**
由于当前的 Go 的栈结构使用的是连续栈,并且初始值才 2k 比较小,因此随着函数的调用层级加深,Go 的初始栈空间就可能不够用,不够用的话,就会触发栈空间的扩容。
**栈空间扩容啥时会触发**
编译器会为函数调用插入运行时检查`runtime.morestack`,它会在几乎所有的函数调用之前检查当前`goroutine` 的栈内存是否充足,如果当前栈需要扩容,会调用`runtime.newstack` 创建新的栈。
而新的栈空间,是旧栈空间大小(通过保存在`goroutine`中的`stack`信息里记录的栈区内存边界计算出来的)的两倍,但最大栈空间大小不能超过 `maxstacksize` ,也就是 1G。
## 缩容流程
**为啥会有栈空间缩容**
在函数返回后,对应的栈空间会回收,如果调用栈比较深,那么随着函数一个一个返回,回收的栈空间会越来越多。假设在调用栈最深的时候,整体的栈空间扩容到了 100M,那么随着函数的返回,到某一个函数的时候,100M 的栈空间只有 1M 是实际占用的,内存利用率只有区区的 1% ,实在太浪费了。
**栈空间缩容啥时会触发**
因此在垃圾回收的时候,有必要检查一下栈空间里内存利用率,当利用率低于 25% 时,就要开始进行缩容,缩容成原来的栈空间的 50%,但同时也不能小于栈空间的原始值即最小值,2KB。
## 相同点
不管是扩容还是缩容,都是使用 `runtime.copystack` 函数来开辟新的栈空间,然后将旧栈的数据全部拷贝至新的栈空间,并调整原来指针的指向。
## 延伸阅读
[Go 语言内存管理三部曲(二)解密栈内存管理](https://xie.infoq.cn/article/530c735982a391604d0eebe71)
\ No newline at end of file
7.20 简述一下 Go 栈空间的扩容/缩容过程?
========================================
扩容流程
--------
**为啥会有栈空间扩容**
由于当前的 Go 的栈结构使用的是连续栈,并且初始值才 2k
比较小,因此随着函数的调用层级加深,Go
的初始栈空间就可能不够用,不够用的话,就会触发栈空间的扩容。
**栈空间扩容啥时会触发**
编译器会为函数调用插入运行时检查\ ``runtime.morestack``\ ,它会在几乎所有的函数调用之前检查当前\ ``goroutine``
的栈内存是否充足,如果当前栈需要扩容,会调用\ ``runtime.newstack``
创建新的栈。
而新的栈空间,是旧栈空间大小(通过保存在\ ``goroutine``\ 中的\ ``stack``\ 信息里记录的栈区内存边界计算出来的)的两倍,但最大栈空间大小不能超过
``maxstacksize`` ,也就是 1G。
缩容流程
--------
**为啥会有栈空间缩容**
在函数返回后,对应的栈空间会回收,如果调用栈比较深,那么随着函数一个一个返回,回收的栈空间会越来越多。假设在调用栈最深的时候,整体的栈空间扩容到了
100M,那么随着函数的返回,到某一个函数的时候,100M 的栈空间只有 1M
是实际占用的,内存利用率只有区区的 1% ,实在太浪费了。
**栈空间缩容啥时会触发**
因此在垃圾回收的时候,有必要检查一下栈空间里内存利用率,当利用率低于 25%
时,就要开始进行缩容,缩容成原来的栈空间的
50%,但同时也不能小于栈空间的原始值即最小值,2KB。
相同点
------
不管是扩容还是缩容,都是使用 ``runtime.copystack``
函数来开辟新的栈空间,然后将旧栈的数据全部拷贝至新的栈空间,并调整原来指针的指向。
延伸阅读
--------
`Go
语言内存管理三部曲(二)解密栈内存管理 <https://xie.infoq.cn/article/530c735982a391604d0eebe71>`__
# 7.21 GMP 模型为什么要有 P ?
## GM 模型是怎样的?
在 Go v1.1 之前,实际上 GMP确实是没有 P 的,所有的 M 线程都要从 全局队列中获取 G 来执行任务,为了避免冲突,从全局队列中获取 G 的时候,要先获取一把大锁。
当一个程序的并发量比较小的时候,影响还不大,而当程序的并发量非常大的时候,这个全局队列会成为性能的瓶颈。
除此之外 ,若直接把 G 从全局队列分配给 M,那么当 G 中当生系统调用或者其他阻塞性的操作时,M 会有一段时间处于挂起的状态,此时又没有新创建线程的线程来代替该线程继续从队列中取出其他 G 来运行,从效率上其实会打折扣。
## P 带来的改变
加了 P 之后会带来什么改变呢?
- 每个 P 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。
- 当一个 M 中 运行的 G 发生阻塞性操作时,P 会重新选择一个 M,若没有 M 就新创建一个 M 来继续从 P 本地队列中取 G 来执行,提高运行效率。
- 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。
7.21 GMP 模型为什么要有 P ?
============================
GM 模型是怎样的?
-----------------
在 Go v1.1 之前,实际上 GMP确实是没有 P 的,所有的 M 线程都要从
全局队列中获取 G 来执行任务,为了避免冲突,从全局队列中获取 G
的时候,要先获取一把大锁。
当一个程序的并发量比较小的时候,影响还不大,而当程序的并发量非常大的时候,这个全局队列会成为性能的瓶颈。
除此之外 ,若直接把 G 从全局队列分配给 M,那么当 G
中当生系统调用或者其他阻塞性的操作时,M
会有一段时间处于挂起的状态,此时又没有新创建线程的线程来代替该线程继续从队列中取出其他
G 来运行,从效率上其实会打折扣。
P 带来的改变
------------
加了 P 之后会带来什么改变呢?
- 每个 P
有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而
GM 模型的性能开销大头就是锁竞争。
- 当一个 M 中 运行的 G 发生阻塞性操作时,P 会重新选择一个 M,若没有 M
就新创建一个 M 来继续从 P 本地队列中取 G 来执行,提高运行效率。
- 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P
的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G
来运行,减少空转,提高了资源利用率。
# 7.22 Go 中的 GC 演变是怎样的?
## 标记清除法
在 Go v1.3 之前采用的是 标记-清除(mark and sweep)算法。
它的逻辑是,先将整个程序挂起(STW, stop the world),然后遍历程序中的对象,只要是可达的对象,都会被标记保留(红色),而那些不可达的对象(白色),则会被清理掉,清理完成后,会恢复程序。然后不断重复该过程。
![](http://image.iswbm.com/20210905105841.png)
这种标记-清除的算法,会有一段 STW 的时间,将整个程序暂停,这对于一些实时性要求比较高的系统是无法接受的。
另外,上面这个标记的过程扫描的是整个堆内存,耗时比较久,最重要的是,它在清除数据的时候,会产生堆内存的碎片。
因此从 Go v1.5 开始,就开始抛弃这种算法,而改用 **三色并发标记法**
## 三色并发标记法
新算法的出现,必然是要解决旧算法存在的最关键问题 ,即STW 的暂停挂起导致的程序卡顿。
它的逻辑就是,准备三种颜色,分别对三种对象进行标记:
- 黑色:检测到有被引用,并且已经遍历完它所有直接引用的对象或者属性
- 白色:还没检测到有引用的对象(检测开始前,所有对象都是白色,检测结束后,没有被引用的对象都是白色,会被清查掉)
- 灰色:检测到有被引用,但是他的属性还没有被遍历,等遍历完后也会变成黑色
既然 STW 会挂起程序,那是不是可以考虑将其摘除呢?
摘除会带来一个问题就是在标记的时候,程序的运行会不断改变对象的引用路径,影响标记的准确性。
关于不使用 STW 带来的影响可以看一下这篇文章:https://segmentfault.com/a/1190000022030353
总结来说,就是当在标记的时候出现 :**一个白色对象被黑色对象引用,同时该白色对象又被某个灰色(或者上级有灰色对象)对象取消引用的情况**,就会标记不准确。
因此如果想摘除 STW,那就得规避掉上面这个场景出现。
解决方法是:使用 **插入屏障****删除屏障**
### 插入屏障
在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
### 删除屏障
被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
## 延伸阅读
- [[典藏版]Golang三色标记、混合写屏障GC模式图文全分析](https://segmentfault.com/a/1190000022030353)
\ No newline at end of file
7.22 Go 中的 GC 演变是怎样的?
==============================
标记清除法
----------
在 Go v1.3 之前采用的是 标记-清除(mark and sweep)算法。
它的逻辑是,先将整个程序挂起(STW, stop the
world),然后遍历程序中的对象,只要是可达的对象,都会被标记保留(红色),而那些不可达的对象(白色),则会被清理掉,清理完成后,会恢复程序。然后不断重复该过程。
|image0|
这种标记-清除的算法,会有一段 STW
的时间,将整个程序暂停,这对于一些实时性要求比较高的系统是无法接受的。
另外,上面这个标记的过程扫描的是整个堆内存,耗时比较久,最重要的是,它在清除数据的时候,会产生堆内存的碎片。
因此从 Go v1.5 开始,就开始抛弃这种算法,而改用 **三色并发标记法**\ 。
三色并发标记法
--------------
新算法的出现,必然是要解决旧算法存在的最关键问题 ,即STW
的暂停挂起导致的程序卡顿。
它的逻辑就是,准备三种颜色,分别对三种对象进行标记:
- 黑色:检测到有被引用,并且已经遍历完它所有直接引用的对象或者属性
- 白色:还没检测到有引用的对象(检测开始前,所有对象都是白色,检测结束后,没有被引用的对象都是白色,会被清查掉)
- 灰色:检测到有被引用,但是他的属性还没有被遍历,等遍历完后也会变成黑色
既然 STW 会挂起程序,那是不是可以考虑将其摘除呢?
摘除会带来一个问题就是在标记的时候,程序的运行会不断改变对象的引用路径,影响标记的准确性。
关于不使用 STW
带来的影响可以看一下这篇文章:https://segmentfault.com/a/1190000022030353
总结来说,就是当在标记的时候出现
:\ **一个白色对象被黑色对象引用,同时该白色对象又被某个灰色(或者上级有灰色对象)对象取消引用的情况**\ ,就会标记不准确。
因此如果想摘除 STW,那就得规避掉上面这个场景出现。
解决方法是:使用 **插入屏障** 和 **删除屏障**
插入屏障
~~~~~~~~
在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
删除屏障
~~~~~~~~
被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
延伸阅读
--------
- `[典藏版]Golang三色标记、混合写屏障GC模式图文全分析 <https://segmentfault.com/a/1190000022030353>`__
.. |image0| image:: http://image.iswbm.com/20210905105841.png
# 7.23 不分配内存的指针类型能用吗?
下面这个例子,先定义了一个类型为 `*int` 的指针类型,可是然后把 `10` 赋值给指针指向的值
```go
package main
import (
"fmt"
)
func main() {
var i *int
*i=10
fmt.Println(*i)
}
```
看起来好像没有啥问题,可为什么会报错呢?
```go
$ go run demo.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x109cea3]
goroutine 1 [running]:
main.main()
/Users/MING/Code/Golang/src/demo/demo.go:9 +0x23
exit status 2
```
原因是我们只声明了指针类型,但并没有为其分配内存,没有内存地址,你赋给它的值应该存在哪里呢?自然只能报错了。
正确的写法应该是这样,在赋值给,先使用 new 函数给 i 分配内存。
```go
package main
import (
"fmt"
)
func main() {
var i *int
i = new(int)
*i=10
fmt.Println(*i)
}
```
7.23 不分配内存的指针类型能用吗?
=================================
下面这个例子,先定义了一个类型为 ``*int`` 的指针类型,可是然后把 ``10``
赋值给指针指向的值
.. code:: go
package main
import (
"fmt"
)
func main() {
var i *int
*i=10
fmt.Println(*i)
}
看起来好像没有啥问题,可为什么会报错呢?
.. code:: go
$ go run demo.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x109cea3]
goroutine 1 [running]:
main.main()
/Users/MING/Code/Golang/src/demo/demo.go:9 +0x23
exit status 2
原因是我们只声明了指针类型,但并没有为其分配内存,没有内存地址,你赋给它的值应该存在哪里呢?自然只能报错了。
正确的写法应该是这样,在赋值给,先使用 new 函数给 i 分配内存。
.. code:: go
package main
import (
"fmt"
)
func main() {
var i *int
i = new(int)
*i=10
fmt.Println(*i)
}
# 7.24 如何让在强制转换类型时不发生内存拷贝?
当你使用要对一个变量从一个类型强制转换成另一个类型,其实都会发生内存的拷贝,而这种拷贝会对性能有所影响的,因此如果可以在转换的时候避免内存的拷贝就好了。
庆幸的是,在一些特定的类型下,这种想法确实是可以实现的。
比如将字符串转成 []byte 类型。
正常的转换方法是
```go
// string to []byte
s1 := "hello"
b := []byte(s1)
// []byte to string
s2 := string(b)
```
具体的代码如下
```go
func main() {
msg1 :="hello"
sh := *(*reflect.StringHeader)(unsafe.Pointer(&msg1))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
msg2 := *(*[]byte)(unsafe.Pointer(&bh))
fmt.Printf("%v", msg2)
}
```
这段代码是不是看着有点晕啊,各种奇奇怪怪的写法,见都没见过。
其实核心知识点有三个:
1. 一种定义变量的怪异方法
2. 字符串的底层数据结构
3. 切片的底层数据结构
正常我们所熟知的变量的声明定义方法是下面两种吧
```go
// 第一种
var name string = "Go编程时光"
// 第二种
name := "Go编程时光"
```
但还有一种方法,可能新手不知道,这种方法,我在之前的文章有提到过 [详细图解:静态类型与动态类型](https://golang.iswbm.com/c02/c02_09.html)
还是用上面的等价例子,它还可以这么写
```go
name := (string)("Go编程时光")
```
再回过头来理解最上面那段怪异的代码
- 第一个括号:肯定是某个类型对应的指针类型
- 第二个括号:就是第一个括号里类型对应的值
```go
tmp := *(*reflect.StringHeader)(unsafe.Pointer(&msg))
```
由于第一个括号里是个指针类型,那么第二个括号里肯定要是指针的值。
而通过 `unsafe.Pointer` 就可以将 `&msg` 指针的内存地址取出来。
两个括号合起来就是,声明并定义了一个 `*reflect.StringHeader` 类型的指针变量,对应的指针值还是原来 msg1 的内存地址。
那最前面的的那个那个 `*` ,大家应该都知道,是从`*reflect.StringHeader` 类型的指针变量中取出值。
那么你肯定要问了,int 和 bool、string 这些类型我都知道啊,这个reflect.StringHeader 是什么类型??没见过啊
其实他就是字符串的底层结构,是字符串最原始的样子。
```go
type StringHeader struct {
Data uintptr
Len int
}
```
同样的, `SliceHeader` 则是切片的底层数据结构
```go
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
```
是不是觉得他们很像?
对咯,只要把 `StringHeader` 里的 Data 塞给 `SliceHeader` 里的 Data,再把 `SliceHeader` 里的 Len 塞给 `SliceHeader` 里的 Len 和 Cap ,就多费任何的空间创造出一个新的变量。
```go
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
```
最后再把 `SliceHeader` 通过上面的强制转换方法,再转成 `[]byte` 就可以了,中间就不会有任何的内存拷贝的过程。
是不是真的有效果呢?来测试一下性能便知
先准备 demo.go
```go
package main
import (
"reflect"
"unsafe"
)
func String2Bytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
```
再准备 `demo_test.go`
```go
package main
import (
"bytes"
"testing"
)
func TestString2Bytes(t *testing.T) {
x := "Hello Gopher!"
y := String2Bytes(x)
z := []byte(x)
if !bytes.Equal(y, z) {
t.Fail()
}
}
// 测试标准转换[]byte性能
func Benchmark_NormalString2Bytes(b *testing.B) {
x := "Hello Gopher! Hello Gopher! Hello Gopher!"
for i := 0; i < b.N; i++ {
_ = []byte(x)
}
}
// 测试强转换string到[]byte性能
func Benchmark_String2Bytes(b *testing.B) {
x := "Hello Gopher! Hello Gopher! Hello Gopher!"
for i := 0; i < b.N; i++ {
_ = String2Bytes(x)
}
}
```
并在当前目录下执行
```go
go mod init
```
最后就可以执行如下命令进行测试,从输出的结果来看使用我们的黑魔法转换的效率要比普通的方法快太多了
```shell
$ go test -bench="." -benchmem
goos: darwin
goarch: amd64
pkg: demo
Benchmark_NormalString2Bytes-8 36596674 28.5 ns/op 48 B/op 1 allocs/op
Benchmark_String2Bytes-8 1000000000 0.253 ns/op 0 B/op 0 allocs/op
```
## 延伸阅读
- [Golang中[]byte与string转换全解析](https://juejin.cn/post/6889713026287534094)
\ No newline at end of file
7.24 如何让在强制转换类型时不发生内存拷贝?
===========================================
当你使用要对一个变量从一个类型强制转换成另一个类型,其实都会发生内存的拷贝,而这种拷贝会对性能有所影响的,因此如果可以在转换的时候避免内存的拷贝就好了。
庆幸的是,在一些特定的类型下,这种想法确实是可以实现的。
比如将字符串转成 []byte 类型。
正常的转换方法是
.. code:: go
// string to []byte
s1 := "hello"
b := []byte(s1)
// []byte to string
s2 := string(b)
具体的代码如下
.. code:: go
func main() {
msg1 :="hello"
sh := *(*reflect.StringHeader)(unsafe.Pointer(&msg1))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
msg2 := *(*[]byte)(unsafe.Pointer(&bh))
fmt.Printf("%v", msg2)
}
这段代码是不是看着有点晕啊,各种奇奇怪怪的写法,见都没见过。
其实核心知识点有三个:
1. 一种定义变量的怪异方法
2. 字符串的底层数据结构
3. 切片的底层数据结构
正常我们所熟知的变量的声明定义方法是下面两种吧
.. code:: go
// 第一种
var name string = "Go编程时光"
// 第二种
name := "Go编程时光"
但还有一种方法,可能新手不知道,这种方法,我在之前的文章有提到过
`详细图解:静态类型与动态类型 <https://golang.iswbm.com/c02/c02_09.html>`__
还是用上面的等价例子,它还可以这么写
.. code:: go
name := (string)("Go编程时光")
再回过头来理解最上面那段怪异的代码
- 第一个括号:肯定是某个类型对应的指针类型
- 第二个括号:就是第一个括号里类型对应的值
.. code:: go
tmp := *(*reflect.StringHeader)(unsafe.Pointer(&msg))
由于第一个括号里是个指针类型,那么第二个括号里肯定要是指针的值。
而通过 ``unsafe.Pointer`` 就可以将 ``&msg`` 指针的内存地址取出来。
两个括号合起来就是,声明并定义了一个 ``*reflect.StringHeader``
类型的指针变量,对应的指针值还是原来 msg1 的内存地址。
那最前面的的那个那个 ``*``
,大家应该都知道,是从\ ``*reflect.StringHeader``
类型的指针变量中取出值。
那么你肯定要问了,int boolstring
这些类型我都知道啊,这个reflect.StringHeader 是什么类型??没见过啊
其实他就是字符串的底层结构,是字符串最原始的样子。
.. code:: go
type StringHeader struct {
Data uintptr
Len int
}
同样的, ``SliceHeader`` 则是切片的底层数据结构
.. code:: go
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
是不是觉得他们很像?
对咯,只要把 ``StringHeader`` 里的 Data 塞给 ``SliceHeader`` 里的
Data,再把 ``SliceHeader`` 里的 Len 塞给 ``SliceHeader`` 里的 Len Cap
,就多费任何的空间创造出一个新的变量。
.. code:: go
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
最后再把 ``SliceHeader`` 通过上面的强制转换方法,再转成 ``[]byte``
就可以了,中间就不会有任何的内存拷贝的过程。
是不是真的有效果呢?来测试一下性能便知
先准备 demo.go
.. code:: go
package main
import (
"reflect"
"unsafe"
)
func String2Bytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
再准备 ``demo_test.go``
.. code:: go
package main
import (
"bytes"
"testing"
)
func TestString2Bytes(t *testing.T) {
x := "Hello Gopher!"
y := String2Bytes(x)
z := []byte(x)
if !bytes.Equal(y, z) {
t.Fail()
}
}
// 测试标准转换[]byte性能
func Benchmark_NormalString2Bytes(b *testing.B) {
x := "Hello Gopher! Hello Gopher! Hello Gopher!"
for i := 0; i < b.N; i++ {
_ = []byte(x)
}
}
// 测试强转换string[]byte性能
func Benchmark_String2Bytes(b *testing.B) {
x := "Hello Gopher! Hello Gopher! Hello Gopher!"
for i := 0; i < b.N; i++ {
_ = String2Bytes(x)
}
}
并在当前目录下执行
.. code:: go
go mod init
最后就可以执行如下命令进行测试,从输出的结果来看使用我们的黑魔法转换的效率要比普通的方法快太多了
.. code:: shell
$ go test -bench="." -benchmem
goos: darwin
goarch: amd64
pkg: demo
Benchmark_NormalString2Bytes-8 36596674 28.5 ns/op 48 B/op 1 allocs/op
Benchmark_String2Bytes-8 1000000000 0.253 ns/op 0 B/op 0 allocs/op
延伸阅读
--------
- `Golang[]bytestring转换全解析 <https://juejin.cn/post/6889713026287534094>`__
# 7.25 Go 里是怎么比较相等与否?
## 1. 两个 interface 比较
interface 的内部实现包含了两个字段,一个是 type,一个是 data
![](http://image.iswbm.com/20200610235106.png)
因此两个 interface 比较,势必与这两个字段有所关系。
经过验证,只有下面两种情况,两个 interface 才会相等。
### 第一种情况
**type 和 data 都相等**
在下面的代码中,p1 和 p2 的 type 都是 Profile,data 都是 `{"iswbm"}`,因此 p1 与 p2 相等
而 p3 和 p3 虽然类型都是 `*Profile`,但由于 data 存储的是结构体的地址,而两个地址和不相同,因此 p3 与 p4 不相等
```go
package main
import "fmt"
type Profile struct {
Name string
}
type ProfileInt interface {}
func main() {
var p1, p2 ProfileInt = Profile{"iswbm"}, Profile{"iswbm"}
var p3, p4 ProfileInt = &Profile{"iswbm"}, &Profile{"iswbm"}
fmt.Printf("p1 --> type: %T, data: %v \n", p1, p1)
fmt.Printf("p2 --> type: %T, data: %v \n", p2, p2)
fmt.Println(p1 == p2) // true
fmt.Printf("p3 --> type: %T, data: %p \n", p3, p3)
fmt.Printf("p4 --> type: %T, data: %p \n", p4, p4)
fmt.Println(p3 == p4) // false
}
```
运行后,输出如下
```
p1 --> type: main.Profile, data: {iswbm}
p2 --> type: main.Profile, data: {iswbm}
true
p3 --> type: *main.Profile, data: 0xc00008e200
p4 --> type: *main.Profile, data: 0xc00008e210
false
```
### 第二种情况
**特殊情况:两个 interface 都是 nil **
当一个 interface 的 type 和 data 都处于 unset 状态的时候,那么该 interface 的值就为 nil
```go
package main
import "fmt"
type ProfileInt interface {}
func main() {
var p1, p2 ProfileInt
fmt.Println(p1==p2) // true
}
```
## 2. interface 与 非 interface 比较
当 interface 与非 interface 比较时,会将 非interface 转换成 interface ,然后再按照 **两个 interface 比较** 的规则进行比较。
示例如下
```go
package main
import (
"fmt"
"reflect"
)
func main() {
var a string = "iswbm"
var b interface{} = "iswbm"
fmt.Println(a==b) // true
}
```
上面这种例子可能还好理解,那么请你看下面这个例子,为什么经过反射看到的他们不相等?
```go
package main
import (
"fmt"
"reflect"
)
func main() {
var a *string = nil
var b interface{} = a
fmt.Println(b==nil) // false
}
```
因此当 nil 转换为interface 后是 `(type=nil, data=nil)` ,这与 b `(type=*string, data=nil)` 虽然 data 是一样的,但 type 不相等,因此他们并不相等。
7.25 Go 里是怎么比较相等与否?
==============================
1. 两个 interface 比较
----------------------
interface 的内部实现包含了两个字段,一个是 type,一个是 data
|image0|
因此两个 interface 比较,势必与这两个字段有所关系。
经过验证,只有下面两种情况,两个 interface 才会相等。
第一种情况
~~~~~~~~~~
**type data 都相等**
在下面的代码中,p1 p2 type 都是 Profiledata 都是
``{"iswbm"}``\ ,因此 p1 p2 相等
p3 p3 虽然类型都是 ``*Profile``\ ,但由于 data
存储的是结构体的地址,而两个地址和不相同,因此 p3 p4 不相等
.. code:: go
package main
import "fmt"
type Profile struct {
Name string
}
type ProfileInt interface {}
func main() {
var p1, p2 ProfileInt = Profile{"iswbm"}, Profile{"iswbm"}
var p3, p4 ProfileInt = &Profile{"iswbm"}, &Profile{"iswbm"}
fmt.Printf("p1 --> type: %T, data: %v \n", p1, p1)
fmt.Printf("p2 --> type: %T, data: %v \n", p2, p2)
fmt.Println(p1 == p2) // true
fmt.Printf("p3 --> type: %T, data: %p \n", p3, p3)
fmt.Printf("p4 --> type: %T, data: %p \n", p4, p4)
fmt.Println(p3 == p4) // false
}
运行后,输出如下
::
p1 --> type: main.Profile, data: {iswbm}
p2 --> type: main.Profile, data: {iswbm}
true
p3 --> type: *main.Profile, data: 0xc00008e200
p4 --> type: *main.Profile, data: 0xc00008e210
false
第二种情况
~~~~~~~~~~
**特殊情况:两个 interface 都是 nil**
当一个 interface type data 都处于 unset 状态的时候,那么该
interface 的值就为 nil
.. code:: go
package main
import "fmt"
type ProfileInt interface {}
func main() {
var p1, p2 ProfileInt
fmt.Println(p1==p2) // true
}
2. interface interface 比较
---------------------------------
interface 与非 interface 比较时,会将 interface 转换成 interface
,然后再按照 **两个 interface 比较** 的规则进行比较。
示例如下
.. code:: go
package main
import (
"fmt"
"reflect"
)
func main() {
var a string = "iswbm"
var b interface{} = "iswbm"
fmt.Println(a==b) // true
}
上面这种例子可能还好理解,那么请你看下面这个例子,为什么经过反射看到的他们不相等?
.. code:: go
package main
import (
"fmt"
"reflect"
)
func main() {
var a *string = nil
var b interface{} = a
fmt.Println(b==nil) // false
}
因此当 nil 转换为interface 后是 ``(type=nil, data=nil)`` ,这与 b
``(type=*string, data=nil)`` 虽然 data 是一样的,但 type
不相等,因此他们并不相等。
.. |image0| image:: http://image.iswbm.com/20200610235106.png
# 7.26 map 的值不可寻址,那如何修改值的属性?
要回答本题,需要你知道什么是不可寻址。
不急,请先看一下如下这段代码,你知道它有什么问题吗?
```go
package main
type Person struct {
Age int
}
func (p *Person) GrowUp() {
p.Age++
}
func main() {
m := map[string]Person{
"iswbm": Person{Age: 20},
}
m["iswbm"].Age = 23
m["iswbm"].GrowUp()
}
```
没错,这段代码是错误的,当你编译时,会直接报错呢?
原因在于这两行
```go
m["iswbm"].Age = 23
m["iswbm"].GrowUp()
```
我们知道 map 的值是不可寻址的,当你使用 ` m["zhangsan"]` 取得值时,其实返回的是其值的拷贝,虽然与原数据值相同,但是在内存中并不是同一个数据。
也正是这样,当 map 的值是一个普通对象(非指针),是无法直接对其修改的。
针对这种错误,解决方法有两种:
## 第一种:新建变量,修改后再覆盖
```go
func main() {
m := map[string]Person{
"iswbm": Person{Age: 20},
}
p := m["iswbm"]
p.Age = 23
p.GrowUp()
m["iswbm"] = p
}
```
## 第二种:使用指针的方式
```go
func main() {
m := map[string]*Person{
"iswbm": &Person{Age: 20},
}
m["iswbm"].Age = 23
m["iswbm"].GrowUp()
}
```
7.26 map 的值不可寻址,那如何修改值的属性?
===========================================
要回答本题,需要你知道什么是不可寻址。
不急,请先看一下如下这段代码,你知道它有什么问题吗?
.. code:: go
package main
type Person struct {
Age int
}
func (p *Person) GrowUp() {
p.Age++
}
func main() {
m := map[string]Person{
"iswbm": Person{Age: 20},
}
m["iswbm"].Age = 23
m["iswbm"].GrowUp()
}
没错,这段代码是错误的,当你编译时,会直接报错呢?
原因在于这两行
.. code:: go
m["iswbm"].Age = 23
m["iswbm"].GrowUp()
我们知道 map 的值是不可寻址的,当你使用 ``m["zhangsan"]``
取得值时,其实返回的是其值的拷贝,虽然与原数据值相同,但是在内存中并不是同一个数据。
也正是这样,当 map 的值是一个普通对象(非指针),是无法直接对其修改的。
针对这种错误,解决方法有两种:
第一种:新建变量,修改后再覆盖
------------------------------
.. code:: go
func main() {
m := map[string]Person{
"iswbm": Person{Age: 20},
}
p := m["iswbm"]
p.Age = 23
p.GrowUp()
m["iswbm"] = p
}
第二种:使用指针的方式
----------------------
.. code:: go
func main() {
m := map[string]*Person{
"iswbm": &Person{Age: 20},
}
m["iswbm"].Age = 23
m["iswbm"].GrowUp()
}
# 7.27 所有的 T 类型都有 *T 类型吗?
`*T` 类型的对象指的是类型是 T 的对象的指针,很明显,只有当 T 类型的对象,是可以寻址的情况,才可以取到其指针。
诸如字符串、map 的元素、常量、包级别的函数,都是不可寻址的,它们都没有对应的 `*T` 类型
随便举个常量的例子
```go
package main
import "fmt"
type T string
func (T *T) say() {
fmt.Println("hello")
}
func main() {
const NAME T = "iswbm"
NAME.say()
}
```
报错如下
```go
./demo.go:13:6: cannot call pointer method on NAME
./demo.go:13:6: cannot take the address of NAME
```
7.27 所有的 T 类型都有 \*T 类型吗?
===================================
``*T`` 类型的对象指的是类型是 T 的对象的指针,很明显,只有当 T
类型的对象,是可以寻址的情况,才可以取到其指针。
诸如字符串、map
的元素、常量、包级别的函数,都是不可寻址的,它们都没有对应的 ``*T`` 类型
随便举个常量的例子
.. code:: go
package main
import "fmt"
type T string
func (T *T) say() {
fmt.Println("hello")
}
func main() {
const NAME T = "iswbm"
NAME.say()
}
报错如下
.. code:: go
./demo.go:13:6: cannot call pointer method on NAME
./demo.go:13:6: cannot take the address of NAME
# 7.28 有类型常量和无类型常量的区别?
在 Go 语言中,常量分为有类型常量和无类型常量。
```go
// 有类型常量
const VERSION string = "v1.0.0"
// 无类型常量
const RELEASE = 3
```
那么他们有什么区别呢?
当你把有无类型的常量,赋值给一个变量的时候,无类型的常量会被隐式的转化成对应的类型
```go
package main
import "fmt"
func main() {
const RELEASE = 3
var x int16 = RELEASE
var y int32 = RELEASE
fmt.Printf("type: %T \n", x) //type: int16
fmt.Printf("type: %T \n", y) //type: int32
}
```
可要是有类型常量,不就会进行转换,在赋值的时候,类型检查就不会通过,从而直接报错
```go
package main
import "fmt"
func main() {
const RELEASE int8 = 3
var x int16 = RELEASE //cannot use RELEASE (type int8) as type int16 in assignment
var y int32 = RELEASE //cannot use RELEASE (type int8) as type int32 in assignment
fmt.Printf("type: %T \n", x)
fmt.Printf("type: %T \n", y)
}
```
解决的方法是进行显式的转换
```go
package main
import "fmt"
func main() {
const RELEASE int8 = 3
var x int16 = int16(RELEASE)
var y int32 = int32(RELEASE)
fmt.Printf("type: %T \n", x) // type: int16
fmt.Printf("type: %T \n", y) // type: int32
}
```
7.28 有类型常量和无类型常量的区别?
===================================
Go 语言中,常量分为有类型常量和无类型常量。
.. code:: go
// 有类型常量
const VERSION string = "v1.0.0"
// 无类型常量
const RELEASE = 3
那么他们有什么区别呢?
当你把有无类型的常量,赋值给一个变量的时候,无类型的常量会被隐式的转化成对应的类型
.. code:: go
package main
import "fmt"
func main() {
const RELEASE = 3
var x int16 = RELEASE
var y int32 = RELEASE
fmt.Printf("type: %T \n", x) //type: int16
fmt.Printf("type: %T \n", y) //type: int32
}
可要是有类型常量,不就会进行转换,在赋值的时候,类型检查就不会通过,从而直接报错
.. code:: go
package main
import "fmt"
func main() {
const RELEASE int8 = 3
var x int16 = RELEASE //cannot use RELEASE (type int8) as type int16 in assignment
var y int32 = RELEASE //cannot use RELEASE (type int8) as type int32 in assignment
fmt.Printf("type: %T \n", x)
fmt.Printf("type: %T \n", y)
}
解决的方法是进行显式的转换
.. code:: go
package main
import "fmt"
func main() {
const RELEASE int8 = 3
var x int16 = int16(RELEASE)
var y int32 = int32(RELEASE)
fmt.Printf("type: %T \n", x) // type: int16
fmt.Printf("type: %T \n", y) // type: int32
}
# 7.13 为什么使用切片而不使用数组?
目的:
1. 节约内存
2. 合理处理好共享内存
但也有例外:
1. 切片底层是数组,对于小数组在栈上拷贝的消耗有可能比新建(make)大。
参考:https://halfrost.com/go_slice/
7.13 为什么使用切片而不使用数组?
=================================
目的:
1. 节约内存
2. 合理处理好共享内存
但也有例外:
1. 切片底层是数组,对于小数组在栈上拷贝的消耗有可能比新建(make)大。
参考:https://halfrost.com/go_slice/
...@@ -137,6 +137,14 @@ W3Cschool 也是一个专业的编程入门学习及技术文档查询应用, ...@@ -137,6 +137,14 @@ W3Cschool 也是一个专业的编程入门学习及技术文档查询应用,
![](http://image.iswbm.com/image-20210411181553375.png) ![](http://image.iswbm.com/image-20210411181553375.png)
### Effective Go
官方出品的一份适合刚入门的 Go 新手阅读的文档,内容是一些使用 Go 的正确姿势,内容不多,花个时间浏览一下是非常有必要的。
当然了,官方原版([Effective Go 英文版](https://golang.org/doc/effective_go))是英文的,国内已经有人翻译成了中文 ([Effective Go 中文版](https://www.kancloud.cn/kancloud/effective/72199)
![](http://image.iswbm.com/20210910210850.png)
## 2. Web开发 ## 2. Web开发
### gin 中文文档 ### gin 中文文档
......
...@@ -153,6 +153,19 @@ Go语言101 ...@@ -153,6 +153,19 @@ Go语言101
|image12| |image12|
Effective Go
~~~~~~~~~~~~
官方出品的一份适合刚入门的 Go 新手阅读的文档,内容是一些使用 Go
的正确姿势,内容不多,花个时间浏览一下是非常有必要的。
当然了,官方原版(\ `Effective Go
英文版 <https://golang.org/doc/effective_go>`__\ )是英文的,国内已经有人翻译成了中文
(\ `Effective Go
中文版 <https://www.kancloud.cn/kancloud/effective/72199>`__\ )
|image13|
2. Web开发 2. Web开发
---------- ----------
...@@ -196,7 +209,7 @@ Revel 中文文档 ...@@ -196,7 +209,7 @@ Revel 中文文档
**网站链接**\ :https://eddycjy.gitbook.io/golang/ **网站链接**\ :https://eddycjy.gitbook.io/golang/
|image13| |image14|
Go语言圣经 Go语言圣经
~~~~~~~~~~ ~~~~~~~~~~
...@@ -206,7 +219,7 @@ Go语言圣经 ...@@ -206,7 +219,7 @@ Go语言圣经
**网站链接**\ :https://books.studygolang.com/gopl-zh/ **网站链接**\ :https://books.studygolang.com/gopl-zh/
|image14| |image15|
mojotv 进阶系列 mojotv 进阶系列
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
...@@ -217,21 +230,21 @@ mojotv 进阶系列 ...@@ -217,21 +230,21 @@ mojotv 进阶系列
**网站链接**\ :https://mojotv.cn/404#Golang **网站链接**\ :https://mojotv.cn/404#Golang
|image15| |image16|
Go 语言高级编程 Go 语言高级编程
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
**网站链接**\ :https://chai2010.gitbooks.io/advanced-go-programming-book/content/ **网站链接**\ :https://chai2010.gitbooks.io/advanced-go-programming-book/content/
|image16| |image17|
冰霜之地 冰霜之地
~~~~~~~~ ~~~~~~~~
网站链接:https://halfrost.com/tag/golang/ 网站链接:https://halfrost.com/tag/golang/
|image17| |image18|
4. 工具使用 4. 工具使用
----------- -----------
...@@ -247,7 +260,7 @@ go 的命令非常多,如果想系统的学习,推荐郝林的 Go 命令教 ...@@ -247,7 +260,7 @@ go 的命令非常多,如果想系统的学习,推荐郝林的 Go 命令教
2、https://wiki.jikexueyuan.com/project/go-command-tutorial/0.0.html 2、https://wiki.jikexueyuan.com/project/go-command-tutorial/0.0.html
|image18| |image19|
Uber 编程规范 Uber 编程规范
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
...@@ -264,7 +277,7 @@ Uber”,也就是 Go 代码应该怎样写、不该怎样写。 ...@@ -264,7 +277,7 @@ Uber”,也就是 Go 代码应该怎样写、不该怎样写。
中文译文:https://www.infoq.cn/article/G6c95VyU5telNXXCC9yO 中文译文:https://www.infoq.cn/article/G6c95VyU5telNXXCC9yO
|image19| |image20|
Go Walker Go Walker
~~~~~~~~~ ~~~~~~~~~
...@@ -272,7 +285,7 @@ Go Walker ...@@ -272,7 +285,7 @@ Go Walker
Go Walker 是一个可以在线生成并浏览 `Go <https://golang.org/>`__ 项目 API Go Walker 是一个可以在线生成并浏览 `Go <https://golang.org/>`__ 项目 API
文档的 Web 服务器,目前已支持包括 **GitHub** 等代码托管平台。 文档的 Web 服务器,目前已支持包括 **GitHub** 等代码托管平台。
|image20| |image21|
CTOLib 码库 CTOLib 码库
~~~~~~~~~~~ ~~~~~~~~~~~
...@@ -282,7 +295,7 @@ CTOLib 码库 ...@@ -282,7 +295,7 @@ CTOLib 码库
**网站链接**\ :https://www.ctolib.com/go/categories/go-guide.html **网站链接**\ :https://www.ctolib.com/go/categories/go-guide.html
|image21| |image22|
A Tour of Go A Tour of Go
~~~~~~~~~~~~ ~~~~~~~~~~~~
...@@ -290,7 +303,7 @@ A Tour of Go ...@@ -290,7 +303,7 @@ A Tour of Go
通过例子学习 Go 通过例子学习 Go
编程,还提高在线运行环境,可以让你通过浏览器就可以直接运行 go 的程序 编程,还提高在线运行环境,可以让你通过浏览器就可以直接运行 go 的程序
|image22| |image23|
5. 技术社区 5. 技术社区
----------- -----------
...@@ -300,7 +313,7 @@ GoCN ...@@ -300,7 +313,7 @@ GoCN
**网站链接**\ :https://gocn.vip/ **网站链接**\ :https://gocn.vip/
|image23| |image24|
Go 语言中文网 Go 语言中文网
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
...@@ -311,7 +324,7 @@ Go历史版本的下载,各种高质量的电子书资源,各种大牛写的 ...@@ -311,7 +324,7 @@ Go历史版本的下载,各种高质量的电子书资源,各种大牛写的
**网站链接**\ :https://studygolang.com/ **网站链接**\ :https://studygolang.com/
|image24| |image25|
6. 源码学习 6. 源码学习
----------- -----------
...@@ -324,14 +337,14 @@ Go 夜读 ...@@ -324,14 +337,14 @@ Go 夜读
**网站链接**\ :https://talkgo.org/ **网站链接**\ :https://talkgo.org/
|image25| |image26|
Go 语言原本 Go 语言原本
~~~~~~~~~~~ ~~~~~~~~~~~
**网站链接**\ :https://changkun.de/golang/ **网站链接**\ :https://changkun.de/golang/
|image26| |image27|
Go 语言设计与实现 Go 语言设计与实现
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
...@@ -340,14 +353,14 @@ Go 语言设计与实现 ...@@ -340,14 +353,14 @@ Go 语言设计与实现
**网站链接**\ :https://draveness.me/golang/ **网站链接**\ :https://draveness.me/golang/
|image27| |image28|
能翻到这里的,一定是真爱了,本以为 Go 能翻到这里的,一定是真爱了,本以为 Go
语言还处于不温不火的状态,没想到收集整理一下,资料还挺多的。 语言还处于不温不火的状态,没想到收集整理一下,资料还挺多的。
-------------- --------------
|image28| |image29|
.. |image0| image:: http://image.iswbm.com/20200607145423.png .. |image0| image:: http://image.iswbm.com/20200607145423.png
.. |image1| image:: http://image.iswbm.com/20200430112024.png .. |image1| image:: http://image.iswbm.com/20200430112024.png
...@@ -362,20 +375,21 @@ Go 语言设计与实现 ...@@ -362,20 +375,21 @@ Go 语言设计与实现
.. |image10| image:: http://image.iswbm.com/20200430174507.png .. |image10| image:: http://image.iswbm.com/20200430174507.png
.. |image11| image:: http://image.iswbm.com/20200430112319.png .. |image11| image:: http://image.iswbm.com/20200430112319.png
.. |image12| image:: http://image.iswbm.com/image-20210411181553375.png .. |image12| image:: http://image.iswbm.com/image-20210411181553375.png
.. |image13| image:: http://image.iswbm.com/20200430105116.png .. |image13| image:: http://image.iswbm.com/20210910210850.png
.. |image14| image:: http://image.iswbm.com/20200430100755.png .. |image14| image:: http://image.iswbm.com/20200430105116.png
.. |image15| image:: http://image.iswbm.com/20200430095544.png .. |image15| image:: http://image.iswbm.com/20200430100755.png
.. |image16| image:: http://image.iswbm.com/20200430175818.png .. |image16| image:: http://image.iswbm.com/20200430095544.png
.. |image17| image:: http://image.iswbm.com/20210901084203.png .. |image17| image:: http://image.iswbm.com/20200430175818.png
.. |image18| image:: http://image.iswbm.com/20200430102821.png .. |image18| image:: http://image.iswbm.com/20210901084203.png
.. |image19| image:: http://image.iswbm.com/20200430113756.png .. |image19| image:: http://image.iswbm.com/20200430102821.png
.. |image20| image:: http://image.iswbm.com/20200430170054.png .. |image20| image:: http://image.iswbm.com/20200430113756.png
.. |image21| image:: http://image.iswbm.com/20200430174109.png .. |image21| image:: http://image.iswbm.com/20200430170054.png
.. |image22| image:: http://image.iswbm.com/20210902222745.png .. |image22| image:: http://image.iswbm.com/20200430174109.png
.. |image23| image:: http://image.iswbm.com/20200506192127.png .. |image23| image:: http://image.iswbm.com/20210902222745.png
.. |image24| image:: http://image.iswbm.com/20200430134207.png .. |image24| image:: http://image.iswbm.com/20200506192127.png
.. |image25| image:: http://image.iswbm.com/20200430174216.png .. |image25| image:: http://image.iswbm.com/20200430134207.png
.. |image26| image:: http://image.iswbm.com/20200506191803.png .. |image26| image:: http://image.iswbm.com/20200430174216.png
.. |image27| image:: http://image.iswbm.com/20200506191632.png .. |image27| image:: http://image.iswbm.com/20200506191803.png
.. |image28| image:: http://image.iswbm.com/20200607174235.png .. |image28| image:: http://image.iswbm.com/20200506191632.png
.. |image29| image:: http://image.iswbm.com/20200607174235.png
...@@ -26,7 +26,7 @@ func main() { ...@@ -26,7 +26,7 @@ func main() {
## 哪些是可以寻址的? ## 哪些是可以寻址的?
### 变量:`&x` ### 变量:&x
```go ```go
func main() { func main() {
...@@ -36,7 +36,7 @@ func main() { ...@@ -36,7 +36,7 @@ func main() {
} }
``` ```
### 指针:`&*x` ### 指针:&*x
```go ```go
type Profile struct { type Profile struct {
...@@ -44,12 +44,12 @@ type Profile struct { ...@@ -44,12 +44,12 @@ type Profile struct {
} }
func main() { func main() {
fmt.Println(&Profile{}) fmt.Println(unsafe.Pointer(&Profile{Name: "iswbm"}))
// output: 0xc00000e028 // output: 0xc000108040
} }
``` ```
### 数组元素索引: `&a[0]` ### 数组元素索引: &a[0]
```go ```go
func main() { func main() {
...@@ -67,7 +67,7 @@ func main() { ...@@ -67,7 +67,7 @@ func main() {
} }
``` ```
### 切片元素索引:`&s[1]` ### 切片元素索引:&s[1]
```go ```go
func main() { func main() {
...@@ -77,7 +77,7 @@ func main() { ...@@ -77,7 +77,7 @@ func main() {
} }
``` ```
### 组合字面量: `&struct{ X int }{1}` ### 组合字面量: &struct{X type}{value}
所有的组合字面量都是不可寻址的,就像下面这样子 所有的组合字面量都是不可寻址的,就像下面这样子
...@@ -124,12 +124,6 @@ func main() { ...@@ -124,12 +124,6 @@ func main() {
} }
``` ```
网上有博客说
>如果一个结构体值是可寻址的,则它的字段也是可寻址的;反之,一个不可寻址的结构体值的字段也是不可寻址的。 不可寻址的字段的值是不可更改的。所有的组合字面量都是不可寻址的。
对于这种说法,我实在无法理解,结构体值(组合字面量)明明是不可寻址的,但为什么其字段可以寻址呢?
## 哪些是不可以寻址的? ## 哪些是不可以寻址的?
### 常量 ### 常量
......
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册