标签 优化 下的文章

Go 1.15中值得关注的几个变化

img{512x368}

Go 1.15版本在8月12日就正式发布了,给我的感觉就是发布的挺痛快^_^。这种感觉来自与之前版本发布时间的对比:Go 1.13版本发布于当年的9月4日,更早的Go 1.11版本发布于当年的8月25日。

不过这个时间恰与我家二宝出生和老婆月子时期有重叠,每天照顾孩子团团转的我实在抽不出时间研究Go 1.15的变化:(。如今,我逐渐从照顾二宝的工作中脱离出来^_^,于是“Go x.xx版本值得关注的几个变化”系列将继续下去。关注Go语言的演变对掌握和精通Go语言大有裨益,凡是致力于成为一名高级Gopher的读者都应该密切关注Go的演进。
截至写稿时,Go 1.15最新版是Go 1.15.2。Go 1.15一如既往的遵循Go1兼容性承诺语言规范方面没有任何变化。可以说这是一个“面子”上变化较小的一个版本,但“里子”的变化还是不少的,在本文中我就和各位读者一起就重要变化逐一了解一下。

一. 平台移植性

Go 1.15版本不再对darwin/386和darwin/arm两个32位平台提供支持了。Go 1.15及以后版本仅对darwin/amd64和darwin/arm64版本提供支持。并且不再对macOS 10.12版本之前的版本提供支持。

Go 1.14版本中,Go编译器在被传入-race和-msan的情况下,默认会执行-d=checkptr,即对unsafe.Pointer的使用进行合法性检查-d=checkptr主要检查两项内容:

  • 当将unsafe.Pointer转型为*T时,T的内存对齐系数不能高于原地址的;

  • 做完指针算术后,转换后的unsafe.Pointer仍应指向原先Go堆对象

但在Go 1.14中,这个检查并不适用于Windows操作系统。Go 1.15中增加了对windows系统的支持。

对于RISC-V架构,Go社区展现出十分积极的姿态,早在Go 1.11版本,Go就为RISC-V cpu架构预留了GOARCH值:riscv和riscv64。Go 1.14版本则为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。在Go 1.15版本中,Go在GOOS=linux, GOARCH=riscv64的环境下的稳定性和性能得到持续提升,并且已经可以支持goroutine异步抢占式调度了。

二. 工具链

1. GOPROXY新增以管道符为分隔符的代理列表值

Go 1.13版本中,GOPROXY支持设置为多个proxy的列表,多个proxy之间采用逗号分隔。Go工具链会按顺序尝试列表中的proxy以获取依赖包数据,但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时,go会终止数据获取。但是当列表中的proxy server返回其他错误时,Go命令不会向GOPROXY列表中的下一个值所代表的的proxy server发起请求,这种行为模式没能让所有gopher满意,很多Gopher认为Go工具链应该向后面的proxy server请求,直到所有proxy server都返回失败。Go 1.15版本满足了Go社区的需求,新增以管道符“|”为分隔符的代理列表值。如果GOPROXY配置的proxy server列表值以管道符分隔,则无论某个proxy server返回什么错误码,Go命令都会向列表中的下一个proxy server发起新的尝试请求。

注:Go 1.15版本中GOPROXY环境变量的默认值依旧为https://proxy.golang.org,direct

2. module cache的存储路径可设置

Go module机制自打在Go 1.11版本中以试验特性的方式引入时就将module的本地缓存默认放在了\$GOPATH/pkg/mod下(如果没有显式设置GOPATH,那么默认值将是~/go;如果GOPATH下面配置了多个路径,那么选择第一个路径),一直到Go 1.14版本,这个位置都是无法配置的。

Go module的引入为去除GOPATH提供了前提,于是module cache的位置也要尽量与GOPATH“脱钩”:Go 1.15提供了GOMODCACHE环境变量用于自定义module cache的存放位置。如果没有显式设置GOMODCACHE,那么module cache的默认存储路径依然是\$GOPATH/pkg/mod

三. 运行时、编译器和链接器

1. panic展现形式变化

在Go 1.15之前,如果传给panic的值是bool, complex64, complex128, float32, float64, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, uintptr等原生类型的值,那么panic在触发时会输出具体的值,比如:

// go1.15-examples/runtime/panic.go

package main

func foo() {
    var i uint32 = 17
    panic(i)
}

func main() {
    foo()
}

使用Go 1.14运行上述代码,得到如下结果:

$go run panic.go
panic: 17

goroutine 1 [running]:
main.foo(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:5
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:9 +0x39
exit status 2

Go 1.15版本亦是如此。但是对于派生于上述原生类型的自定义类型而言,Go 1.14只是输出变量地址:

// go1.15-examples/runtime/panic.go

package main

type myint uint32

func bar() {
    var i myint = 27
    panic(i)
}

func main() {
    bar()
}

使用Go 1.14运行上述代码:

$go run panic.go
panic: (main.myint) (0x105fca0,0xc00008e000)

goroutine 1 [running]:
main.bar(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39
exit status 2

Go 1.15针对此情况作了展示优化,即便是派生于这些原生类型的自定义类型变量,panic也可以输出其值。使用Go 1.15运行上述代码的结果如下:

$go run panic.go
panic: main.myint(27)

goroutine 1 [running]:
main.bar(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39
exit status 2

2. 将小整数([0,255])转换为interface类型值时将不会额外分配内存

Go 1.15在runtime/iface.go中做了一些优化改动:增加一个名为staticuint64s的数组,预先为[0,255]这256个数分配了内存。然后在convT16、convT32等运行时转换函数中判断要转换的整型值是否小于256(len(staticuint64s)),如果小于,则返回staticuint64s数组中对应的值的地址;否则调用mallocgc分配新内存。

$GOROOT/src/runtime/iface.go

// staticuint64s is used to avoid allocating in convTx for small integer values.
var staticuint64s = [...]uint64{
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
        0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
        0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
        0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
        0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,

        ... ...

        0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
        0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,

}

func convT16(val uint16) (x unsafe.Pointer) {
        if val < uint16(len(staticuint64s)) {
                x = unsafe.Pointer(&staticuint64s[val])
                if sys.BigEndian {
                        x = add(x, 6)
                }
        } else {
                x = mallocgc(2, uint16Type, false)
                *(*uint16)(x) = val
        }
        return
}

func convT32(val uint32) (x unsafe.Pointer) {
        if val < uint32(len(staticuint64s)) {
                x = unsafe.Pointer(&staticuint64s[val])
                if sys.BigEndian {
                        x = add(x, 4)
                }
        } else {
                x = mallocgc(4, uint32Type, false)
                *(*uint32)(x) = val
        }
        return
}

我们可以用下面例子来验证一下:

// go1.15-examples/runtime/tinyint2interface.go

package main

import (
    "math/rand"
)

func convertSmallInteger() interface{} {
    i := rand.Intn(256)
    var j interface{} = i
    return j
}

func main() {
    for i := 0; i < 100000000; i++ {
        convertSmallInteger()
    }
}

我们分别用go 1.14和go 1.15.2编译这个源文件(注意关闭内联等优化,否则很可能看不出效果):

// go 1.14

go build  -gcflags="-N -l" -o tinyint2interface-go14 tinyint2interface.go

// go 1.15.2

go build  -gcflags="-N -l" -o tinyint2interface-go15 tinyint2interface.go

我们使用下面命令输出程序执行时每次GC的信息:

$env GODEBUG=gctrace=1 ./tinyint2interface-go14
gc 1 @0.025s 0%: 0.009+0.18+0.021 ms clock, 0.079+0.079/0/0.20+0.17 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 2 @0.047s 0%: 0.003+0.14+0.013 ms clock, 0.031+0.099/0.064/0.037+0.10 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 3 @0.064s 0%: 0.008+0.20+0.016 ms clock, 0.071+0.071/0.018/0.081+0.13 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 4 @0.081s 0%: 0.005+0.14+0.013 ms clock, 0.047+0.059/0.023/0.032+0.10 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 5 @0.098s 0%: 0.005+0.10+0.017 ms clock, 0.042+0.073/0.027/0.080+0.13 ms cpu, 4->4->0 MB, 5 MB goal, 8 P

... ...

gc 192 @3.264s 0%: 0.003+0.11+0.013 ms clock, 0.024+0.060/0.005/0.035+0.11 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 193 @3.281s 0%: 0.005+0.13+0.032 ms clock, 0.042+0.075/0.041/0.050+0.25 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 194 @3.298s 0%: 0.004+0.12+0.013 ms clock, 0.033+0.072/0.030/0.033+0.10 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 195 @3.315s 0%: 0.003+0.17+0.023 ms clock, 0.029+0.062/0.055/0.024+0.18 ms cpu, 4->4->0 MB, 5 MB goal, 8 P

$env GODEBUG=gctrace=1 ./tinyint2interface-go15

我们看到和go 1.14编译的程序不断分配内存,不断导致GC相比,go1.15.2没有输出GC信息,间接证实了小整数转interface变量值时不会触发内存分配。

3. 加入更现代化的链接器(linker)

一个新版的现代化linker正在逐渐加入到Go中,Go 1.15是新版linker的起点。后续若干版本,linker优化会逐步加入进来。在Go 1.15中,对于大型项目,新链接器的性能要提高20%,内存占用减少30%。

4. objdump支持输出GNU汇编语法

go 1.15为objdump工具增加了-gnu选项,以在Go汇编的后面,辅助输出GNU汇编,便于对照

// go 1.14:

$go tool objdump -S tinyint2interface-go15|more
TEXT go.buildid(SB)

  0x1001000             ff20                    JMP 0(AX)
  0x1001002             476f                    OUTSD DS:0(SI), DX
  0x1001004             206275                  ANDB AH, 0x75(DX)
  0x1001007             696c642049443a20        IMULL $0x203a4449, 0x20(SP), BP
... ...

//go 1.15.2:

$go tool objdump  -S -gnu tinyint2interface-go15|more
TEXT go.buildid(SB)

  0x1001000             ff20                    JMP 0(AX)                            // jmpq *(%rax)           

  0x1001002             476f                    OUTSD DS:0(SI), DX                   // rex.RXB outsl %ds:(%rsi),(%dx)
  0x1001004             206275                  ANDB AH, 0x75(DX)                    // and %ah,0x75(%rdx)     

  0x1001007             696c642049443a20        IMULL $0x203a4449, 0x20(SP), BP      // imul $0x203a4449,0x20(%rsp,%riz,2),%ebp

... ...

四. 标准库

和以往发布的版本一样,标准库有大量小改动,这里挑出几个笔者感兴趣的和大家一起看一下。

1. 增加tzdata包

Go time包中很多方法依赖时区数据,但不是所有平台上都自带时区数据。Go time包会以下面顺序搜寻时区数据:

- ZONEINFO环境变量指示的路径中

- 在类Unix系统中一些常见的存放时区数据的路径(zoneinfo_unix.go中的zoneSources数组变量中存放这些常见路径):

    "/usr/share/zoneinfo/",
    "/usr/share/lib/zoneinfo/",
    "/usr/lib/locale/TZ/"

- 如果平台没有,则尝试使用$GOROOT/lib/time/zoneinfo.zip这个随着go发布包一起发布的时区数据。但在应用部署的环境中,很大可能不会进行go安装。

如果go应用找不到时区数据,那么go应用运行将会受到影响,就如下面这个例子:

// go1.15-examples/stdlib/tzdata.go

package main

import (
    "fmt"
    "time"
)

func main() {
    loc, err := time.LoadLocation("America/New_York")
    if err != nil {
        fmt.Println("LoadLocation error:", err)
        return
    }
    fmt.Println("LoadLocation is:", loc)
}

我们移除系统的时区数据(比如将/usr/share/zoneinfo改名)和Go安装包自带的zoneinfo.zip(改个名)后,在Go 1.15.2下运行该示例:

$ go run tzdata.go
LoadLocation error: unknown time zone America/New_York

为此,Go 1.15提供了一个将时区数据嵌入到Go应用二进制文件中的方法:导入time/tzdata包

// go1.15-examples/stdlib/tzdata.go

package main

import (
    "fmt"
    "time"
    _ "time/tzdata"
)

func main() {
    loc, err := time.LoadLocation("America/New_York")
    if err != nil {
        fmt.Println("LoadLocation error:", err)
        return
    }
    fmt.Println("LoadLocation is:", loc)
}

我们再用go 1.15.2运行一下上述导入tzdata包的例子:

$go run testtimezone.go
LoadLocation is: America/New_York

不过由于附带tzdata数据,应用二进制文件的size会增大大约800k,下面是在ubuntu下的实测值:

-rwxr-xr-x 1 root root 2.0M Oct 11 02:42 tzdata-withouttzdata*
-rwxr-xr-x 1 root root 2.8M Oct 11 02:42 tzdata-withtzdata*

2. 增加json解码限制

json包是日常使用最多的go标准库包之一,在Go 1.15中,go按照json规范的要求,为json的解码增加了一层限制:

// json规范要求

//https://tools.ietf.org/html/rfc7159#section-9

A JSON parser transforms a JSON text into another representation.  A
   JSON parser MUST accept all texts that conform to the JSON grammar.
   A JSON parser MAY accept non-JSON forms or extensions.

   An implementation may set limits on the size of texts that it
   accepts.  An implementation may set limits on the maximum depth of
   nesting.  An implementation may set limits on the range and precision
   of numbers.  An implementation may set limits on the length and
   character contents of strings.

这个限制就是增加了一个对json文本最大缩进深度值:

// $GOROOT/src/encoding/json/scanner.go

// This limits the max nesting depth to prevent stack overflow.
// This is permitted by https://tools.ietf.org/html/rfc7159#section-9
const maxNestingDepth = 10000

如果一旦传入的json文本数据缩进深度超过maxNestingDepth,那json包就会panic。当然,绝大多数情况下,我们是碰不到缩进10000层的超大json文本的。因此,该limit对于99.9999%的gopher都没啥影响。

3. reflect包

Go 1.15版本之前reflect包存在一处行为不一致的问题,我们看下面例子(例子来源于https://play.golang.org/p/Jnga2_6Rmdf):

// go1.15-examples/stdlib/reflect.go

package main

import "reflect"

type u struct{}

func (u) M() { println("M") }

type t struct {
    u
    u2 u
}

func call(v reflect.Value) {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string))
        }
    }()
    v.Method(0).Call(nil)
}

func main() {
    v := reflect.ValueOf(t{}) // v := t{}
    call(v)                   // v.M()
    call(v.Field(0))          // v.u.M()
    call(v.Field(1))          // v.u2.M()
}

我们使用Go 1.14版本运行该示例:

$go run reflect.go
M
M
reflect: reflect.flag.mustBeExported using value obtained using unexported field

我们看到同为类型t中的非导出字段(field)的u和u2(u是以嵌入类型方式称为类型t的字段的),通过reflect包可以调用字段u的导出方法(如输出中的第二行的M),却无法调用非导出字段u2的导出方法(如输出中的第三行的panic信息)。

这种不一致在Go 1.15版本中被修复,我们使用Go 1.15.2运行上述示例:

$go run reflect.go
M
reflect: reflect.Value.Call using value obtained using unexported field
reflect: reflect.Value.Call using value obtained using unexported field

我们看到reflect无法调用非导出字段u和u2的导出方法了。但是reflect依然可以通过提升到类型t的方法来间接使用u的导出方法,正如运行结果中的第一行输出。
这一改动可能会影响到遗留代码中使用reflect调用以类型嵌入形式存在的非导出字段方法的代码,如果你的代码中存在这样的问题,可以直接通过提升(promote)到包裹类型(如例子中的t)中的方法(如例子中的call(v))来替代之前的方式。

五. 小结

由于Go 1.15删除了一些GC元数据和一些无用的类型元数据,Go 1.15编译出的二进制文件size会减少5%左右。我用一个中等规模的go项目实测了一下:

-rwxr-xr-x   1 tonybai  staff    23M 10 10 16:54 yunxind*
-rwxr-xr-x   1 tonybai  staff    24M  9 30 11:20 yunxind-go14*

二进制文件size的确有变小,大约4%-5%。

如果你还没有升级到Go 1.15,那么现在正是时候

本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/go1.15-examples


我的Go技术专栏:“改善Go语⾔编程质量的50个有效实践”上线了,欢迎大家订阅学习!

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

记一次go panic问题的解决过程

一. Panic问题概述

本周收到客户在bugclose上填写的一个issue:添加一个下发通道后,pushd程序panic并退出了!程序panic时输出的stacktrace信息摘录如下:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x8ca449]

goroutine 266900 [running]:
pkg.tonybai.com/smspush/vendor/github.com/bigwhite/gocmpp.(*Client).Connect(0xc42040c7f0, 0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0x0, 0x0)
        /root/.go/src/pkg.tonybai.com/smspush/vendor/github.com/bigwhite/gocmpp/client.go:79 +0x239
pkg.tonybai.com/smspush/pkg/pushd/pusher.cmpp2Login(0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0xc4203d29c0, 0x11, 0x73)
        /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/cmpp2_handler.go:25 +0x9a
pkg.tonybai.com/smspush/pkg/pushd/pusher.newCMPP2Loop(0xc42071f800, 0x4, 0xaaecd8)
        /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/cmpp2_handler.go:65 +0x226
pkg.tonybai.com/smspush/pkg/pushd/pusher.(*tchanSession).Run(0xc42071f800, 0xaba7c3, 0x17)
        /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/session.go:52 +0x98
pkg.tonybai.com/smspush/pkg/pushd/pusher.(*gateway).addSession.func1(0xc4200881a0, 0xc42071f800, 0xc42040c700)
        /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/gateway.go:61 +0x11e
created by pkg.tonybai.com/smspush/pkg/pushd/pusher.(*gateway).addSession
        /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/gateway.go:58 +0x350

印象中近大半年用Go写的程序,遇到panic情况不多。上一次是因为原生map变量的并发访问导致的panic,那次panic一眼就看到问题所在了。但这次又是因为啥呢?

二. 分析和debug过程

这个问题在印象中似乎出现过,不过由于当初没有复现,客户环境中又没有panic信息提供,那时没能定位和解决,后来问题并没有出现,显然这个问题是有一定“随机属性”。

对于panic,我们首先检查直接导致panic发生的那一行代码:

        /root/.go/src/pkg.tonybai.com/smspush/vendor/github.com/bigwhite/gocmpp/client.go:79 +0x239

下面是client.go 79行周围的代码片段:

img{512x368}

也许是疏忽大意,当时瞅了一眼后,就断定这块没有问题(更多从业务协议层面考虑),这也直接导致后面绕了一个大圈子才查到”真凶”。如果您还没看出来问题,那继续往下看。

定式思维让我认为很可能是函数栈中的内存问题,于是我开始调查panic输出的函数调用栈中参数是否正确。

要想知道函数调用栈中参数传递是否有问题,先要知晓panic后输出的栈帧信息都是什么!比如下面panic dump信息中参数中的各种magic number都代表什么!

gocmpp.(*Client).Connect(0xc42040c7f0, 0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0x0, 0x0)

pusher.cmpp2Login(0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0xc4203d29c0, 0x11, 0x73)

pusher.newCMPP2Loop(0xc42071f800, 0x4, 0xaaecd8)

在Joe Shaw的《Understanding Go panic output》和William Kennedy的《Stack Traces In Go》中有针对Stack trace输出信息的解析。关于Stack trace输出信息的识别,总体遵循几个要点:

  • stack trace中每个函数/方法后面的“参数数值”个数与函数/方法原型的参数个数不是一一对应的;

  • stack trace中每个函数/方法后面的“参数数值”是按照函数/方法原型参数列表中从左到右的参数类型的内存布局逐一展开的; 每个数值占用一个word(64位平台下面为8字节)

  • 如果是method,则第一个参数是receiver自身。如果reciever是指针类型,则第一个参数数值就是一个指针地址;如果是非指针的实例,则stack trace会按照其内存布局输出;

  • 函数/方法返回值放在stack trace的“参数数值”列表的后面;如果有多个返回值,则同样按从左到右顺序,按照返回值类型的内存布局输出;

  • 指针类型参数:占用stack trace的“参数数值”列表的1个位置;数值表示指针值,也是指针指向的对象的地址;

  • string类型参数:由于string在内存中由两个字(word)表示,第一个字是数据指针,第二个字是string的长度,因此在stack trace的“参数数值”列表中将占用两个位置;

  • slice类型参数:由于slice类型在内存中由三个字表示,第一个word是数据指针,第二个word是len,第三个字是cap,因此在stack trace的“参数数值”列表中将占用三个位置;

  • 内建整型(int,rune,byte):由于按word逐个输出,对于类型长度不足一个Word的参数,会做合并处理;比如:一个函数有5个int16类型的参数,那么在stack trace的信息中,这5个参数将占用stack trace的“参数数值”列表中的两个位置;第一个位置是前4个参数的“合体”,第二个位置则是最后那个int16类型的参数值。

  • struct类型参数: 会按照struct中字段的内存布局顺序在stack trace中展开。

  • interface类型参数:由于interface类型在内存中由两部分组成,一部分是接口类型的参数指针,一部分是接口值的参数指针,因此interface类型参数将用stack trace的“参数数值”列表中的两个位置。

  • stack trace输出的信息是在函数调用过程中的“快照”信息,因此一些输出数值看似不合理,但是由于其并不是最终值,所以问题不一定发生在这些参数身上,比如:返回值参数。

结合上面要点、函数/方法原型以及stack trace的输出,我们来“定位”一下stack trace输出的各个“参数”的含义:

cmpp2Login和Connect的原型以及调用关系如下:

func cmpp2Login(dstAddr, user, password string, connectTimeout time.Duration) (*cmpp.Client, error)

func (cli *Client) Connect(servAddr, user, password string, timeout time.Duration) error

func cmpp2Login(dstAddr, user, password string, connectTimeout time.Duration) (*cmpp.Client, error) {
    c := cmpp.NewClient(cmpp.V21)
    return c, c.Connect(dstAddr, user, password, connectTimeout)
}

对照后,我们得出下面对应关系:

pusher.cmpp2Login(
        0xc4203d29c0,  // dstAddr的data pointer
        0x11,                  // dstAddr string的length
        0xc420423256,  // user 的data pointer
        0x6,                    // user string的length
        0xc420423260,  // password的data pointer
        0x8,                    // password string的length
        0x37e11d600,    // connectTimeout
        0xc4203d29c0,  // 返回值:Client的指针
        0x11,                 // 返回值:error接口的type pointer
        0x73)                 // 返回值:error接口的data pointer

gocmpp.(*Client).Connect(
        0xc42040c7f0,   //cli的指针
        0xc4203d29c0,  //servAddr string的data pointer
        0x11,                  //servAddr string的 length
        0xc420423256,  // user string的data pointer
        0x6,                    // user string的length
        0xc420423260,  // password的data pointer
        0x8,                    // password string的length
        0x37e11d600,   // timeout
        0x0,                   // 返回值:error接口的type pointer
        0x0)                   // 返回值:error接口的data pointer

在这里,cmpp2Login的dstAddr、user、password、connectTimeout这些输入参数值都非常正常;看起来不正常的两个返回值在栈帧中的值其实意义不大,因为connect没有返回,所以这些值处于“非最终态”;而Connect执行到第79行panic,因此其返回值error的两个值也是处于“中间状态”。

这样一来,似乎没有参数是错误的!

三. 回到起点,捉住“真凶”

在反复查看代码和对比stack trace的参数列表后,依然没有找到蛛丝马迹。遂决定平复心情,从头再来,回到起点!

        var ok bool
        var status uint8
        if cli.typ == V20 || cli.typ == V21 {
                var rsp *Cmpp2ConnRspPkt
                rsp, ok = p.(*Cmpp2ConnRspPkt)
                status = rsp.Status
        } else {
                var rsp *Cmpp3ConnRspPkt
                rsp, ok = p.(*Cmpp3ConnRspPkt)
                status = uint8(rsp.Status)   <------ 79行
        }

        if !ok {
                err = ErrRespNotMatch
                return err
        }

又反复看了这段代码!程序正常执行时都是经过这段代码的,都是正常的。为何随机爆出panic呢?79行如果要panic,显然是rsp为nil或其他非法地址。但rsp是由p进行type assertion而来的!难道是type assertion失败了!!!

从正常业务流程来看,这里是不会失败的!这也是当初这里没有立即检查ok这个bool值的原因。但是特殊情况下,也就是当tcp连接建立后,conn包发出后,对方未必返回是conn response包,很可能是其他包回来(比如active test),这样就会导致这块的type assertion失败!这也与这个问题随机发生的情况吻合!

而且当初保留了“ok”,而不是用”_”代替,说明设计思路中是存在返回的包不是conn response包的情况。看来是当初coding时逻辑混乱了:(

这就是问题所在了!教训:type assertion后一定要在检查ok这个bool值之后再决定是否使用assertion之后的变量

四. 其他

借着这个问题的解决过程,再多说一句 stacktrace。在Go 1.11及以后版本中,go compiler做了更深入的优化,很多“简单”的函数或方法会被自动inline(内联)了,函数一旦内联化了,那么在stack trace中我们就无法看到栈帧信息了,就会看到如下在栈帧信息中存在省略号的情况:

 $go run stacktrace.go
panic: panic in foo

goroutine 1 [running]:
main.(*Y).foo(...)
    /Users/tony/test/go/stacktrace/stacktrace2.go:32
main.main()
    /Users/tony/test/go/stacktrace/stacktrace.go:51 +0x39
exit status 2

可以使用-gcflags=”-l”来告诉编译器不要inline。至于是否要这么做,就要看debug和性能之间您是如何权衡的了。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats