Go 1.17新特性详解:使用基于寄存器的调用惯例

本文永久链接 – https://tonybai.com/2021/08/20/using-register-based-calling-convention-in-go-1-17

除了Go语言特性go module有重要变化之外,Go编译器与Go运行时也都有着优化与改进,这两方面的变化对Go程序的构建与运行影响巨大。在这个系列的最后一篇中,我们来看看编译器与运行时中那些值得关注的变化。

1. 使用基于寄存器的调用惯例替代基于堆栈的调用惯例

所谓“调用惯例(calling convention)”是调用方和被调用方对于函数调用的一个明确的约定,包括:函数参数与返回值的传递方式、传递顺序。只有双方都遵守同样的约定,函数才能被正确地调用和执行。如果不遵守这个约定,函数将无法正确执行。

Go 1.17版本之前,Go采用基于栈的调用约定,即函数的参数与返回值都通过栈来传递,这种方式的优点是实现简单,不用担心底层cpu架构寄存器的差异,适合跨平台;但缺点就是牺牲了一些性能,我们都知道寄存器的访问速度要远高于内存。

大多数平台上的大多数语言实现都使用基于寄存器的调用约定,通过寄存器而不是内存传递函数参数和返回结果,并指定一些寄存器为调用保存寄存器,允许函数在不同的调用中保持状态。

于是Go在1.17版本决定向这些语言看齐,在amd64架构下率先实现了从基于堆栈的调用惯例到基于寄存器的调用惯例切换

在Go 1.17的版本发布说明文档中有提到:切换到基于寄存器的调用惯例后,一组有代表性的Go包和程序的基准测试显示,Go程序的运行性能提高了约5%,二进制文件大小典型减少约2%。

我们来实测一下,下面采用的是之前进阶专栏中的一个多种方法进行字符串连接的benchmark测试,在Go 1.16.5和Go 1.17下面分别运行Benchmark结果如下:

Go 1.16.5:

$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/demo
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkConcatStringByOperator-8                       12132355            91.51 ns/op
BenchmarkConcatStringBySprintf-8                         2707862           445.1 ns/op
BenchmarkConcatStringByJoin-8                           24101215            50.84 ns/op
BenchmarkConcatStringByStringsBuilder-8                 11104750           124.4 ns/op
BenchmarkConcatStringByStringsBuilderWithInitSize-8     24542085            48.24 ns/op
BenchmarkConcatStringByBytesBuffer-8                    14425054            77.73 ns/op
BenchmarkConcatStringByBytesBufferWithInitSize-8        20863174            49.07 ns/op
PASS
ok      github.com/bigwhite/demo    9.166s

Go 1.17:

$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/demo
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
BenchmarkConcatStringByOperator-8                       13058850            89.47 ns/op
BenchmarkConcatStringBySprintf-8                         2889898           410.1 ns/op
BenchmarkConcatStringByJoin-8                           25469310            47.15 ns/op
BenchmarkConcatStringByStringsBuilder-8                 13064298            92.33 ns/op
BenchmarkConcatStringByStringsBuilderWithInitSize-8     29780911            41.14 ns/op
BenchmarkConcatStringByBytesBuffer-8                    16900072            70.28 ns/op
BenchmarkConcatStringByBytesBufferWithInitSize-8        27310650            43.96 ns/op
PASS
ok      github.com/bigwhite/demo    9.198s

我们看到,相对于Go 1.16.5跑出的结果,Go 1.17在每一个测试项上都有小幅的性能提升,有些性能提升甚至达到10%左右。这种新版本带来的性能的“自然提升”显然是广大Gopher想看到的。

我们再来看看编译后的Go二进制文件的Size变化。以一个自有的1w行左右代码的Go程序为例,分别用Go 1.16.5和Go 1.17进行编译,得到的结果如下:

-rwxr-xr-x   1 tonybai  staff  7264432  8 13 18:31 myapp-go1.16.5*
-rwxr-xr-x   1 tonybai  staff  6934352  8 13 18:32 myapp-go1.17*

我们看到Go 1.17编译后的二进制文件大小相较于Go 1.16.5版本的减少了约4%。

另外Go 1.17发布说明也提到了:改为基于register的调用惯例后,绝大多数程序不会受到影响。只有那些之前就已经违反unsafe.Pointer的使用规则的代码可能会受到影响,比如不遵守unsafe规则通过unsafe.Pointer访问函数参数,或依赖一些像比较函数代码指针的未公开的行为。

除了改为基于寄存器的调用惯例之外,Go 1.17编译器还支持包含闭包的函数的内联(inline)了!这样一来,一个带有闭包的函数可能会在函数被内联的每个地方产生一个不同的闭包代码指针,因此,Go函数的值不能直接比较

2. 引入//go:build形式的构建约束指示符,以替代原先易错的// +build形式

Go 1.17之前,我们可以通过在源码文件头部放置+build构建约束指示符来实现构建约束,但这种形式十分易错,并且它并不支持&&和||这样的直观的逻辑操作符,而是用逗号、空格替代,下面是原+build形式构建约束指示符的用法及含义:

这种与程序员直觉“有悖”的形式让Gopher们十分痛苦,于是Go 1.17回归“正规”,引入了//go:build形式的构建约束指示符,这样一方面是与源文件中的其他指示符保持形式一致,比如: //go:nosplit、//go:norace、//go:noinline、//go:generate等。另外一方面,新形式将支持&&和||逻辑操作符,对于程序员来说,这样的形式就是自解释的,我们无需再像上面那样列出一个表来解释每个指示符组合的含义了,如下代码所示:

//go:build linux && (386 || amd64 || arm || arm64 || mips64 || mips64le || ppc64 || ppc64le)
//go:build linux && (mips64 || mips64le)
//go:build linux && (ppc64 || ppc64le)
//go:build linux && !386 && !arm

考虑到兼容性,Go命令可以识别这两种形式的构建约束指示符,但推荐Go 1.17之后都用新引入的这种形式。

gofmt可以兼容处理两种形式,处理原则是:如果一个源码文件只有// +build形式的指示符,gofmt会将与其等价的//go:build行加入。否则,如果一个源文件中同时存在这两种形式的指示符行,那么//+build行的信息将被//go:build行的信息所覆盖。

go vet工具也会检测源文件中同时存在的不同形式的构建指示符语义不一致的情况,比如针对下面这段代码:

// github.com/bigwhite/experiments/tree/master/go1.17-examples/runtime/buildtag.go

//go:build linux && !386 && !arm
// +build linux

package main

import "fmt"

func main() {
    fmt.Println("hello, world")
}

go vet会提示如下问题:

./buildtag.go:2:1: +build lines do not match //go:build condition

3. 运行时栈跟踪输出信息的格式更“可读”

之前写过一篇文章《记一次go panic问题的解决过程》,在那篇文章中,我们探讨了如何解读panic发生后输出的函数栈跟踪信息。

下面的代码示例用于对比运行时栈输出信息的差异:

// github.com/bigwhite/experiments/tree/master/go1.17-examples/runtime/stacktrace.go

package main

type myStruct struct {
    m int
    s string
    p *float64
}

func foo(a int, b string, c []byte, f *myStruct) (int, error) {
    panic("mypanic")
}

func main() {
    f := 3.14
    ms := myStruct{
        m: 17,
        s: "myStruct",
        p: &f,
    }
    a := 11
    b := "hello"
    c := []byte{'a', 'b', 'c'}
    foo(a, b, c, &ms)
}

在这个示例程序中,我们在foo函数中“故意”panic,以便go运行时在程序退出前输出栈跟踪信息(注意编译时关闭内联优化)。针对这个示例程序,Go 1.17之前的版本输出的栈跟踪信息是这样的(go 1.16.5版本):

$go build -gcflags '-N -l' -o stacktrace-go1.16.5 stacktrace.go
$./stacktrace-go1.16.5
panic: mypanic

goroutine 1 [running]:
main.foo(0xb, 0x1073f53, 0x5, 0xc000046715, 0x3, 0x3, 0xc000046758, 0x0, 0x0, 0x0)
    /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:10 +0x4a
main.main()
    /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:23 +0x148

上面输出信息中foo函数后面括号中的各个值与foo函数原型完全对不上。要想知道这些数值的含义究竟是什么,可以参考我上面提到的那篇文章,这里不赘述。

使用Go 1.17版本编译后会是什么样子呢?我们再来看一下:

go 1.17:

$go build -gcflags '-N -l' -o stacktrace-go1.17 stacktrace.go
$./stacktrace
panic: mypanic

goroutine 1 [running]:
main.foo(0xb, {0x10608d4, 0x5}, {0xc00004270d, 0x3, 0x3}, 0xc000042750)
    /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:10 +0x59
main.main()
    /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:23 +0x10f

对照着该示例程序中foo函数的原型:

func foo(a int, b string, c []byte, f *myStruct) (int, error)

这回一目了然了!我们看到Go 1.17改进了当发送未捕获的panic或当runtime.Stack被调动时,运行时输出的栈跟踪信息的格式。Go 1.17版本之前,函数参数被打印成基于内存布局的十六进制值的形式,就像前面那个难于解读的输出信息。Go 1.17版,源码中函数的每个参数都被单独打印,用逗号分隔。聚合类型(结构体、数组、字符串、切片、接口和complex)的参数用大括号分隔。需要注意的是,只存在于寄存器中而没有存储到内存中的参数的值可能是不准确的。函数的返回值(通常是不准确的)不再被打印了。

通过上的输出,我们还可以清晰的看到stringbyte切片以及结构体在内存中的表示方式,string本质上是一个拥有两个字段的结构,而切片则是一个三元组表示的结构。

3. 小结

上面是Go 1.17编译器与运行时的主要改动,通过使用寄存器的调用惯例,我们的Go程序可以轻松获得5%左右的性能提升,可执行程序的Size也会得到减小。Go 1.17对运行时栈输出信息的“可读化”改进进一步提升了开发体验。

除此之外,Go的标准库随着新版本的发布都会有大量的改动,但每个开发人员对标准库的关注点差别很大,因此,在这个系列中不会详细做说明了,大家还是参考Go 1.17的发布说明文档各取所需吧^_^。

本文所涉及的源码可以在这里 – https://github.com/bigwhite/experiments/tree/master/go1.17-examples/


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://51smspush.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
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

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

Go 1.17新特性详解:module依赖图修剪与延迟module加载

本文永久链接 – https://tonybai.com/2021/08/19/go-module-changes-in-go-1-17

Go module的引入终于让Go语言有了自己的包依赖管理标准机制与工具,虽说它的引入与推广过程略显坎坷,但不得不承认Go 1.11及之后的每一次Go版本发布,Go module都在进步!在Go 1.17版本中亦是如此,本篇我们就来详细聊聊在Go 1.17版本中Go module都有哪些重要的变化。

1. module依赖图修剪

本文的标题暗示了Go 1.17中go module的两个主要变化。module依赖图修剪(module graph pruning)延迟module加载(lazy module loading)的基础。

我们以下图中的例子来解释一下什么是module graph pruning。

注:上图中的例子来自于Go 1.17源码中的src/cmd/go/testdata/script/mod_lazy_new_import.txt,通过执行txtar工具,我们可以将该txt转换为mod_lazy_new_import.txt中所描述的示例结构,转换命令为: txtar -x < \$GOROOT/src/cmd/go/testdata/script/mod_lazy_new_import.txt 。

在上面这个示例中,main module中的lazy.go导入了module a的package x,后者则导入了module b;并且module a还有一个package y,该包导入了module c。通过go mod graph命令我们可以得到main module的module graph,如图右上。

现在问题来了!package y是因为自身是module a的一部分而被main module所依赖,它没有为main module的构建做出任何“代码级贡献”;同理,package y所依赖的module c亦是如此。但是在Go 1.17之前的版本中,如果Go编译器找不到module c,那么main module的构建将会失败,这会让开发者们觉得不够合理!

我们回到上图对应的示例。在Go 1.16.5下,我们看看该示例的go.mod如下:

// github.com/bigwhite/experiments/tree/master/go1.17-examples/module/demo1/go.mod
module example.com/lazy

go 1.15

require example.com/a v0.1.0

replace (
    example.com/a v0.1.0 => ./a
    example.com/b v0.1.0 => ./b
    example.com/c v0.1.0 => ./c1
    example.com/c v0.2.0 => ./c2
)

我们只需关注require块中的内容,下面的replace块儿主要是为了示例能找到各种依赖module而设置的。我们知道在Go 1.16及以前支持Go module的版本所建立的go module中,其中的go.mod在go mod tidy后,require块儿中保留的都是main module直接依赖(direct dependency)在某些情况下,也会记录indirect依赖,这些依赖会在行尾用indirect指示符明示),因此在这里我们看不到main module的间接依赖以及它们的版本,我们可以用go mod graph来查看module依赖图,如下面命令及输出:

$go mod graph
example.com/lazy example.com/a@v0.1.0
example.com/a@v0.1.0 example.com/b@v0.1.0
example.com/a@v0.1.0 example.com/c@v0.1.0

这个go mod graph的输出与我们在上面图中右上角所画的module graph是一致的。此时,如果我们将replace中的第三行删除(example.com/c v0.1.0 => ./c1这一行),即让Go编译器找不到module c@v0.1.0,那么我们构建main modue时将得到下面的错误提示:

$go build
go: example.com/a@v0.1.0 requires
    example.com/c@v0.1.0: missing go.sum entry; to add it:
    go mod download example.com/c

现在我们将执行权限交给Go 1.17!我们需要对go.mod做一些修改(将go.mod中的go 1.15改为go 1.17),这样Go 1.17才能起到作用。接下来,我们执行go mod tidy,让Go 1.17重新构建go.mod:

$go mod tidy
$cat go.mod

module example.com/lazy

go 1.17

require example.com/a v0.1.0

require example.com/b v0.1.0 // indirect

replace (
    example.com/a v0.1.0 => ./a
    example.com/b v0.1.0 => ./b
    example.com/c v0.1.0 => ./c1
    example.com/c v0.2.0 => ./c2
)

我们看到执行go mod tidy之后,go.mod发生了变化:增加了一个require语句块,记录了main module的间接依赖:module b@v0.10。

现在,我们也同样将go.mod replace块中的第三行删除(example.com/c v0.1.0 => ./c1这一行),我们再来用go 1.17构建一次main module,这一次我们没有看到Go编译器的错误提示。也就是说在构建过程中,Go编译器所看到的main module依赖图中并没有module c@v0.1.0。由于module c并没有为main module的构建提供“代码级贡献”,Go命令将其从module依赖图中剪除,Go编译器使用的真实的依赖图如上图右下角所示(pruned module graph)。

这种将那些“占着茅坑不拉屎”、对构建完全没有“贡献”的间接依赖module从构建时使用的依赖图中修剪掉的过程,就被称为module依赖图修剪

之所以要这么做,就是因为Go社区反馈了较多的这方面的问题。这么做的好处是显而易见的:我们再也不用为那些没有对构建没有做出任何“代码级贡献”但又容易出现各种“问题”的module的埋单了

但module依赖图修剪也带来了一个副作用,那就是go.mod文件size的变大。因为Go 1.17版本后,每次go mod tidy,go命令都会对main module的依赖做一次深度扫描(deepening scan),并将main module的所有直接和间接依赖都记录在go.mod中。考虑到内容较多,go 1.17将直接依赖和间接依赖分别放在两个不同的require块儿中。

我们以gnet为例,Go 1.17版本之前的go.mod如下:

module github.com/panjf2000/gnet

go 1.16

require (
    github.com/BurntSushi/toml v0.3.1 // indirect
    github.com/panjf2000/ants/v2 v2.4.6
    github.com/stretchr/testify v1.7.0
    github.com/valyala/bytebufferpool v1.0.0
    go.uber.org/atomic v1.8.0 // indirect
    go.uber.org/multierr v1.7.0 // indirect
    go.uber.org/zap v1.18.1
    golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
    gopkg.in/natefinch/lumberjack.v2 v2.0.0
)

// Go module checksum mismatch, see https://github.com/panjf2000/gnet/issues/219
// retract v1.4.5

使用Go 1.17重新mod tidy后,go.mod内容如下:

module github.com/panjf2000/gnet

go 1.17

require (
    github.com/BurntSushi/toml v0.3.1 // indirect
    github.com/panjf2000/ants/v2 v2.4.6
    github.com/stretchr/testify v1.7.0
    github.com/valyala/bytebufferpool v1.0.0
    go.uber.org/atomic v1.8.0 // indirect
    go.uber.org/multierr v1.7.0 // indirect
    go.uber.org/zap v1.18.1
    golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
    gopkg.in/natefinch/lumberjack.v2 v2.0.0
)

require (
    github.com/davecgh/go-spew v1.1.1 // indirect
    github.com/pmezard/go-difflib v1.0.0 // indirect
    gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

我们看到go 1.17后,go.mod中的main module的依赖分成了两个require块儿,第一个是直接依赖,第二个是间接依赖。但我们看到第一个require中也有包含indirect指示符的依赖。Go关于indirect指示符的使用总是很让人迷惑。关于究竟在Go 1.17中的require中如何使用indirect指示符,可以在这个讨论中找到一些线索。

Go 1.17后go.mod中存储了main module的所有依赖module列表,这似乎也是Go项目第一次有了项目依赖的完整列表,这是不是让你想起了其他主流语言构架系统中的那个lock文件。虽然go.mod不是lock文件,但有了完整依赖列表,至少我们可以像其他语言的lock文件那样,知晓当前Go项目所有依赖的精确版本了。

2. 延迟module加载(lazy module loading)

有了module依赖图休假作为铺垫,延迟module加载就相对好理解了。其含义就是那些在完整的module graph(complete module graph)中,但不在pruned module graph中的module的go.mod不会被go命令加载

3. module deprecation注释

Go 1.16版本在go.mod中加入retract以支持作废某个module的特定版本后,go 1.17更进一步地在go.mod中增加了Deprecated注释,以帮助go module作者作废自己的module。go module作者只需在自己的go.mod中的module声明上面用// Deprecated: comment对module做出注释,就像下面这样:

// Deprecated: use example.com/mod/v2 instead.
module example.com/mod

对于那些使用了被废弃的module的go项目,go list、go get命令都会给出warning。

注意和retract不同,我们不能用Deprecated去作废minor或patch版本,Deprecated是用来作废整个module的。但不同major版本的module被视为不同的module。因此,go module作者一般像上面例子中那样,在Deprecated注释中提示升级到更新的版本,比如:v2版本。

4. 其他一些改变

  • 如果一个main module的go.mod中没有go版本指示符,go 1.17版本的go命令会将其默认为go 1.11版本;
  • 如果一个module的依赖module没有go.mod或该依赖module的go.mod中没有go指示符,go 1.17版本的go命令会将其go.mod中的版本指示符默认为go 1.16,而不是go命令的版本;
  • 如果main module的go.mod中go版本指示符的值为go 1.17及以上版本,那么go mod vendor会在vendor/modules.txt中记录被vendor的module使用的go version,见下面对比:

main module的go.mod中go version < 1.17:

# github.com/BurntSushi/toml v0.3.1
## explicit
# github.com/davecgh/go-spew v1.1.1
github.com/davecgh/go-spew/spew
# github.com/panjf2000/ants/v2 v2.4.6
## explicit
github.com/panjf2000/ants/v2
github.com/panjf2000/ants/v2/internal
# github.com/pmezard/go-difflib v1.0.0
github.com/pmezard/go-difflib/difflib
# github.com/stretchr/testify v1.7.0
## explicit
github.com/stretchr/testify/assert
github.com/stretchr/testify/require
# github.com/valyala/bytebufferpool v1.0.0
## explicit
github.com/valyala/bytebufferpool
... ...

main module的go.mod中go version >= 1.17:

// vendor/modules.txt

# github.com/BurntSushi/toml v0.3.1
## explicit
# github.com/davecgh/go-spew v1.1.1
## explicit
github.com/davecgh/go-spew/spew
# github.com/panjf2000/ants/v2 v2.4.6
## explicit; go 1.14
github.com/panjf2000/ants/v2
github.com/panjf2000/ants/v2/internal
# github.com/pmezard/go-difflib v1.0.0
## explicit
github.com/pmezard/go-difflib/difflib
# github.com/stretchr/testify v1.7.0
## explicit; go 1.13
github.com/stretchr/testify/assert
github.com/stretchr/testify/require
# github.com/valyala/bytebufferpool v1.0.0
... ...
  • 如果main module go.mod中go version指示版本>= go 1.17,那么go mod vendor将忽略各个依赖包中的go.mod和go.sum,这两个文件将不会出现在vendor目录下的各个被vendor的module目录中。

5. 小结

Go 1.17通过对构建时使用的module graph的修剪,让我们再也不用为那些对项目没有代码级贡献但又极容易给构建过程带来麻烦的依赖所烦恼了。新版go.mod的格式还让我们拥有了一份完整的项目依赖列表,这种直观让开发者有一种安全和踏实的感觉。

本文涉及的所有代码可以从这里下载:https://github.com/bigwhite/experiments/tree/master/go1.17-examples


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://51smspush.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
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 商务合作请联系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

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats