Skip to content

Commit

Permalink
fix code format
Browse files Browse the repository at this point in the history
  • Loading branch information
bootun committed Jan 19, 2022
1 parent 2bcfbb9 commit 1df1643
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 57 deletions.
38 changes: 10 additions & 28 deletions articles/1-array.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
Go数组剖析
数组剖析
====

## 数组的概念
[数组(array)](https://go.dev/ref/spec#Array_types "Go Specification-Array types")**单一类型**元素的编号序列,元素的个数称为数组的长度,长度不能为负数。

## 数组的初始化方式
```Go
```go
// 显式指定数组大小,长度为3
var a = [3]int{1,2,3}

Expand All @@ -18,7 +18,7 @@ ab两种初始化方式在运行期间是等价的,只不过a的类型在编

## 数组的类型
数组的长度也是类型的一部分,也就是说,下面两个数组不是同一种类型
```Go
```go
a := [3]int{}
b := [4]int{}
a = b // 编译错误:cannot use b (type [4]int) as type [3]int in assignment
Expand All @@ -28,7 +28,7 @@ a = b // 编译错误:cannot use b (type [4]int) as type [3]int in assignment
## 数组的内存分配
数组在内存中是由一块连续的内存组成的。

```Go
```go
arr := [3]int{1, 2, 3}
fmt.Println(&arr[0]) // 0xc00001a240
fmt.Println(&arr[1]) // 0xc00001a248
Expand Down Expand Up @@ -62,7 +62,7 @@ MOVQ $3, 16(AX)
- **编译期间不能确定访问位置的情况**

假设有以下代码
```Go
```go
func foo() int{
arr := [3]int{1,2,3}
return arr[2]
Expand Down Expand Up @@ -99,7 +99,7 @@ Exit v27 (23)

## 数组的比较
如果数组的元素类型可以相互比较,那么数组也可以。比如两个**长度相等**的int数组可以进行比较,但两个长度相等的map数组就不可以比较,因为map之间不可以互相比较。
```Go
```go
var a, b [3]int
fmt.Println(a == b) // true

Expand All @@ -108,7 +108,7 @@ fmt.Println(c == d) // invalid operation: c == d (map can only be compared to ni
```
## 数组的传递
Go中所有的内容都是值传递[](https://go.dev/doc/faq#pass_by_value "Go中的值传递"),因此赋值/传参/返回数组等操作都会将整个数组进行复制。更好的方式是使用slice,这样就能避免复制大对象所带来的开销。
```Go
```go
func getArray() [3]int {
arr := [3]int{1,2,3}
fmt.Printf("%p\n",&arr) // 0xc00001a258
Expand All @@ -125,7 +125,7 @@ func main() {

## 编译时的数组
数组在编译时的节点表示为`ir.OTARRAY`,我们可以在类型检查阶段找到对该节点的处理:
```Go
```go
// typecheck1 should ONLY be called from typecheck.
func typecheck1(n ir.Node, top int) ir.Node {
...
Expand All @@ -139,7 +139,7 @@ func typecheck1(n ir.Node, top int) ir.Node {
}
```
我们将`tcArrayType`的关键节点放在下面:
```Go
```go
func tcArrayType(n *ir.ArrayType) ir.Node {
if n.Len == nil { // [...]T的形式
// 如果长度是...会直接返回,等到下一阶段进行处理
Expand All @@ -160,7 +160,7 @@ func tcArrayType(n *ir.ArrayType) ir.Node {
如果直接使用常数作为数组的长度,那么数组的类型在这里就确定好了。
如果使用`[...]T`+字面量这种形式,则会在`typecheck.tcCompLit`函数中确认元素的数量,并将其op更改为`ir.OARRAYLIT`以便于之后阶段使用

```Go
```go
func tcCompLit(n *ir.CompLitExpr) (res ir.Node) {
...
// Need to handle [...]T arrays specially.
Expand All @@ -181,21 +181,3 @@ func tcCompLit(n *ir.CompLitExpr) (res ir.Node) {
}
```
TODO:
如果我们是使用字面值初始化数组的,在`walk.walkCompLit`函数中会分析是否应该将其放置在静态区:
```go
// walkCompLit walks a composite literal node:
// OARRAYLIT, OSLICELIT, OMAPLIT, OSTRUCTLIT (all CompLitExpr), or OPTRLIT (AddrExpr).
func walkCompLit(n ir.Node, init *ir.Nodes) ir.Node {
if isStaticCompositeLiteral(n) && !ssagen.TypeOK(n.Type()) {
n := n.(*ir.CompLitExpr) // not OPTRLIT
// n can be directly represented in the read-only data section.
// Make direct reference to the static data. See issue 12841.
vstat := readonlystaticname(n.Type())
fixedlit(inInitFunction, initKindStatic, n, vstat, init)
return typecheck.Expr(vstat)
}
var_ := typecheck.Temp(n.Type())
nylit(n, var_, init)
return var_
}
```
34 changes: 17 additions & 17 deletions articles/2-slice.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Go slice剖析
切片剖析
===
[切片(slice)](https://go.dev/ref/spec#Slice_types "Go Specification Slice types")类似数组,都是一段相同类型元素在内存中的连续集合。和数组不同的是,切片的长度是可以动态改变的,而数组一旦确定了长度之后就无法再改变了。

Expand All @@ -8,15 +8,15 @@ Go slice剖析
- 使用关键字make()创建切片
- 使用[切片表达式](https://go.dev/ref/spec#Slice_expressions "Slice expressions")从切片或数组构造切片
- 使用字面值显式初始化切片
```Go
```go
a := make([]int,3,5) // 使用make关键字
b := a[1:3] // 使用切片表达式
c := []int{1,2,3} // 使用字面值显式初始化
```

#### 使用
切片的使用方式类似数组,都是通过下标访问对应的元素:
```Go
```go
// 使用字面值初始化切片
foo := []int{5,6,7,8,9}
f1 := foo[0] // f1 = 5
Expand All @@ -41,7 +41,7 @@ b3 = bar[2] // b3 = 9 #和上面的b3比较一下#
在回答这些问题之前,让我们先看看切片的结构。
## 切片的运行时结构
切片在运行时的表示为`reflect.SliceHeader`,其结构如下
```Go
```go
type SliceHeader struct {
Data uintptr // 底层数组的指针
Len int // 切片的长度
Expand All @@ -51,7 +51,7 @@ type SliceHeader struct {
我们可以发现,切片本身并没有“存储数据”的能力,它"肚子里“有一个指针,这个指针指向的才是真正存放数据的数组。当我们使用`make([]int,3,5)`来创建切片时,编译器会在底层创建一个长度为5的数组来作为这个切片存储数据的容器,并将切片的Data字段设为指向数组的指针。

我们可以使用如下方法来将一个切片转换为`reflect.SliceHeader`结构:
```Go
```go
// 初始化一个切片
slice := make([]int, 3, 5)
// 将切片的指针转化为unsafe.Pointer类型,再进一步将其转化为reflect.SliceHeader
Expand All @@ -63,7 +63,7 @@ fmt.Printf("Cap:%v\n",sliceHeader.Cap) // Cap:5
```

通过这种方法,我们可以很容易的观察使用切片表达式创建切片和原数组的关系:
```Go
```go
// 初始化一个数组
arr := [3]int{1, 2, 3}
// 使用切片表达式在数组arr上创建一个切片
Expand All @@ -80,7 +80,7 @@ fmt.Printf("Cap:%v\n", sliceHeader.Cap) // Cap:3
说了这么多,切片究竟长什么样子呢?

我们拿下面这段代码来做个示范:
```Go
```go
// 初始化一个长度为5的数组
arr := [5]int{5, 6, 7, 8, 9}
// 使用切片表达式在arr上构建一个切片
Expand All @@ -96,7 +96,7 @@ slice := arr[2:4]
所以`slice`的内容是数组`arr[2]``arr[4]`中间的这部分(从2开始但并不包括4)。

也就是说,`slice``Data`字段指针指向的是`arr[2]`的位置,**当前slice里有两个元素**(4 - 2 = 2),`slice[0]`的值是7,`slice[1]`的值是8。
```Go
```go
fmt.Println(slice[0]) // 7
fmt.Println(slice[1]) // 8
```
Expand All @@ -105,7 +105,7 @@ fmt.Println(slice[1]) // 8
因为`slice`的底层数组,也就是`arr`依然有额外的空间供`slice`使用,目前`slice`只包括了`7``8`两个元素,但如果需要添加新的元素进`slice`里,那么9这个元素的空间就可以供`slice`使用。

我们可以查看汇编验证一下是否正确
```Go
```go
...
// 初始化部分
LEAQ type.[5]int(SB), AX
Expand All @@ -128,7 +128,7 @@ MOVL $3, CX // Cap = 3
切片本身并不是一个动态数组,那么它是如何实现动态扩容的呢?

我们依然拿上面的例子讲解
```Go
```go
arr := [5]int{5, 6, 7, 8, 9}
slice := arr[2:4]
// 当前slice属性 Len:2,Cap:3
Expand All @@ -141,7 +141,7 @@ slice = append(slice,5) // 此时slice为[7,8,5]
![](slice/slice-2.png)

现在我们再次`append`一个元素看看会发生什么事情
```Go
```go
// 接前面
// 在底层数组没有空间的情况下再次追加元素
slice = append(slice,6)
Expand Down Expand Up @@ -172,11 +172,11 @@ GOSSAFUNC=main.grow go build -gcflags "-N -l" slice.go
![](slice/slice-4.png)

仔细观察我们发现,源代码第7行所对应的汇编指令中,有一行`CALL runtime.growslice`,这个函数定义在`runtime/slice.go`文件中,函数签名如下
```Go
```go
func growslice(et *_type, old slice, cap int) slice
```
该函数传入**旧的slice**和**期望的新容量**,返回**新的`slice`**,让我们看看函数的具体实现:
```Go
```go
func growslice(et *_type, old slice, cap int) slice {
// 省略了部分代码
newcap := old.cap
Expand Down Expand Up @@ -206,7 +206,7 @@ func growslice(et *_type, old slice, cap int) slice {
在本例中,我们向`growslice`函数传入的`cap``4`(因为旧的`slice`本身有3个元素,再次`append`1个元素,所以期望容量是`4`,为了印证这一点,我们可以观察对应的汇编代码,在`Go 1.17`之后的版本里,x86平台下函数调用使用寄存器来传递函数的参数,寄存器`AX`传递第一个参数,参数二的`runtime.slice`是个结构体,有三个字段,占用3个寄存器,所以`BX``CX``DI`寄存器是第二个参数,`SI`寄存器为`cap`,仔细看调用前的准备工作`MOVL $4, SI`,印证了我们之前说传入的`cap`为4的说法),`cap`传入4之后我们向下走,`4 < 2 x 3`,不满足 `cap > doublecap`,继续向下走,到`old.cap < 1024`时条件满足,所以新的容量就等于旧的容量翻倍,也就是`2 x 3 = 6`

**上述部分得到的结果并不是真正的新切片容量**,为了提高内存分配效率,减少内存碎片,会在这个newcap的基础上**向上取整**,取整的参考是一个数组,位置在`runtime/sizeclasses.go`中,数组内容如下:
```Go
```go
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, ..., 32768}
```
上面我们计算的`newcap`为6,每个`int`占用8字节,共计`6 x 8 = 48`字节,正好在这个数组中,不需要再向上取整,如果我们刚刚计算的`newcap`为5,`5 x 8 = 40`字节,需要向上对齐到`48`字节,于是最终的新切片大小就为`6`个元素而不是`5`个。
Expand All @@ -223,7 +223,7 @@ var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, ..., 32768

## 切片的传递
我们之前提到过,Go中的参数传递都是**值传递**[](https://go.dev/doc/faq#pass_by_value "pass by value")。但如果你有过一些Go语言的经验,可能会遇到下面这种情况:
```Go
```go
// foo 尝试把数组的第一个元素修改为9
func foo(arr [3]int) {
arr[0] = 9
Expand Down Expand Up @@ -261,7 +261,7 @@ func main() {
那是不是可以认为,我们在任意场景下都可以传递`slice`到别的函数?反正里面有指针,做的修改都能在函数外面“看到”呀?

那我们稍微修改一下上面代码中的`bar`函数:
```Go
```go
// bar 尝试在slice后追加元素
func bar(slice []int) {
slice = append(slice,4)
Expand All @@ -286,7 +286,7 @@ func main() {
## 切片的复制

切片使用内建函数`copy`进行复制,`copy`将元素从源切片(`src`)复制到目标切片(`dst`),返回复制的元素数。
```Go
```go
// copy 的函数签名
func copy(dst, src []Type) int

Expand Down
2 changes: 1 addition & 1 deletion articles/3-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Map剖析

## 编译时的map
使用字面值初始化map时,map编译时节点的op类型为`ir.OMAPLIT`
```Go
```go
func maplit(n *ir.CompLitExpr, m ir.Node, init *ir.Nodes) {

// make the map var
Expand Down
22 changes: 11 additions & 11 deletions articles/appendix/1-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
`go version go1.17.5 linux/amd64`
# 查看Go内建函数的实现
初学Go时,你是否好奇过make函数的实现
```Go
```go
// make slice
slice := make([]int,20)
// make channel
channel := make(chan int,3)
```
这个神奇的函数能用来创建好多东西,但当你在IDE里点进去后,却发现它只有一行函数声明:
```Go
```go
func make(t Type, size ...IntegerType) Type
```
于是你的探索就止步于此了...
Expand All @@ -30,7 +30,7 @@ func make(t Type, size ...IntegerType) Type
### 方法1:使用汇编
**场景代码**

```Go
```go
// foo.go
func foo()[]int {
slice := make([]int, 3)
Expand Down Expand Up @@ -68,7 +68,7 @@ $ go tool objdump -s "foo" foo.o
### 方法2:查看SSA
**场景代码**

```Go
```go
// main.go
func foo() (string, bool) {
// 大小尽量大一些,否则如果map分配到栈上,后面的内容可能对不上
Expand All @@ -94,7 +94,7 @@ $ GOSSAFUNC=foo go build main.go

### 方法3:跟踪Go编译器源码
**场景代码**
```Go
```go
func foo() ([]int,[]int) {
s1 := make([]int,10)
s2 := []int{1,2,3,4,5}
Expand All @@ -107,12 +107,12 @@ func foo() ([]int,[]int) {
Go编译器源码的位置在`$GOROOT/src/cmd/compile/internal/gc`目录下(此处的gc指Go compiler),入口函数是`/cmd/compile/internal/gc/main.go`中的`Main`函数,篇幅原因,这里我们只介绍上述代码所经过的几个关键节点,如果想详细了解Go编译过程,可以参考[《Go语言设计与实现》](https://draveness.me/golang/ "《Go语言设计与实现》")一书。

语法分析和类型检查的入口在上述`Main`函数的这一行:
```Go
```go
// Parse and typecheck input.
noder.LoadPackage(flag.Args())
```
这个函数内可以看到语法分析和类型检查的不同阶段:
```Go
```go
// Phase 1: const, type, and names and types of funcs.
// This will gather all the information about types
// and methods but doesn't depend on any of it.
Expand All @@ -136,13 +136,13 @@ for i := 0; i < len(typecheck.Target.Decls); i++ {
`typecheck.Target.Decls[i] = typecheck.Stmt(n)`这行代码,这里是进入类型检查的入口,该函数其实是`cmd/compile/internal/typecheck/typecheck.go``typecheck`函数的包装函数,`typecheck`函数又会调用`typecheck1`函数,我们的要找的第一部分内容就在这里啦。

`typecheck1`函数下有这样一处地方:
```Go
```go
case ir.OMAKE:
n := n.(*ir.CallExpr)
return tcMake(n)
```
表示当前处理的节点是`OMAKE`时的情景,让我们跟踪`tcMake`函数,看看里面都做了些什么:
```Go
```go
// tcMake typechecks an OMAKE node.
func tcMake(n *ir.CallExpr) ir.Node {
...
Expand All @@ -163,7 +163,7 @@ switch t.Kind() {
后面追踪起起来函数太多了,这里只放最后两个,第一个是处理op为`OMAKESLICE`时的场景:
```Go
```go
func walkExpr1(n ir.Node, init *ir.Nodes) ir.Node {
...
switch n.Op() {
Expand All @@ -176,7 +176,7 @@ func walkExpr1(n ir.Node, init *ir.Nodes) ir.Node {
```
第二个就是上面函数调用的`walkMakeSlice`函数:
```Go 最终结果
```go 最终结果
// walkMakeSlice walks an OMAKESLICE node.
func walkMakeSlice(n *ir.MakeExpr, init *ir.Nodes) ir.Node {
...
Expand Down

0 comments on commit 1df1643

Please sign in to comment.