分类 GO 下的文章

golang 疑问1:defer 调用之后到底发生了什么

先来看一个个函数,并猜猜它们的返回值

func f1() (result int) {
    defer func() {
        result *= 7
    }()
    return 6
}

尝试打印这个函数的返回值看看

fmt.Println(f1())
//stdout:42 

Why ‘42’ ???


‘42’ 的前世今生

首先,我们先用最简单的方式:单步调试看一下返回值的变化情况。

3f0076f6334f2d77907291a3c4dfa4d9.png
figure-1

55d020405d2e5102f15222240404124a.png
figure-2

ebbfa320125006bab01e2c7cbf0fe2e4.png
figure-3

69b503f7f002a832d18ac4ca51d55fc6.png
figure-4

通过上面的简单截图,我们可以看到:

  1. 返回值result初始值为 0
  2. 程序先在 figure-2 的时候,通过语句 return 6 将返回值修改为 6
  3. 然后在 figure-3的时候开始执行 defer 闭包
  4. 在 defer 执行完之后,返回值被修改成了42

通过上面简单分析,我们可以看到是 defer 最终修改了返回值,但是要想知道修改的机制,就要看下汇编了。

汇编分析

首先通过命令 GOOS=linux GOARCH=amd64 go tool compile -l -S main.go 获得如下汇编代码(注释是我自己加的):

"".f1 STEXT size=124 args=0x8 locals=0x20
    0x0000 00000 (main.go:7)    TEXT    "".f1(SB), ABIInternal, $32-8 // 生命函数即将分配的 栈帧32字节 参数8字节
    0x0000 00000 (main.go:7)    MOVQ    (TLS), CX // 这三行是检测栈空间是否足够,不够的话会跳转到runtime.morestack_noctxt(SB) 申请更多空间
    0x0009 00009 (main.go:7)    CMPQ    SP, 16(CX)
    0x000d 00013 (main.go:7)    JLS    117
    0x000f 00015 (main.go:7)    SUBQ    $32, SP // 申请32字节栈空间
    0x0013 00019 (main.go:7)    MOVQ    BP, 24(SP) // 保存 caller BP ,供函数执行完成之后恢复 caller 使用
    0x0018 00024 (main.go:7)    LEAQ    24(SP), BP // 将f1() 的栈低地址传入BP寄存器
    0x001d 00029 (main.go:7)    MOVQ    $0, "".result+40(SP) //赋初始值0给 返回参数 result
    0x0026 00038 (main.go:8)    MOVL    $8, (SP) // 将 deferproc 第一个参数压栈,runtime.panic.go:92 deferproc(siz int32, fn *funcval)
    0x002d 00045 (main.go:8)    LEAQ    "".f1.func1·f(SB), AX //保存匿名函数 AX
    0x0034 00052 (main.go:8)    MOVQ    AX, 8(SP) // 压栈
    0x0039 00057 (main.go:8)    LEAQ    "".result+40(SP), AX // 保存返回值地址 AX
    0x003e 00062 (main.go:8)    MOVQ    AX, 16(SP) //压栈, 和下方的deferproc 没太多关系,deferproc 只接受2个参数
    0x0043 00067 (main.go:8)    CALL    runtime.deferproc(SB) //调用 deferproc
    0x0048 00072 (main.go:8)    TESTL    AX, AX
    0x004a 00074 (main.go:8)    JNE    101
    0x004c 00076 (main.go:11)    MOVQ    $6, "".result+40(SP) //将数字6传入 返回参数 result
    0x0055 00085 (main.go:11)    XCHGL    AX, AX // 据说是只是为了填充指令 nop
    0x0056 00086 (main.go:11)    CALL    runtime.deferreturn(SB) //执行匿名函数, 在 runtime.panic.go:351 中,getcallersp() 获取了当前函数栈,
    0x005b 00091 (main.go:11)    MOVQ    24(SP), BP //恢复BP寄存器
    0x0060 00096 (main.go:11)    ADDQ    $32, SP // 释放32字节栈空间
    0x0064 00100 (main.go:11)    RET
    0x0065 00101 (main.go:8)    XCHGL    AX, AX
    0x0066 00102 (main.go:8)    CALL    runtime.deferreturn(SB)
    0x006b 00107 (main.go:8)    MOVQ    24(SP), BP
    0x0070 00112 (main.go:8)    ADDQ    $32, SP
    0x0074 00116 (main.go:8)    RET
    0x0075 00117 (main.go:8)    NOP
    0x0075 00117 (main.go:7)    CALL    runtime.morestack_noctxt(SB)
    0x007a 00122 (main.go:7)    JMP    0

"".f1.func1 STEXT nosplit size=20 args=0x8 locals=0x0 // 这里的栈结构要看 runtime.panic.go:345 deferreturn(arg0 uintptr)
    0x0000 00000 (main.go:8)    TEXT    "".f1.func1(SB), NOSPLIT|ABIInternal, $0-8
    0x0000 00000 (main.go:9)    MOVQ    "".&result+8(SP), AX // result 指针放入AX
    0x0005 00005 (main.go:9)    MOVQ    (AX), CX // AX指向的值放入 CX
    0x0008 00008 (main.go:9)    LEAQ    (CX)(CX*2), DX //DX = 2*CX + CX  -> 2*result + result = 3*result
    0x000c 00012 (main.go:9)    LEAQ    (CX)(DX*2), CX //CX = 2*DX + CX  -> 2*(3*result) + result = 7*result
    0x0010 00016 (main.go:9)    MOVQ    CX, (AX) // 将AX指向的值更新为 7*result
    0x0013 00019 (main.go:10)    RET

--未完待续--

参考连接

https://github.com/go-internals-cn/go-internals/blob/master/chapter1_assembly_primer/README.md
http://xargin.com/plan9-assembly/
https://github.com/golang/go/files/447163/GoFunctionsInAssembly.pdf
https://github.com/chai2010/advanced-go-programming-book/tree/master/ch3-asm
http://mcll.top/2018/04/15/golang%E4%B8%AD%E7%9A%84defer%E5%AE%9E%E7%8E%B0/

如果计算机体系不太熟,建议先看看这个

https://www.bilibili.com/video/av31289365/?p=6
http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html

golang中的rune

rune这个概念困扰我好久了。。。中文直译字面量给我的感觉就是犇猋骉麤

今天在看The way to go的时候,看到类型判断,然后尝试了下

fmt.Println(1,"a",'a')
//output:1 a 97

看到97的时候就开心了一下,a的unicode编码就是97字面量果然就是字面量...,stackoverflow有个解释很棒:字面量就是它所代表的unicode编码值,a就代表整数97

参考:unicode码表 //16进制

Golang 中的slice

PHP写久了,对各种数据类型就不太在意了。一开始对Golang中slice类型的理解就是一个简单的array,今天在刷leetcode的时候碰到一个很有意思的题目Remove Duplicates from Sorted Array 题目描述如下:

Given a sorted array, remove the duplicates in place such that each element appear only once and return the new length.

Do not allocate extra space for another array, you must do this in place with constant memory.

For example,
Given input array nums = [1,1,2],

Your function should return length = 2, with the first two elements of nums being 1 and 2 respectively. It doesn't matter what you leave beyond the new length.

有意思的是 :Do not allocate extra space for another array, you must do this in place with constant memory.

大体的想法是写一个删除slice中元素的方法,然后再循环找到重复的值,删除之。

实现删除方法的时候碰到了很有意思的问题:Golang 中并没有append()的对应方法,没有减少slice长度的内建方法。想要减小slice的长度,只有对slice重新切片:s = s[x:y]。问题就是,重新切片有没有使用新的空间?

带着疑问查找了一些资料,其中Go博客中有一篇 Go 切片:用法和本质 讲解的非常透彻。其中提到了:

一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。
go-slices-usage-and-internals_slice-struct
长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)。

s=s[2:4] 对切片重新切片
go-slices-usage-and-internals_slice-2
切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e []byte{'a', 'd'}
e[1] = 'm'
// e
[]byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

既然是一个指针,那就放心大胆的用了

func deleteNode(nums []int,index int) []int{
    nums=append(nums[:index],nums[index+1]...)
    return nums
}

搞定!

Go fmt.Printf()

全文Copy 自 Go语言中文网

package main
import "fmt"
import "os"
type point struct {
    x, y int
}
func main() {
//Go 为常规 Go 值的格式化设计提供了多种打印方式。例如,这里打印了 point 结构体的一个实例。
    p := point{1, 2}
    fmt.Printf("%v\n", p) // {1 2}
//如果值是一个结构体,%+v 的格式化输出内容将包括结构体的字段名。
    fmt.Printf("%+v\n", p) // {x:1 y:2}
//%#v 形式则输出这个值的 Go 语法表示。例如,值的运行源代码片段。
    fmt.Printf("%#v\n", p) // main.point{x:1, y:2}
//需要打印值的类型,使用 %T。
    fmt.Printf("%T\n", p) // main.point
//格式化布尔值是简单的。
    fmt.Printf("%t\n", true)
//格式化整形数有多种方式,使用 %d进行标准的十进制格式化。
    fmt.Printf("%d\n", 123)
//这个输出二进制表示形式。
    fmt.Printf("%b\n", 14)
//这个输出给定整数的对应字符。
    fmt.Printf("%c\n", 33)
//%x 提供十六进制编码。
    fmt.Printf("%x\n", 456)
//对于浮点型同样有很多的格式化选项。使用 %f 进行最基本的十进制格式化。
    fmt.Printf("%f\n", 78.9)
//%e 和 %E 将浮点型格式化为(稍微有一点不同的)科学技科学记数法表示形式。
    fmt.Printf("%e\n", 123400000.0)
    fmt.Printf("%E\n", 123400000.0)
//使用 %s 进行基本的字符串输出。
    fmt.Printf("%s\n", "\"string\"")
//像 Go 源代码中那样带有双引号的输出,使用 %q。
    fmt.Printf("%q\n", "\"string\"")
//和上面的整形数一样,%x 输出使用 base-16 编码的字符串,每个字节使用 2 个字符表示。
    fmt.Printf("%x\n", "hex this")
//要输出一个指针的值,使用 %p。
    fmt.Printf("%p\n", &p)
//当输出数字的时候,你将经常想要控制输出结果的宽度和精度,可以使用在 % 后面使用数字来控制输出宽度。默认结果使用右对齐并且通过空格来填充空白部分。
    fmt.Printf("|%6d|%6d|\n", 12, 345)
//你也可以指定浮点型的输出宽度,同时也可以通过 宽度.精度 的语法来指定输出的精度。
    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
//要最对齐,使用 - 标志。
    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
//你也许也想控制字符串输出时的宽度,特别是要确保他们在类表格输出时的对齐。这是基本的右对齐宽度表示。
    fmt.Printf("|%6s|%6s|\n", "foo", "b")
//要左对齐,和数字一样,使用 - 标志。
    fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
//到目前为止,我们已经看过 Printf了,它通过 os.Stdout输出格式化的字符串。Sprintf 则格式化并返回一个字符串而不带任何输出。
    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s)
//你可以使用 Fprintf 来格式化并输出到 io.Writers而不是 os.Stdout。
    fmt.Fprintf(os.Stderr, "an %s\n", "error")
}

学习goroutine

goroutine

goroutine 协程,可以看成一个轻量级的线程。与线程相比,协程的调度方是自己,而线程则是操作系统。go 中select 一直监视着IO变化

package main

import (
    "fmt"
    "time"
)

func Producer (queue chan<- int){
    for i:=0;i<100;i++{
        queue <- i
    }

}

func Consumer( queue <-chan int){
    for {
        v := <- queue
        fmt.Println("receive:", v)
    }
}

func main(){
    queue := make(chan int)
    go Producer(queue)
    time.Sleep(1e9)
    go Consumer(queue)
    go Consumer(queue)
    go Consumer(queue)
    go Consumer(queue)
    time.Sleep(1e9) //让Producer与Consumer完成
}

等价二叉树

package main

import "golang.org/x/tour/tree"
import "fmt"

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}
// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int){
    if t.Left !=nil{
         Walk(t.Left,ch)
    }
    ch<-t.Value
    if t.Right !=nil{
         Walk(t.Right,ch)
    }
    
}

// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool{
    var ret bool
    
    ch1 :=make(chan int,10)
    ch2 :=make(chan int,10)    
    go Walk(t1,ch1)
    go Walk(t2,ch2)
    
    for  i:=0;i<10;i++{
        if  <-ch1 != <-ch2{
            ret= false
            break
        }else{
            ret= true
        }
    }
    return ret
}

func main() {
    fmt.Println(Same(tree.New(1), tree.New(2)))
}

go channel实现
深入学习go --channel