标签 RussCox 下的文章

Go语言回顾:从Go 1.0到Go 1.13

Go 1.13版本在2019.9.3正式发布!国外的Gopher Vincent Blanchon发表了一篇文章《Go: Retrospective》(科学上网阅读),对Go从1.0版本到1.13版本做了简要的回顾,这里是那篇文章的译文。

img{512x368}

对于每一位Go开发者来说,Go语言的演化历程是必须要知道的事情。了解这些横跨年份发布的大版本的主要变化将有助于Gopher理解Go语言的发展理念以及该语言每个版本的优势与不足。更多关于特定版本的变更细节,可以参考每个版本对应的Changelog

Go 1.0 – 2012.3月

伴随着Go语言的第一个版本,Go的缔造者还发布了一份兼容性文档。该文档保证未来的Go版本将保持向后兼容性(backward-compatible),即始终兼容已有的代码,保证已有代码在Go新版本下编译和运行的正确性。

Go 1.0版本还包含了go tool pprof命令,这是一个Google pprof C++ profiler的变体。Go 1.0还提供了go vet命令(之前的go tool vet),用于报告Go package中可能的错误。

Go 1.1 – 2013.5月

该版本主要专注于语言改善和性能提升(编译器、垃圾回收、map、goroutine调度)。这里是一个改善后的效果示意图:

img{512x368}
图来自https://dave.cheney.net/2013/05/21/go-11-performance-improvements

这个版本同时还嵌入了一个竞态探测器(race detector),这个工具对于Go这种原生并发的语言是十分必要的。在《Race Detector with ThreadSanitizer”》一文中,你可以找到有关race detector的更多详细信息。

在这个版本中的一个重点变动是Goroutine调度器被重写了,重写后的调度器性能大幅提升。

重写后的Go调度器的设计如下图:

img{512x368}
图来自 https://rakyll.org/scheduler/

M对应的是操作系统的线程。P表示一个处理器(P的数量不能超过GOMAXPROCS),每个P拥有一个本地goroutine队列。在1.1版本之前,P这个抽象并不存在。所有goroutine的调度通过全局互斥锁进行全局级别的管理。这次改进实现了”work-stealing”算法,允许某个P从其他P的队列中”偷goroutine”:

img{512x368}
图来自 https://rakyll.org/scheduler/

更多关于Go调度器调度原理以及”work-stealing”算法的信息,可以查看Jaana B. Dogan的文章《Go’s work-stealing scheduler》

Go 1.2 – 2013.12

在该版本中,Go test命令开始支持代码测试覆盖率统计了,并且通过go提供的新子命令: go tool cover可以查看代码测试覆盖率统计信息:

img{512x368}
图来自 https://blog.golang.org/cover

它还能提供代码覆盖信息:

img{512x368}
图来自 https://blog.golang.org/cover

Go 1.3 – 2014.6

该版本包含了栈管理的一个重要改进。在该版本中,栈内存分配采用连续段(contiguous segment)的分配模式以提升内存分配效率。这将为下一个版本将栈size降到2KB奠定基础。之前的分割栈分配方式(segment stack)存在频繁分配/释放栈段导致栈内存分配性能不稳定(较低)的问题,引入新机制后,分配稳定性和性能都有较大改善。

这里是一个json包的例子,图中显示json包对栈size的敏感度:

img{512x368}
图来自 contiguous stack

使用连续段的栈内存分配管理模式解决了一些程序性能低下的问题。下面是html/template包的性能对stack size的敏感度图:

img{512x368}

更多信息可参见[《How Does the Goroutine Stack Size Evolve?”》(https://medium.com/@blanchon.vincent/go-how-does-the-goroutine-stack-size-evolve-447fc02085e5)]。

这个版本还发布了sync.Pool。这个组件允许我们后面重用结构体,减少内存分配的次数。它也将成为Go生态圈中许多性能提升的源头,比如:标准库中的encoding/json、net/http或是Go社区中的zap等。

关于sync.Pool的更多信息,可以参考文章《Understand the Design of Sync.Pool》

Go开发组在该版本中对channel进行了优化改善,使其性能获得提升。下面是channel在Go 1.2和Go 1.3版本中的基准测试数据对比:

img{512x368}

Go 1.4 – 2014.12

在该版本中,Go提供了对Android的官方支持。使用golang.org/x/mobile包,gopher们可以使用Go编写简单的Android应用。

同时,之前版本中大量用C语言和汇编语言实现的运行时已经被翻译为Go,一个更为精确的垃圾回收器让堆内存分配减少了10~30%。

和版本自身无关的是,Go工程在本次发布后已经从Mercurial迁移到Git,从Google code迁移到github。

Go还发布了go generate命令,该命令可以通过扫码代码中的//go:generate指示器来生成代码,可以帮助Gopher简化代码生成工作。

更多关于这方面的信息可以参考Go blog和这篇文章《Generating code》

Go 1.5 – 2015.8

这个新版本推迟了两个月发布,目的是适应Go新的开发发布周期:每年二月和八月进行发布:

img{512x368}
图来自:https://github.com/golang/go/wiki/Go-Release-Cycle

在该版本中,垃圾回收器全面重构。由于引入并发回收器,回收阶段带来的延迟大幅减少。下面是来自一个生产环境服务器上的延迟数据,我们看到延迟从300ms降到了30ms:

img{512x368}
图片来自 https://blog.golang.org/ismmkeynote

这个版本还发布go tool trace命令,通过该命令我们可以实现执行器的跟踪(trace)。这些跟踪是在test执行、运行时生成的,跟踪信息可以通过浏览器呈现:

img{512x368}
图片来自原始Go Execution Tracer文档

Go 1.6 – 2016.2

这个版本的最显著变化是当使用HTTPS时,将默认支持HTTP/2。

垃圾回收器的延迟在该版本中进一步降低:

img{512x368}
图片来自https://blog.golang.org/ismmkeynote

Go 1.7 – 2016.8

这个版本发布了context包。该包用于处理timeout和取消任务。

更多关于context包的信息,可参考文章:《Context and Cancellation by Propagation》

编译器工具链的性能得到了较大幅度优化,编译速度更快,二进制文件size更小,有些时候幅度可达20~30%。

Go 1.8 – 2017.2

垃圾回收器的延迟在该版本中进一步改善,延迟时间已经全面降到毫秒级别以下:

img{512x368}
图片来自https://blog.golang.org/ismmkeynote

对延迟的优化还将继续。接下来版本的目标是将延迟降到100微秒左右。

这个版本还大幅提升了defer的性能:

img{512x368}
图片来自 https://medium.com/@blanchon.vincent/go-how-does-defer-statement-work-1a9492689b6e

更多关于defer的信息,可以参考文章How Does Defer statement Work?

Go 1.9 – 2017.8

该版本引入了alias语法。

type byte = uint8

这里byte是unit8的一个alias。

sync包增加了Map类型,该类型支持并发访问(原生map类型不支持)。

关于map的更多信息,参考文章“Concurrency Access with Maps”

Go 1.10 – 2018.2

在该版本中,test包引入了一个新的缓存机制,所有通过测试的结果都将被缓存下来。当test没有变化时,重复执行test会节省大量运行test的时间。

first run:
ok      /go/src/retro 0.027s
second run:
ok      /go/src/retro (cached)

go build命令也维护了一个已构建的包的缓存以加速构建性能。

该版本中垃圾回收器并没有显著性能提升。但是Go team为垃圾回收定义了一个新的SLO(Service-Level Objective):

img{512x368}
图片来自https://blog.golang.org/ismmkeynote

Go 1.11 – 2018.8

Go 1.11引入了一个重要的新功能:Go modules。Go module的引入是为了应对过去几年官方调查问卷结果中Go社区反馈的几个主要挑战:

img{512x368}
图片来自 https://blog.golang.org/survey2018-results

另外一个重要功能是一个试验功能:支持WebAssembly。允许开发人员将Go源码编译成一个兼容四个主流浏览器的二进制格式文件。

Go 1.12 – 2019.2

该版本中,go vet基于analysis包进行了重写,使得go vet更为灵活并支持Go开发人员编写自己的checker。

更多关于analyzer的信息可以参考文章《How to Build Your Own Analyzer》

Go 1.13 – 2019.8

在该版本中,sync.Pool得到了改善:当垃圾回收时,pool中对象不会被完全清理掉。它引入了一个cache,用于在两次GC之前清理pool中未使用的对象实例。

逃逸分析(escape analysis)被重新实现了,在该版本中,Go得意更少地在堆上分配内存了。下面是新旧逃逸分析的基准测试对比:

img{512x368}
图片来自 https://github.com/golang/go/issues/23109


我的网课“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 module机制下升级major版本号的实践

Go module机制Go 1.11版本引入,虽然也伴随着不小的质疑声,但总体上Go社区多数Gopher是接受go module的,很多标杆式的Go项目(比如kubernetes、kubernetes client-go等)也都逐渐转向了Go module,并且Gopher也在向core team反馈了自己的建议和问题。Go core team也在go module最初设计的基础上持续进行着改进,比如:即将到来的Go 1.13版本中将增加默认GOPROXY(https://proxy.golang.org)、GOSUMDB(sum.golang.org);增加GONOPROXY、GONOSUMDB以应对私有module的处理;不断丰富的go mod子命令功能等。

随着Go module应用的日益逐步广泛和深入,Gopher们也开始遇到一些最初使用Go module时未曾遇到过的问题,比如升级major版本号(这是由于多数Go project仍处于untag的状态或者1.x.x状态,因此在go module引入初期,少有gopher遇到该类问题)。这篇文章我们就来简单看看如何在go module机制下面升级库的主版本号(major version number)。

一. Go module的“semantic import versioning”

在Russ Cox关于go module的系列论述文章“semantic import versioning”一文中,Russ说明了Go import包兼容性的总原则:

如果新旧版本的包使用相同的导入路径(import path),那么新包与旧包是兼容的。

也就是说如果新旧两个包不兼容,那么应该采用不同的导入路径。

因此,Russ采用了将“major版本”作为导入路径的一部分的设计。这种设计支持在同一个项目或go source文件中import同一个module下的package的不同版本。同一个package虽然包名字相同,但是import path不同。vN作为import path的一部分将用于区分包的不同版本。同时在同一个源文件中,我们可以使用包别名的方式来区分同一个包的不同版本,比如:

import (

    "github.com/bigwhite/foo/bar"

    barV2 "github.com/bigwhite/foo/v2/bar"

    ... ...

)

go module的这种设计对Go包的consumer(包的使用者)来说似乎并未有太多额外工作,但是这给Go包的author们带来了一定的复杂性,他们需要考虑在go module机制下如何将自己的Go module升级major version。稍有不慎,可能就会导致自身代码库的混乱或者package consumer侧无法通过编译或执行行为的混乱。

下面我们就从go package author的角度实践一下究竟该如何做module major版本号的升级。Go module为go package author提供了两种major version升级的方案,我们下面逐一看一下。我们的实验环境基于go 1.12.5 (ubuntu 16.04)。

二. 使用“major branch”方案

“major branch”方案对于多数gopher来说是一个过渡比较自然的方案,它通过建立vN分支并基于vN分支打vN.x.x的tag的方式做major version的升级。当然是否建立vN分支以及打vN.x.x tag都是一个可选的操作。

我们在bitbucket.org上建立一个公共仓库:bitbucket.org/bigwhite/modules-major-branch,其初始结构和代码如下(注意:此时本地开发环境中GO111MODULE=on):

# tree -LF 2 modules-major-branch
modules-major-branch
├── foo/
│   └── foo.go
├── go.mod
└── README.md

1 directory, 3 files

//go.mod

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch

go 1.12

// foo.go

package foo

import "fmt"

func Foo() {
        fmt.Println("foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1")
}

接下来,我们建立modules-major-branch/foo包的消费者项目:modules-major-branch-test

# tree -LF 1 ./modules-major-branch-test/
./modules-major-branch-test/
├── go.mod
├── go.sum
└── main.go

0 directories, 3 files

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

# cat main.go
package main

import (
    "bitbucket.org/bigwhite/modules-major-branch/foo"
)

func main() {
    foo.Foo()
}

我们run一下“消费者”:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch/foo latest
go: finding bitbucket.org/bigwhite/modules-major-branch latest
go: downloading bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295
go: extracting bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295
foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1

我们看到在这个阶段消费成功。

作为modules-major-branch的author,随着module功能演进,modules-major-branch到达了发布1.0版本的节点:

# cat foo/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0")
}

# git tag v1.0.0
# git push --tag origin master

接下来,我们让consumer升级对modules-major-branch/foo的依赖到v1.0.0。这种升级是不会自动进行,是需要consumer的开发者自己决策后手工进行的,否则会给开发者带来困惑。我们通过go mod edit命令修改consumer的require:

# go mod edit -require=bitbucket.org/bigwhite/modules-major-branch@v1.0.0

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require bitbucket.org/bigwhite/modules-major-branch v1.0.0

我们来运行一下升级依赖后的程序:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch v1.0.0
go: downloading bitbucket.org/bigwhite/modules-major-branch v1.0.0
go: extracting bitbucket.org/bigwhite/modules-major-branch v1.0.0
foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0

从pre-v1到v1在最新的go module机制中还算不上major版本的升级,接下来我们就来看看foo包的作者应该如何对modules-major-branch module做出不兼容的升级:v1 -> v2。

当modules-major-branch module即将做出不兼容升级时,一般会为当前版本建立维护分支(比如:v1分支,并在v1分支上继续对v1版本进行维护、打补丁),然后再在master分支上做出不兼容的修改。

# git checkout -b v1

# git checkout master

# cat foo/foo.go
package foo

import "fmt"

func Foo2() {
    fmt.Println("foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0")
}

从代码可以看到,在master分支上,我们删除了foo包中的Foo函数,新增了Foo2函数。但仅做这些还不够。在本文一开始我们就提到过原则:如果新旧两个包不兼容,那么应该采用不同的导入路径。我们为modules-major-branch module做出了不兼容的修改,也需要modules-major-branch module有着不同的导入路径,我们需要修改modules-major-branch module的module根路径:

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch/v2

go 1.12

# git tag v2.0.0
# git push --tag origin master

我们在module根路径后面加上了v2,并基于master建立了tag: v2.0.0。

我们再来看看consumer端应该如何应对modules-major-branch module的不兼容修改。如果consumer要使用最新的Foo2函数的话,我们需要对main.go做出如下改动:

//modules-major-branch-test/main.go

package main

import (
    "bitbucket.org/bigwhite/modules-major-branch/v2/foo"
)

func main() {
    foo.Foo2()
}

接下来我们不需要手工修改modules-major-branch-test的go.mod中依赖,直接运行go run即可:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch/v2/foo latest
go: finding bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
go: downloading bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
go: extracting bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0

我们看到go编译器会自动发现依赖变更,并下载对应的包并更新go.mod和go.num:

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require (
    bitbucket.org/bigwhite/modules-major-branch v1.0.0
    bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 // indirect
)

modules-major-branch-test此时已经不再需要依赖v1.0.0了,我们可以通过go mod tidy清理一下go.mod中的依赖:

# go mod tidy
# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0

我们看到:现在就只剩下对modules-major-branch v2的依赖了。

后续modules-major-branch可以在master分支上持续演进,直到又有不兼容改动时,可以基于master建立v2维护分支,master分支将升级为v3(module)。

再小结一下:

对包的作者而言,升级major版本号需要:

  • 升级module的根路径,增加vN

  • 建立vN.x.x形式的tag(可选,如果不打tag,go会在consumer的go.mod中使用伪版本号,比如:bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0-20190603050009-28a5b8da279e)

如果modules-major-branch内部有相互的包引用,那么在升级major号的时候,这些包的import路径也要增加vN,否则就会存在在高major version的代码中引用低major version包代码的情况,这也是包作者最容易忽略的事情。github.com/marwan-at-work/mod是一个为module作者提供的升级/降级major version号的工具,它可以帮助包作者方便地自动修改项目内所有源文件中的import path。有gopher已经提出希望go官方提供upgrade/downgrade的支持,但目前core team尚未明确是否增加。

对于consumer而言,升级依赖包的major版本号,只需要在import包时在import path中增加vN即可,当然代码中也要针对不兼容的部分进行修改,然后go工具会自动下载相关包。

三. 使用 “major subdirectory”方案

go module机制还提供了一种我个人觉得较为怪异的方案或者说用起来不那么自然的方案,那就是利用子目录分割不同主版本。如果某个module目前已经演化到v3版本了,那么这个module所在仓库的目录结构应该是这样的:

# tree modules-major-subdir
modules-major-subdir
├── bar
│   └── bar.go
├── go.mod
├── v2
│   ├── bar
│   │   └── bar.go
│   └── go.mod
└── v3
    ├── bar
    │   └── bar.go
    └── go.mod

这里直接用vN作为子目录名字,在代码仓库中将不同版本module放置在不同的subdir中,这样即便不打tag,通过subdir也能找到对应版本的module包。以上图中的v2为例,该子目录下go.mod如下:

# cat go.mod
module bitbucket.org/bigwhite/modules-major-subdir/v2

go 1.12

v3也是类似。在各自子目录中,module的根路径都是带有vN扩展的。

接下来,我们就来创建consumer来分别调用不同版本的modules-major-subdir/bar包。和modules-major-branch-test类似,我们建立modules-major-subdir-test来作为consumer调用modules-major-subdir/bar包:

// modules-major-subdir-test

# cat go.mod
module bitbucket.org/bigwhite/modules-major-subdir-test

go 1.12

# cat main.go
package main

import (
    "bitbucket.org/bigwhite/modules-major-subdir/bar"
)

func main() {
    bar.Bar()
}

运行一下consumer:

# go run main.go

go: finding bitbucket.org/bigwhite/modules-major-subdir/bar latest
go: finding bitbucket.org/bigwhite/modules-major-subdir latest
go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
bar.Bar of module: bitbucket.org/bigwhite/modules-major-subdir

我们修改main.go,调用v2版本bar包中Bar2函数:

package main

import (
    "bitbucket.org/bigwhite/modules-major-subdir/v2/bar"
)

func main() {
    bar.Bar2()
}

再次运行main.go:

# go run main.go
go: finding bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: finding bitbucket.org/bigwhite/modules-major-subdir/v2/bar latest
go: finding bitbucket.org/bigwhite/modules-major-subdir/v2 latest
go: downloading bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9
go: extracting bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9
bar.Bar2 of module: bitbucket.org/bigwhite/modules-major-subdir v2

我们看到:go编译器自动找到了位于modules-major-subdir仓库下v2子目录下的v2版本bar包。

从demo来看,似乎这种通过subdir方式来实现major version升级的方式更为“简单”一些。但笔者总感觉这种方式有些“怪”,尤其是在与tag交叉使用时可能会带来一些困惑,其他主流语言也鲜有使用这种方式进行major version升级的。另外一旦使用这种方式,似乎也很难利用git工具在不同major版本之间进行代码的merge(复用)了。

另外和major branch方案一样,如果module内部有相互的包引用,那么在升级major号的时候,这些包的import路径也要增加vN,否则也会存在在高major version的代码中引用低major version包代码的情况。

四. 小结

Go module作为主流语言依赖管理思路之外的一个“探索性”创新,势必在初期要有一段坎坷的道路要走。好事多磨,相信经过Go 1.11~Go 1.13三个版本的改进以及社区在工具方面对go module的逐渐的完善的支持,Go module会成为gopher日常Go开发的一柄利器,彻底解决Go的包依赖问题。

上述demo源码可在bitbucket.org/bigwhite下找到。

另外这里要提一点:国内的码云(gitee.com)目前对go module major version升级支持的还有问题。同样的操作,但在gitee.com下总是提示:“go.mod has post-v0 module path”


我的网课“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 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
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats