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

新增 7 篇面试题文章

上级 290814a6
......@@ -72,9 +72,19 @@
* [7.4 Go 有异常类型吗?](https://golang.iswbm.com/c07/c07_04.html)
* [7.5 Go 中的 rune 类型是什么?](https://golang.iswbm.com/c07/c07_05.html)
* [7.6 Go 语言中的深拷贝和浅拷贝?](https://golang.iswbm.com/c07/c07_06.html)
* [7.7 局部变量分配在栈上还是堆上?](https://golang.iswbm.com/c07/c07_07.html)
* [7.8 什么叫字面量和组合字面量?](https://golang.iswbm.com/c07/c07_08.html)
* [7.9 为什么常量、字符串和字典不可寻址?](https://golang.iswbm.com/c07/c07_09.html)
* [7.10 为什么 slice 元素是可寻址的?](https://golang.iswbm.com/c07/c07_10.html)
* [7.11 对象选择器自动解引用怎么用?](https://golang.iswbm.com/c07/c07_11.html)
* [7.12 slice 扩容后容量及内存如何计算?](https://golang.iswbm.com/c07/c07_12.html)
* [7.13 goroutine 存在的意义是什么?](https://golang.iswbm.com/c07/c07_13.html)
* [7.14 说说 Go 中闭包的底层原理?](https://golang.iswbm.com/c07/c07_14.html)
- **第八章:暂未分类**
* [8.1 20 个学习 Go 语言的精品网站](https://golang.iswbm.com/c08/c08_01.html)
* [8.2 Go 语言中边界检查](https://golang.iswbm.com/c08/c08_02.html)
* [8.3 Go 语言中的内存分配规律及逃逸分析](https://golang.iswbm.com/c08/c08_03.html)
* [8.4 Go 中晦涩难懂的寻址问题](https://golang.iswbm.com/c08/c08_04.html)
## 欢迎交流
......
......@@ -36,7 +36,75 @@ type Profile struct {
}
```
## 2. 定义方法
若相邻的属性(字段)是相同类型,可以合并写在一起
```go
type Profile struct {
name,gender string
age int
mother *Profile // 指针
father *Profile // 指针
}
```
通过结构体可以定义一个组合字面量,有几个细节,也算是规则,需要你注意。
**规则一**:当最后一个字段和结果不在同一行时,`,` 不可省略。
```go
xm := Profile{
name: "小明",
age: 18,
gender: "male",
}
```
反之,不在同一行,就可以省略。
```go
xm := Profile{
name: "小明",
age: 18,
gender: "male"}
```
**规则二**:字段名要嘛全写,要嘛全不写,不能有的写,有的不写。
例如下面这种写法是会报 `mixture of field:value and value initializers` 错误的
```go
xm := Profile{
name: "小明",
18,
"male",
}
```
**规则三**:初始化结构体,并不一定要所有字段都赋值,未被赋值的字段,会自动赋值为其类型的零值。
```go
xm := Profile{name: "小明"}
fmt.Println(xm.age)
// output: 0
```
但要注意的是,只有通过指定字段名才可以赋值部分字段。
若你没有指定字段名,像这样
```go
xm := Profile{"小明"}
```
在编译的时候,是会直接报错的
```shell
$ go run demo.go
# command-line-arguments
./demo.go:12:16: too few values in Profile literal
```
## 2. 绑定方法
在 Go 语言中,我们无法在结构体内定义方法,那如何给一个结构体定义方法呢,答案是可以使用组合函数的方式来定义结构体方法。它和普通函数的定义方式有些不一样,比如下面这个方法
......@@ -256,6 +324,89 @@ func main() {
## 6. 三种实例化方法
### 第一种:正常实例化
```go
func main() {
xm := Profile{
name: "小明",
age: 18,
gender: "male",
}
}
```
### 第二种:使用 new
```go
func main() {
xm := new(Profile)
// 等价于: var xm *Profile = new(Profile)
fmt.Println(xm)
// output: &{ 0 }
xm.name = "iswbm" // 或者 (*xm).name = "iswbm"
xm.age = 18 // 或者 (*xm).age = 18
xm.gender = "male" // 或者 (*xm).gender = "male"
fmt.Println(xm)
//output: &{iswbm 18 male}
}
```
### 第三种:使用 &
```go
func main() {
var xm *Profile = &Profile{}
fmt.Println(xm)
// output: &{ 0 }
xm.name = "iswbm" // 或者 (*xm).name = "iswbm"
xm.age = 18 // 或者 (*xm).age = 18
xm.gender = "male" // 或者 (*xm).gender = "male"
fmt.Println(xm)
//output: &{iswbm 18 male}
}
```
## 7. 选择器的冷知识
从一个结构体实例对象中获取字段的值,通常都是使用 `.` 这个操作符,该操作符叫做 **选择器**
选择器有一个妙用,可能大多数人都不清楚。
当你对象是结构体对象的指针时,你想要获取字段属性时,按照常规理解应该这么做
```go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println((*p1).Name) // output: iswbm
}
```
但还有一个更简洁的做法,可以直接省去 `*` 取值的操作,选择器 `.` 会直接解引用,示例如下
```go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println(p1.Name) // output: iswbm
}
```
---
![](http://image.iswbm.com/20200607174235.png)
\ No newline at end of file
......@@ -41,7 +41,76 @@
father *Profile // 指针
}
2. 定义方法
若相邻的属性(字段)是相同类型,可以合并写在一起
.. code:: go
type Profile struct {
name,gender string
age int
mother *Profile // 指针
father *Profile // 指针
}
通过结构体可以定义一个组合字面量,有几个细节,也算是规则,需要你注意。
**规则一**\ :当最后一个字段和结果不在同一行时,\ ``,`` 不可省略。
.. code:: go
xm := Profile{
name: "小明",
age: 18,
gender: "male",
}
反之,不在同一行,就可以省略。
.. code:: go
xm := Profile{
name: "小明",
age: 18,
gender: "male"}
**规则二**\ :字段名要嘛全写,要嘛全不写,不能有的写,有的不写。
例如下面这种写法是会报 ``mixture of field:value and value initializers``
错误的
.. code:: go
xm := Profile{
name: "小明",
18,
"male",
}
**规则三**\ :初始化结构体,并不一定要所有字段都赋值,未被赋值的字段,会自动赋值为其类型的零值。
.. code:: go
xm := Profile{name: "小明"}
fmt.Println(xm.age)
// output: 0
但要注意的是,只有通过指定字段名才可以赋值部分字段。
若你没有指定字段名,像这样
.. code:: go
xm := Profile{"小明"}
在编译的时候,是会直接报错的
.. code:: shell
$ go run demo.go
# command-line-arguments
./demo.go:12:16: too few values in Profile literal
2. 绑定方法
-----------
Go
......@@ -265,6 +334,92 @@ staff 中,做为 staff 的一个匿名字段,staff 就直接拥有了 compan
- 当方法的首字母为大写时,这个方法对于所有包都是Public,其他包可以随意调用
- 当方法的首字母为小写时,这个方法是Private,其他包是无法访问的。
6. 三种实例化方法
-----------------
第一种:正常实例化
~~~~~~~~~~~~~~~~~~
.. code:: go
func main() {
xm := Profile{
name: "小明",
age: 18,
gender: "male",
}
}
第二种:使用 new
~~~~~~~~~~~~~~~~
.. code:: go
func main() {
xm := new(Profile)
// 等价于: var xm *Profile = new(Profile)
fmt.Println(xm)
// output: &{ 0 }
xm.name = "iswbm" // 或者 (*xm).name = "iswbm"
xm.age = 18 // 或者 (*xm).age = 18
xm.gender = "male" // 或者 (*xm).gender = "male"
fmt.Println(xm)
//output: &{iswbm 18 male}
}
第三种:使用 &
~~~~~~~~~~~~~~
.. code:: go
func main() {
var xm *Profile = &Profile{}
fmt.Println(xm)
// output: &{ 0 }
xm.name = "iswbm" // 或者 (*xm).name = "iswbm"
xm.age = 18 // 或者 (*xm).age = 18
xm.gender = "male" // 或者 (*xm).gender = "male"
fmt.Println(xm)
//output: &{iswbm 18 male}
}
7. 选择器的冷知识
-----------------
从一个结构体实例对象中获取字段的值,通常都是使用 ``.``
这个操作符,该操作符叫做 **选择器**\
选择器有一个妙用,可能大多数人都不清楚。
当你对象是结构体对象的指针时,你想要获取字段属性时,按照常规理解应该这么做
.. code:: go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println((*p1).Name) // output: iswbm
}
但还有一个更简洁的做法,可以直接省去 ``*`` 取值的操作,选择器 ``.``
会直接解引用,示例如下
.. code:: go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println(p1.Name) // output: iswbm
}
--------------
|image1|
......
# 7.8 什么叫字面量和组合字面量?
值的字面量是代码中值的文字表示。一个值可能存在多种字面量表示。
举个例子,十进制的数值 15,可以由三种字面量表示
```go
// 16进制
0xF
// 8进制
0o17
// 2进制
0b1111
```
通过比较,可以看出他们是相等的
```go
import "fmt"
func main() {
fmt.Println(15 == 0xF) // true
fmt.Println(15 == 017) // true
fmt.Println(15 == 0b1111) // true
}
```
表示基本类型值的文本称为基本字面量,基本字面量也被称为字面量常量或未命名常量。包括:
- 布尔字面量(非严格意义上的字面量)
- 整型字面量
- 浮点数字面量
- 虚数字面量
- Rune 字面量
- 字符串字面量
- 等等
基本字面量,很好理解,那组合字面量呢?
不就是把多个基本字面量组合在一起的字面量嘛。
比如结构体,切片,数组,字典,他们都可以容纳多个基本字面量。
参考阅读:https://www.jianshu.com/p/0134120abc39
\ No newline at end of file
7.8 什么叫字面量和组合字面量?
==============================
值的字面量是代码中值的文字表示。一个值可能存在多种字面量表示。
举个例子,十进制的数值 15,可以由三种字面量表示
.. code:: go
// 16进制
0xF
// 8进制
0o17
// 2进制
0b1111
通过比较,可以看出他们是相等的
.. code:: go
import "fmt"
func main() {
fmt.Println(15 == 0xF) // true
fmt.Println(15 == 017) // true
fmt.Println(15 == 0b1111) // true
}
表示基本类型值的文本称为基本字面量,基本字面量也被称为字面量常量或未命名常量。包括:
- 布尔字面量(非严格意义上的字面量)
- 整型字面量
- 浮点数字面量
- 虚数字面量
- Rune 字面量
- 字符串字面量
- 等等
基本字面量,很好理解,那组合字面量呢?
不就是把多个基本字面量组合在一起的字面量嘛。
比如结构体,切片,数组,字典,他们都可以容纳多个基本字面量。
参考阅读:https://www.jianshu.com/p/0134120abc39
# 7.9 为什么常量、字符串和字典不可寻址?
## 常量
首先要明白一件事,什么叫不可寻址?它指的是,不能通过 `&` 来获得内存地址的行为。
以常量为例
```go
import "fmt"
const VERSION = "1.0"
func main() {
fmt.Println(&VERSION)
}
```
编译的时候会直接报错说:无法取得 VERSION 的地址。**因为它是常量**
```sh
$ go build main.go
# command-line-arguments
./main.go:8:14: cannot take the address of VERSION
```
这其实还挺容易理解的,如果常量可以寻址的话,我们就可以通过指针修改常数的值,这无疑破坏了常数的定义。
## 字典
那么字典里的元素呢?为什么它不可以寻址?
```go
func main() {
p := map[string]string {
"name": "iswbm",
}
fmt.Println(&p["name"])
// cannot take the address of p["name"]
}
```
它比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现 什么问题?
1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,如果能寻址问题就大了。
2. 而如果字典的元素存在,考虑到 Go 中 map 实现中元素的地址是变化的,这意味着寻址的结果也是无意义的。
基于这两点,Map 中的元素不可寻址,符合常理。
## 字符串
字符串是不可变的,因此不可寻址,没有问题
7.9 为什么常量、字符串和字典不可寻址?
======================================
常量
----
首先要明白一件事,什么叫不可寻址?它指的是,不能通过 ``&``
来获得内存地址的行为。
以常量为例
.. code:: go
import "fmt"
const VERSION = "1.0"
func main() {
fmt.Println(&VERSION)
}
编译的时候会直接报错说:无法取得 VERSION 的地址。\ **因为它是常量**
.. code:: sh
$ go build main.go
# command-line-arguments
./main.go:8:14: cannot take the address of VERSION
这其实还挺容易理解的,如果常量可以寻址的话,我们就可以通过指针修改常数的值,这无疑破坏了常数的定义。
字典
----
那么字典里的元素呢?为什么它不可以寻址?
.. code:: go
func main() {
p := map[string]string {
"name": "iswbm",
}
fmt.Println(&p["name"])
// cannot take the address of p["name"]
}
它比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现
什么问题?
1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,如果能寻址问题就大了。
2. 而如果字典的元素存在,考虑到 Go 中 map
实现中元素的地址是变化的,这意味着寻址的结果也是无意义的。
基于这两点,Map 中的元素不可寻址,符合常理。
字符串
------
字符串是不可变的,因此不可寻址,没有问题
# 7.10 为什么 slice 元素是可寻址的?
因为 slice 底层实现了一个数组,它是可以寻址的。
7.10 为什么 slice 元素是可寻址的?
==================================
因为 slice 底层实现了一个数组,它是可以寻址的。
# 7.11 对象选择器自动解引用怎么用?
从一个结构体实例对象中获取字段的值,通常都是使用 `.` 这个操作符,该操作符叫做 **选择器**
选择器有一个妙用,可能大多数人都不清楚。
当你对象是结构体对象的指针时,你想要获取字段属性时,按照常规理解应该这么做
```go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println((*p1).Name) // output: iswbm
}
```
但还有一个更简洁的做法,可以直接省去 `*` 取值的操作,选择器 `.` 会直接解引用,示例如下
```go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println(p1.Name) // output: iswbm
}
```
7.11 对象选择器自动解引用怎么用?
=================================
从一个结构体实例对象中获取字段的值,通常都是使用 ``.``
这个操作符,该操作符叫做 **选择器**\ 。
选择器有一个妙用,可能大多数人都不清楚。
当你对象是结构体对象的指针时,你想要获取字段属性时,按照常规理解应该这么做
.. code:: go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println((*p1).Name) // output: iswbm
}
但还有一个更简洁的做法,可以直接省去 ``*`` 取值的操作,选择器 ``.``
会直接解引用,示例如下
.. code:: go
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println(p1.Name) // output: iswbm
}
# 7.12 slice 扩容后容量及内存如何计算?
## 1. 扩容后预估容量
假设现在有一个长度为 2 的切片,对其进行扩容,增加三个元素
```go
sli := []int{1,2}
sli = append(sli, 3, 4, 5
```
对于扩容后的切片,长度为 5,这一点没有任何争议。
但容量呢?难道也是 5?
经过运行验证,实际的容量为 6 。
什么情况?这 6 是如何计算出来的呢?
这就不得不去 Go 源代码中一探究竟,在 `runtime/slice.go` 关于 slice 扩容增长的代码如下:
```go
newcap := old.cap
if newcap+newcap < cap {
newcap = cap
} else {
for {
if old.len < 1024 {
newcap += newcap
} else {
newcap += newcap / 4
}
if newcap >= cap {
break
}
}
}
```
对于这段代码,只要理解前两行,其他的就不攻自破了
- 第一行的 old.cap:扩容前的容量,对于此例,就是 2
- 第二行的 cap:扩容前容量加上扩容的元素数量,对于此例,就是 2+3
整段代码的核心就是要计算出扩容后的预估容量,也就是 newcap,计算的具体逻辑是:
1. 若 old.cap * 2 小于 cap,那 newcap 就取大的 cap
2. 若 old.cap * 2 大于 cap,并且old.cap 小于 1024,那 newcap 还是取大,也即 newcap = old.cap * 2
3. 若 old.cap * 2 大于 cap,但是old.cap 大于 1024,那两倍冗余可能就有点大了,系数换成 1.25,也即 newcap = old.cap * 2
但 newcap 只是预估容量,并不是最终的容量,要计算最终的容量,还需要参考另一个维度,也就是内存分配。
## 2. 内存的分配规律
举个现实中的例子来说
你家里有五个人,每个人都想吃绿豆糕,因此你的需求就是 5,对应上例中的 cap ,于是你就到超市里去买。
但超市并不是你家开的,绿豆糕都是整盒整盒的卖,没有卖散的,每盒的数量是 6 个,因此你最少买 6 个。
每次购买的最少数量,就可以类比做 Go 的内存分配规律。
只有了解了 Go 的内存分配规律,我们才能准确的计算出我们最少得买多少的绿豆糕(得申请多少的内存,分配多少的容量)。
关于内存管理模块的代码,在 `runtime/sizeclasses.go`
```go
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
...
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
...
// 66 32768 32768 1 0 12.50%
```
从上面这个表格中,可以总结出一些规律。
- 在小于16字节时,每次以8个字节增加
- 当大于16小于2^8时,每次以16字节增加
- 当大于2^8小于2^9时以32字节增加
- 依此规律…
## 3. 匹配到合适的内存
第一节中我们例子中,主人公是一个元素类型为 int 的切片,每个 int 占用为 8 个字节,由于我们计算出的 newcap 为 5,因此新的切片,最少最少要占用 5*8 = 40 个字节。
再到第二节中的表格中查看,发现离 40 byte 最接近的是 32 和 48 两个档位。
如果是 32 byte,就是不够用了,
因此 只能选择 48 这个档位去分配内存。
有了实际分配的内存,再反回去计算容量,就是扩容后真实的切片容量,也就是 `48/8 = 6`
7.12 slice 扩容后容量及内存如何计算?
=====================================
1. 扩容后预估容量
-----------------
假设现在有一个长度为 2 的切片,对其进行扩容,增加三个元素
.. code:: go
sli := []int{1,2}
sli = append(sli, 3, 4, 5)
对于扩容后的切片,长度为 5,这一点没有任何争议。
但容量呢?难道也是 5?
经过运行验证,实际的容量为 6 。
什么情况?这 6 是如何计算出来的呢?
这就不得不去 Go 源代码中一探究竟,在 ``runtime/slice.go`` 关于 slice
扩容增长的代码如下:
.. code:: go
newcap := old.cap
if newcap+newcap < cap {
newcap = cap
} else {
for {
if old.len < 1024 {
newcap += newcap
} else {
newcap += newcap / 4
}
if newcap >= cap {
break
}
}
}
对于这段代码,只要理解前两行,其他的就不攻自破了
- 第一行的 old.cap:扩容前的容量,对于此例,就是 2
- 第二行的 cap:扩容前容量加上扩容的元素数量,对于此例,就是 2+3
整段代码的核心就是要计算出扩容后的预估容量,也就是
newcap,计算的具体逻辑是:
1. 若 old.cap \* 2 小于 cap,那 newcap 就取大的 cap
2. 若 old.cap \* 2 大于 cap,并且old.cap 小于 1024,那 newcap
还是取大,也即 newcap = old.cap \* 2
3. 若 old.cap \* 2 大于 cap,但是old.cap 大于
1024,那两倍冗余可能就有点大了,系数换成 1.25,也即 newcap = old.cap
\* 2
但 newcap
只是预估容量,并不是最终的容量,要计算最终的容量,还需要参考另一个维度,也就是内存分配。
2. 内存的分配规律
-----------------
举个现实中的例子来说
你家里有五个人,每个人都想吃绿豆糕,因此你的需求就是 5,对应上例中的 cap
,于是你就到超市里去买。
但超市并不是你家开的,绿豆糕都是整盒整盒的卖,没有卖散的,每盒的数量是 6
个,因此你最少买 6 个。
每次购买的最少数量,就可以类比做 Go 的内存分配规律。
只有了解了 Go
的内存分配规律,我们才能准确的计算出我们最少得买多少的绿豆糕(得申请多少的内存,分配多少的容量)。
关于内存管理模块的代码,在 ``runtime/sizeclasses.go``
.. code:: go
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
...
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
...
// 66 32768 32768 1 0 12.50%
从上面这个表格中,可以总结出一些规律。
- 在小于16字节时,每次以8个字节增加
- 当大于16小于2^8时,每次以16字节增加
- 当大于2\ :sup:`8小于2`\ 9时以32字节增加
- 依此规律…
3. 匹配到合适的内存
-------------------
第一节中我们例子中,主人公是一个元素类型为 int 的切片,每个 int 占用为 8
个字节,由于我们计算出的 newcap 为 5,因此新的切片,最少最少要占用 5*8 =
40 个字节。
再到第二节中的表格中查看,发现离 40 byte 最接近的是 32 和 48 两个档位。
如果是 32 byte,就是不够用了,
因此 只能选择 48 这个档位去分配内存。
有了实际分配的内存,再反回去计算容量,就是扩容后真实的切片容量,也就是
``48/8 = 6``
# 7.13 goroutine 存在的意义是什么?
线程其实分两种:
- 一种是传统意义的操作系统线程
- 一种是编程语言实现的用户态线程,也称为协程,在 Go 中就是 goroutine
因此,goroutine 的存在必然是为了换个方式解决操作系统线程的一些弊端 -- **太重**
太重表现在如下几个方面:
**第一:创建和切换太重**
操作系统线程的创建和切换都需要进入内核,而进入内核所消耗的性能代价比较高,开销较大;
**第二:内存使用太重**
一方面,为了尽量避免极端情况下操作系统线程栈的溢出,内核在创建操作系统线程时默认会为其分配一个较大的栈内存(虚拟地址空间,内核并不会一开始就分配这么多的物理内存),然而在绝大多数情况下,系统线程远远用不了这么多内存,这导致了浪费;
另一方面,栈内存空间一旦创建和初始化完成之后其大小就不能再有变化,这决定了在某些特殊场景下系统线程栈还是有溢出的风险。
相对的,用户态的goroutine则轻量得多:
- goroutine是用户态线程,其创建和切换都在用户代码中完成而无需进入操作系统内核,所以其开销要远远小于系统线程的创建和切换;
- goroutine启动时默认栈大小只有2k,这在多数情况下已经够用了,即使不够用,goroutine的栈也会自动扩大,同时,如果栈太大了过于浪费它还能自动收缩,这样既没有栈溢出的风险,也不会造成栈内存空间的大量浪费。
7.13 goroutine 存在的意义是什么?
=================================
线程其实分两种:
- 一种是传统意义的操作系统线程
- 一种是编程语言实现的用户态线程,也称为协程,在 Go 中就是 goroutine
因此,goroutine 的存在必然是为了换个方式解决操作系统线程的一些弊端 –
**太重** 。
太重表现在如下几个方面:
**第一:创建和切换太重**
操作系统线程的创建和切换都需要进入内核,而进入内核所消耗的性能代价比较高,开销较大;
**第二:内存使用太重**
一方面,为了尽量避免极端情况下操作系统线程栈的溢出,内核在创建操作系统线程时默认会为其分配一个较大的栈内存(虚拟地址空间,内核并不会一开始就分配这么多的物理内存),然而在绝大多数情况下,系统线程远远用不了这么多内存,这导致了浪费;
另一方面,栈内存空间一旦创建和初始化完成之后其大小就不能再有变化,这决定了在某些特殊场景下系统线程栈还是有溢出的风险。
相对的,用户态的goroutine则轻量得多:
- goroutine是用户态线程,其创建和切换都在用户代码中完成而无需进入操作系统内核,所以其开销要远远小于系统线程的创建和切换;
- goroutine启动时默认栈大小只有2k,这在多数情况下已经够用了,即使不够用,goroutine的栈也会自动扩大,同时,如果栈太大了过于浪费它还能自动收缩,这样既没有栈溢出的风险,也不会造成栈内存空间的大量浪费。
# 7.14 说说 Go 中闭包的底层原理?
## 1. 什么是闭包?
一个函数内引用了外部的局部变量,这个函数内的值,就称之为闭包。
例如下面的这段代码中,adder 函数返回了一个匿名函数,而该匿名函数中引用了 adder 函数中的局部变量 `sum` ,那这个函数就是一个闭包。
```go
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
```
而这个闭包中引用的外部局部变量并不会随着 adder 函数的返回而被从栈上销毁。
我们尝试着调用这个函数,发现每一次调用,sum 的值都会保留在 闭包函数中以待使用。
```go
func main() {
valueFunc:= adder()
fmt.Println(valueFunc(2)) // output: 2
fmt.Println(valueFunc(2)) // output: 4
}
```
## 2. 复杂的闭包场景
写一个闭包是比较容易的事,但单单会写简单的闭包函数,还远远不够,如果不搞清楚闭包真正的原理,那很容易在一些复杂的闭包场景中对函数的执行逻辑进行误判。
别的不说,就拿下来这个例子来说吧?
你觉得它会打印什么呢?
是 6 还是 11 呢?
```go
import "fmt"
func func1() (i int) {
i = 10
defer func() {
i += 1
}()
return 5
}
func main() {
closure := func1()
fmt.Println(closure)
}
```
## 3. 闭包的底层原理?
还是以最上面的例子来分析
```go
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
valueFunc:= adder()
fmt.Println(valueFunc(2)) // output: 2
}
```
我们先对它进行逃逸分析,很容易发现 sum 作为 adder 函数局部变量,并不是分配在栈上,而是分配在堆上的。
这就解决了第一个疑惑:**为什么 adder 函数返回后, sum 不会随之销毁?**
```go
$ go build -gcflags="-m -m -l" demo.go
# command-line-arguments
./demo.go:8:3: adder.func1 capturing by ref: sum (addr=true assign=true width=8)
./demo.go:7:9: func literal escapes to heap:
./demo.go:7:9: flow: ~r0 = &{storage for func literal}:
./demo.go:7:9: from func literal (spill) at ./demo.go:7:9
./demo.go:7:9: from return func literal (return) at ./demo.go:7:2
./demo.go:6:2: sum escapes to heap:
./demo.go:6:2: flow: {storage for func literal} = &sum:
./demo.go:6:2: from func literal (captured by a closure) at ./demo.go:7:9
./demo.go:6:2: from sum (reference) at ./demo.go:8:3
./demo.go:6:2: moved to heap: sum
./demo.go:7:9: func literal escapes to heap
./demo.go:15:23: valueFunc(2) escapes to heap:
./demo.go:15:23: flow: {storage for ... argument} = &{storage for valueFunc(2)}:
./demo.go:15:23: from valueFunc(2) (spill) at ./demo.go:15:23
./demo.go:15:23: flow: {heap} = {storage for ... argument}:
./demo.go:15:23: from ... argument (spill) at ./demo.go:15:13
./demo.go:15:23: from fmt.Println(valueFunc(2)) (call parameter) at ./demo.go:15:13
./demo.go:15:13: ... argument does not escape
./demo.go:15:23: valueFunc(2) escapes to heap
```
可另一个问题,又浮现出来了,就算它不会销毁,那闭包函数若是存储的若是 sum 拷贝后的值,那每次调用闭包函数,里面的 sum 应该都是一样的,调用两次都应该返回 2,而不是可以累加记录。
因此,可以大胆猜测,闭包函数的结构体里存储的是 sum 的指针。
为了验证这一猜想,只能上汇编了。
通过执行下面的命令,可以输出对应的汇编代码
```shell
go build -gcflags="-S" demo.go
```
输出的内容相当之多,我提取出下面最关键的一行代码,它定义了闭包函数的结构体。
其中 F 是函数的指针,但这不是重点,重点是 sum 存储的确实是指针,验证了我们的猜。
```
type.noalg.struct { F uintptr; "".sum *int }(SB), CX
```
## 4. 迷题揭晓
有了上面第三节的背景知识,那对于第二节给出的这道题,想必你也有答案了。
首先,由于 `i` 在函数定义的返回值上声明,因此根据 go 的 `caller-save` 模式, `i` 变量会存储在 main 函数的栈空间。
然后,`func1` 的 return 重新把 5 赋值给了 `i` ,此时 `i = 5`
由于闭包函数存储了这个变量 `i` 的指针。
因此最后,在 defer 中对 `i` 进行自增,是直接更新到 `i` 的指针上,此时 `i = 5+1`,所以最终打印出来的结果是 `6`
```go
import "fmt"
func func1() (i int) {
i = 10
defer func() {
i += 1
}()
return 5
}
func main() {
closure := func1()
fmt.Println(closure)
}
```
## 5. 再度变题
上面那题听懂了的话,再来看看下面这道题。
`func1` 的返回值我们不写变量名 `i` 了,然后原先返回具体字面量,现在改成变量 `i` ,就是这两小小小的改动,会导致运行结果大大不同,你可以思考一下结果。
```go
import "fmt"
func func1() (int) {
i := 10
defer func() {
i += 1
}()
return i
}
func main() {
closure := func1()
fmt.Println(closure)
}
```
如果你在返回值里写了变量名,那么该变量会存储 main 的栈空间里,而如果你不写,那 i 只能存储在 `func1` 的栈空间里,与此同时,return 的值,不会作用于原变量 `i` 上,而是会存储在该函数在另一块栈内存里。
因此你在 defer 中对原 `i` 进行自增,并不会作用到 func1 的返回值上。
所以打印的结果,只能是 `10`
你答对了吗?
## 6. 最后一个问题
不知道你有没有发现,在第一节示例中的 sum 是存储在堆内存中的,而后面几个示例都是存储在栈内存里。
这是为什么呢?
仔细对比,不难发现,示例一返回的是闭包函数,闭包函数在 `adder` 返回后还要在其他地方继续使用,在这种情况下,为了保证闭包函数的正常运行,无论闭包函数在哪里,`i` 都不能回收,所以 Go 编译器会智能地将其分配在堆上。
而后面的其他示例,都只是涉及了闭包的特性,并不是直接把闭包函数返回,因此完全可以将其分配在栈上,非常的合理。
7.14 说说 Go 中闭包的底层原理?
===============================
1. 什么是闭包?
---------------
一个函数内引用了外部的局部变量,这个函数内的值,就称之为闭包。
例如下面的这段代码中,adder 函数返回了一个匿名函数,而该匿名函数中引用了
adder 函数中的局部变量 ``sum`` ,那这个函数就是一个闭包。
.. code:: go
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
而这个闭包中引用的外部局部变量并不会随着 adder
函数的返回而被从栈上销毁。
我们尝试着调用这个函数,发现每一次调用,sum 的值都会保留在
闭包函数中以待使用。
.. code:: go
func main() {
valueFunc:= adder()
fmt.Println(valueFunc(2)) // output: 2
fmt.Println(valueFunc(2)) // output: 4
}
2. 复杂的闭包场景
-----------------
写一个闭包是比较容易的事,但单单会写简单的闭包函数,还远远不够,如果不搞清楚闭包真正的原理,那很容易在一些复杂的闭包场景中对函数的执行逻辑进行误判。
别的不说,就拿下来这个例子来说吧?
你觉得它会打印什么呢?
6 还是 11 呢?
.. code:: go
import "fmt"
func func1() (i int) {
i = 10
defer func() {
i += 1
}()
return 5
}
func main() {
closure := func1()
fmt.Println(closure)
}
3. 闭包的底层原理?
-------------------
还是以最上面的例子来分析
.. code:: go
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
valueFunc:= adder()
fmt.Println(valueFunc(2)) // output: 2
}
我们先对它进行逃逸分析,很容易发现 sum 作为 adder
函数局部变量,并不是分配在栈上,而是分配在堆上的。
这就解决了第一个疑惑:\ **为什么 adder 函数返回后, sum 不会随之销毁?**
.. code:: go
$ go build -gcflags="-m -m -l" demo.go
# command-line-arguments
./demo.go:8:3: adder.func1 capturing by ref: sum (addr=true assign=true width=8)
./demo.go:7:9: func literal escapes to heap:
./demo.go:7:9: flow: ~r0 = &{storage for func literal}:
./demo.go:7:9: from func literal (spill) at ./demo.go:7:9
./demo.go:7:9: from return func literal (return) at ./demo.go:7:2
./demo.go:6:2: sum escapes to heap:
./demo.go:6:2: flow: {storage for func literal} = &sum:
./demo.go:6:2: from func literal (captured by a closure) at ./demo.go:7:9
./demo.go:6:2: from sum (reference) at ./demo.go:8:3
./demo.go:6:2: moved to heap: sum
./demo.go:7:9: func literal escapes to heap
./demo.go:15:23: valueFunc(2) escapes to heap:
./demo.go:15:23: flow: {storage for ... argument} = &{storage for valueFunc(2)}:
./demo.go:15:23: from valueFunc(2) (spill) at ./demo.go:15:23
./demo.go:15:23: flow: {heap} = {storage for ... argument}:
./demo.go:15:23: from ... argument (spill) at ./demo.go:15:13
./demo.go:15:23: from fmt.Println(valueFunc(2)) (call parameter) at ./demo.go:15:13
./demo.go:15:13: ... argument does not escape
./demo.go:15:23: valueFunc(2) escapes to heap
可另一个问题,又浮现出来了,就算它不会销毁,那闭包函数若是存储的若是 sum
拷贝后的值,那每次调用闭包函数,里面的 sum
应该都是一样的,调用两次都应该返回 2,而不是可以累加记录。
因此,可以大胆猜测,闭包函数的结构体里存储的是 sum 的指针。
为了验证这一猜想,只能上汇编了。
通过执行下面的命令,可以输出对应的汇编代码
.. code:: shell
go build -gcflags="-S" demo.go
输出的内容相当之多,我提取出下面最关键的一行代码,它定义了闭包函数的结构体。
其中 F 是函数的指针,但这不是重点,重点是 sum
存储的确实是指针,验证了我们的猜。
::
type.noalg.struct { F uintptr; "".sum *int }(SB), CX
4. 迷题揭晓
-----------
有了上面第三节的背景知识,那对于第二节给出的这道题,想必你也有答案了。
首先,由于 ``i`` 在函数定义的返回值上声明,因此根据 go
``caller-save`` 模式, ``i`` 变量会存储在 main 函数的栈空间。
然后,\ ``func1`` return 重新把 5 赋值给了 ``i`` ,此时 ``i = 5``
由于闭包函数存储了这个变量 ``i`` 的指针。
因此最后,在 defer 中对 ``i`` 进行自增,是直接更新到 ``i``
的指针上,此时 ``i = 5+1``\ ,所以最终打印出来的结果是 ``6``
.. code:: go
import "fmt"
func func1() (i int) {
i = 10
defer func() {
i += 1
}()
return 5
}
func main() {
closure := func1()
fmt.Println(closure)
}
5. 再度变题
-----------
上面那题听懂了的话,再来看看下面这道题。
``func1`` 的返回值我们不写变量名 ``i``
了,然后原先返回具体字面量,现在改成变量 ``i``
,就是这两小小小的改动,会导致运行结果大大不同,你可以思考一下结果。
.. code:: go
import "fmt"
func func1() (int) {
i := 10
defer func() {
i += 1
}()
return i
}
func main() {
closure := func1()
fmt.Println(closure)
}
如果你在返回值里写了变量名,那么该变量会存储 main
的栈空间里,而如果你不写,那 i 只能存储在 ``func1``
的栈空间里,与此同时,return 的值,不会作用于原变量 ``i``
上,而是会存储在该函数在另一块栈内存里。
因此你在 defer 中对原 ``i`` 进行自增,并不会作用到 func1 的返回值上。
所以打印的结果,只能是 ``10``\
你答对了吗?
6. 最后一个问题
---------------
不知道你有没有发现,在第一节示例中的 sum
是存储在堆内存中的,而后面几个示例都是存储在栈内存里。
这是为什么呢?
仔细对比,不难发现,示例一返回的是闭包函数,闭包函数在 ``adder``
返回后还要在其他地方继续使用,在这种情况下,为了保证闭包函数的正常运行,无论闭包函数在哪里,\ ``i``
都不能回收,所以 Go 编译器会智能地将其分配在堆上。
而后面的其他示例,都只是涉及了闭包的特性,并不是直接把闭包函数返回,因此完全可以将其分配在栈上,非常的合理。
8.4 Go 中晦涩难懂的寻址问题
===========================
什么叫可寻址?
--------------
可直接使用 ``&``
操作符取地址的对象,就是可寻址的(Addressable)。比如下面这个例子
.. code:: go
func main() {
name := "iswbm"
fmt.Println(&name)
// output: 0xc000010200
}
程序运行不会报错,说明 name 这个变量是可寻址的。
但不能说 ``"iswbm"`` 这个字符串是可寻址的。
``"iswbm"`` 是字符串,字符串都是不可变的,是不可寻址的,后面会介绍到。
在开始逐个介绍之前,先说一下结论
- 指针可以寻址:\ ``&Profile{}``
- 变量可以寻址:\ ``name := Profile{}``
- 字面量通通不能寻址:\ ``Profile{}``
哪些是可以寻址的?
------------------
变量:\ ``&x``
~~~~~~~~~~~~~~
.. code:: go
func main() {
name := "iswbm"
fmt.Println(&name)
// output: 0xc000010200
}
指针:\ ``&*x``
~~~~~~~~~~~~~~~
.. code:: go
type Profile struct {
Name string
}
func main() {
fmt.Println(&Profile{})
// output: 0xc00000e028
}
数组元素索引: ``&a[0]``
~~~~~~~~~~~~~~~~~~~~~~~
.. code:: go
func main() {
s := [...]int{1,2,3}
fmt.Println(&s[0])
// output: xc0000b4010
}
切片
~~~~
.. code:: go
func main() {
fmt.Println([]int{1, 2, 3}[1:])
}
切片元素索引:\ ``&s[1]``
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: go
func main() {
s := make([]int , 2, 2)
fmt.Println(&s[0])
// output: xc0000b4010
}
组合字面量: ``&struct{ X int }{1}``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
所有的组合字面量都是不可寻址的,就像下面这样子
.. code:: go
type Profile struct {
Name string
}
func new() Profile {
return Profile{Name: "iswbm"}
}
func main() {
fmt.Println(&new())
// cannot take the address of new()
}
注意上面写法与这个写法的区别,下面这个写法代表不同意思,其中的 ``&``
并不是取地址的操作,而代表实例化一个结构体的指针。
.. code:: go
type Profile struct {
Name string
}
func main() {
fmt.Println(&Profile{Name: "iswbm"}) // ok
}
虽然组合字面量是不可寻址的,但却可以对组合字面量的字段属性进行寻址(直接访问)
.. code:: go
type Profile struct {
Name string
}
func new() Profile {
return Profile{Name: "iswbm"}
}
func main() {
fmt.Println(new().Name)
}
网上有博客说
如果一个结构体值是可寻址的,则它的字段也是可寻址的;反之,一个不可寻址的结构体值的字段也是不可寻址的。
不可寻址的字段的值是不可更改的。所有的组合字面量都是不可寻址的。
对于这种说法,我实在无法理解,结构体值(组合字面量)明明是不可寻址的,但为什么其字段可以寻址呢?
哪些是不可以寻址的?
--------------------
常量
~~~~
.. code:: go
import "fmt"
const VERSION = "1.0"
func main() {
fmt.Println(&VERSION)
}
字符串
~~~~~~
.. code:: go
func getStr() string {
return "iswbm"
}
func main() {
fmt.Println(&getStr())
// cannot take the address of getStr()
}
函数或方法
~~~~~~~~~~
.. code:: go
func getStr() string {
return "iswbm"
}
func main() {
fmt.Println(&getStr)
// cannot take the address of getStr
}
基本类型字面量
~~~~~~~~~~~~~~
字面量分:\ **基本类型字面量** **复合型字面量**\
基本类型字面量,是一个值的文本表示,都是不应该也是不可以被寻址的。
.. code:: go
func getInt() int {
return 1024
}
func main() {
fmt.Println(&getInt())
// cannot take the address of getInt()
}
map 中的元素
~~~~~~~~~~~~
字典比较特殊,可以从两个角度来反向推导,假设字典的元素是可寻址的,会出现
什么问题?
1. 如果字典的元素不存在,则返回零值,而零值是不可变对象,如果能寻址问题就大了。
2. 而如果字典的元素存在,考虑到 Go map
实现中元素的地址是变化的,这意味着寻址的结果也是无意义的。
基于这两点,Map 中的元素不可寻址,符合常理。
.. code:: go
func main() {
p := map[string]string {
"name": "iswbm",
}
fmt.Println(&p["name"])
// cannot take the address of p["name"]
}
搞懂了这点,你应该能够理解下面这段代码为什么会报错啦~
.. code:: go
package main
import "fmt"
type Person struct {
Name string
Email string
}
func main() {
m := map[int]Person{
1:Person{"Andy", "1137291867@qq.com"},
2:Person{"Tiny", "qishuai231@gmail.com"},
3:Person{"Jack", "qs_edu2009@163.com"},
}
//编译错误:cannot assign to struct field m[1].Name in map
m[1].Name = "Scrapup"
数组字面量
~~~~~~~~~~
数组字面量是不可寻址的,当你对数组字面量进行切片操作,其实就是寻找内部元素的地址,下面这段代码是会报错的
.. code:: go
func main() {
fmt.Println([3]int{1, 2, 3}[1:])
// invalid operation [3]int literal[1:] (slice of unaddressable value)
}
参考文章:
- https://gfw.go101.org/article/struct.html
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册