golang struct tag内置函数new 和struct 初始化的区别

make、new操作
make用于内建类型(map、slice&和channel)的内存分配。new用于各种类型的内存分配。
内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:
new返回指针。
内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。
make返回初始化后的(非零)值。
下面这个图详细的解释了new和make之间的区别。
图2.5 make和new对应底层的内存分配
关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 此处罗列 部分类型 的 “零值”
0 //rune的实际类型是 int32
0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
代码示例:
var map1 map[string]string = make(map[string]string)
fmt.Println(map1)
fmt.Println(map1 == nil)
var map2 *map[string]string = new(map[string]string)
fmt.Println(map2)
fmt.Println(map2 == nil)
fmt.Println(*map2)
fmt.Println(*map2 == nil)
var map1 map[string]string
map1["aaa"] = "AAA"
map1["bbb"] = "BBB"
map1["ccc"] = "CCC"
fmt.Println(map1)
fmt.Println(len(map1))
map[bbb:BBB ccc:CCC aaa:AAA]
<span style="color: #
Success: process exited with code <span style="color: #.
参考资料:
golang语言中map的初始化及使用:&/articles/2379
Go语言中new()和 make()的区别详解:&http://www.jb51.net/article/56837.htm
:&http://blog.csdn.net/xtxy/article/details/
golang 内置函数new() 和struct{} 初始化的区别:&/articles/3377
/astaxie/build-web-application-with-golang/blob/master/zh/02.2.md
阅读(...) 评论()Golang | Tony Bai
标签 Golang 下的文章
二月 3, 2017
在已经过去的,继在2009年之后再次成为编程语言界的明星- 问鼎 2016年度语言。这与Go team、Go community和全世界的Gophers的努力是分不开的。按计划在这个2月份,Go team将正式发布Go 1.8版本(截至目前,Go的最新版本是)。在这里我们一起来看一下在Go 1.8版本中都有哪些值得Gopher们关注的变化。
一、语言(Language)
Go 1.8版本依旧坚守Go Team之前的承诺,即:使用Go 1.7及以前版本编写的Go代码,理论上都可以通过Go 1.8进行编译并运行。因此在臆想中的变成现实之前,每个Go Release版本在语言这方面的“改变”都会是十分微小的。
1、仅tags不同的两个struct可以相互做显式类型转换
在Go 1.8版本以前,两个struct即便字段个数相同且每个字段类型均一样,但如果某个字段的tag描述不一样,这两个struct相互间也不能做显式类型转换,比如:
//go18-examples/language/structtag.go
package main
import "fmt"
type XmlEventRegRequest struct {
string `xml:"appid"`
NeedReply int
`xml:"Reply,omitempty"`
type JsonEventRegRequest struct {
string `json:"appid"`
NeedReply int
`json:"reply,omitempty"`
func convert(in *XmlEventRegRequest) *JsonEventRegRequest {
out := &JsonEventRegRequest{}
*out = (JsonEventRegRequest)(*in)
return out
func main() {
in := XmlEventRegRequest{
NeedReply: 1,
out := convert(&in)
fmt.Println(out)
采用Go 1.7.4版本go compiler进行编译,我们会得到如下错误输出:
$go build structtag.go
# command-line-arguments
./structtag.go:17: cannot convert *in (type XmlEventRegRequest) to type JsonEventRegRequest
但在Go 1.8中,gc将忽略tag值的不同,使得显式类型转换成为可能:
$go run structtag.go
改变虽小,但带来的便利却不小,否则针对上面代码中的convert,我们只能做逐一字段赋值了。
2、浮点常量的指数部分至少支持16bits长
在Go 1.8版本之前的中,关于浮点数常量的指数部分的描述如下:
Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed exponent of at least 32 bits.
在Go 1.8版本中,文档中对于浮点数常量指数部分的长度的实现的条件放宽了,由支持最少32bit,放宽到最少支持16bits:
Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed binary exponent of at least 16 bits.
但Go 1.8版本go compiler实际仍然支持至少32bits的指数部分长度,因此这个改变对现存的所有Go源码不会造成影响。
二、标准库(Standard Library)
Go号称是一门”Batteries Included”编程语言。“Batteries Included”指的就是Go语言强大的标准库。使用Go标准库,你可以完成绝大部分你想要的功能,而无需再使用第三方库。Go语言的每次版本更新,都会在标准库环节增加强大的功能、提升性能或是提高使用上的便利性。每次版本更新,标准库也是改动最大的部分。这次也不例外,我们逐一来看。
1、便于slice sort的sort.Slice函数
在Go 1.8之前我们要对一个slice进行sort,需要定义出实现了下面接口的slice type:
//$GOROOT/src/sort.go
type Interface interface {
// Len is the number of elements in the collection.
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
标准库定义了一些应对常见类型slice的sort类型以及对应的函数:
StringSlice -& sort.Strings
IntSlice -& sort.Ints
Float64Slice -& sort.Float64s
但即便如此,对于用户定义的struct或其他自定义类型的slice进行排序仍需定义一个新type,比如下面这个例子中的TiboeIndexByRank:
//go18-examples/stdlib/sort/sortslice-before-go18.go
package main
type Lang struct {
Name string
type TiboeIndexByRank []Lang
func (l TiboeIndexByRank) Len() int
{ return len(l) }
func (l TiboeIndexByRank) Less(i, j int) bool { return l[i].Rank & l[j].Rank }
func (l TiboeIndexByRank) Swap(i, j int)
{ l[i], l[j] = l[j], l[i] }
func main() {
langs := []Lang{
{"rust", 2},
{"go", 1},
{"swift", 3},
sort.Sort(TiboeIndexByRank(langs))
fmt.Printf("%v\n", langs)
$go run sortslice-before-go18.go
[{go 1} {rust 2} {swift 3}]
从上面的例子可以看到,我们要对[]Lang这个slice进行排序,我们就需要为之定义一个专门用于排序的类型:这里是TiboeIndexByRank,并让其实现sort.Interface接口。使用过sort包的gophers们可能都意识到了,我们在为新的slice type实现sort.Interface接口时,那三个方法的Body几乎每次都是一样的。为了使得gopher们在排序slice时编码更为简化和便捷,减少copy&paste,Go 1.8为slice type新增了三个函数:Slice、SliceStable和SliceIsSorted。我们重新用Go 1.8的sort.Slice函数实现上面例子中的排序需求,代码如下:
//go18-examples/stdlib/sort/sortslice-in-go18.go
package main
type Lang struct {
Name string
func main() {
langs := []Lang{
{"rust", 2},
{"go", 1},
{"swift", 3},
sort.Slice(langs, func(i, j int) bool { return langs[i].Rank & langs[j].Rank })
fmt.Printf("%v\n", langs)
$go run sortslice-in-go18.go
[{go 1} {rust 2} {swift 3}]
实现sort,需要三要素:Len、Swap和Less。在1.8之前,我们通过实现sort.Interface实现了这三个要素;而在1.8版本里,Slice函数通过reflect获取到swap和length,通过结合闭包实现的less参数让Less要素也具备了。我们从下面sort.Slice的源码可以看出这一点:
// $GOROOT/src/sort/sort.go
func Slice(slice interface{}, less func(i, j int) bool) {
rv := reflect.ValueOf(slice)
swap := reflect.Swapper(slice)
length := rv.Len()
quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))
2、支持HTTP/2 Push
继在全面支持之后,Go 1.8又新增了对的支持。是在的基础上的下一代HTTP协议,虽然当前HTTPS的应用尚不是十分广泛。而是HTTP/2的一个重要特性,无疑其提出的初衷也仍然是为了改善网络传输性能,提高Web服务的用户侧体验。这里我们可以借用知名网络提供商上的一幅示意图来诠释HTTP/2 Push究竟是什么:
从上图中,我们可以看到:当Browser向Server发起Get page.html请求后,在同一条TCP Connection上,Server主动将style.css和image.png两个资源文件推送(Push)给了Browser。这是由于Server端启用了HTTP/2 Push机制,并预测判断Browser很可能会在接下来发起Get style.css和image.png两个资源的请求。这是一种典型的:“你可能会需要,但即使你不要,我也推给你”的处世哲学^0^。这种机制虽然在一定程度上能改善网络传输性能(减少Client发起Get的次数),但也可能造成带宽的浪费,因为这些主动推送给Browser的资源很可能是Browser所不需要的或是已经在Browser cache中存在的资源。
接下来,我们来看看Go 1.8是如何在net/http包中提供对HTTP/2 Push的支持的。由于HTTP/2是基于HTTPS的,因此我们先使用generate_cert.go生成程序所需的私钥和证书:
// 在go18-examples/stdlib/http2-push目录下,执行:
$go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1
10:58:01 written cert.pem
10:58:01 written key.pem
支持HTTP/2 Push的server端代码如下:
// go18-examples/stdlib/http2-push/server.go
package main
"net/http"
const mainJS = `document.write('Hello World!');`
func main() {
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
pusher, ok := w.(http.Pusher)
// If it's a HTTP/2 Server.
// Push is supported. Try pushing rather than waiting for the browser.
if err := pusher.Push("/static/img/gopherizeme.png", nil); err != nil {
log.Printf("Failed to push: %v", err)
fmt.Fprintf(w, `&html&
&title&Hello Go 1.8&/title&
&img src="/static/img/gopherizeme.png"&&/img&
log.Fatal(http.ListenAndServeTLS(":8080", "./cert.pem", "./key.pem", nil))
运行这段代码,打开Google Chrome浏览器,输入:https://127.0.0.1:8080,忽略浏览器的访问非受信网站的警告,继续浏览你就能看到下面的页面(这里打开了Chrome的“检查”功能):
从示例图中的“检查”窗口,我们可以看到这个image资源就是Server主动推送给客户端的,这样浏览器在Get /后无需再发起一次Get /static/img/gopherizeme.png的请求了。
而这一切的背后,其实是HTTP/2的ResponseWriter实现了Go 1.8新增的http.Pusher interface:
// $GOROOT/src/net/http/http.go
// Pusher is the interface implemented by ResponseWriters that support
// HTTP/2 server push. For more background, see
// https://tools.ietf.org/html/rfc7540#section-8.2.
type Pusher interface {
Push(target string, opts *PushOptions) error
3、支持HTTP Server优雅退出
Go 1.8中增加对HTTP Server优雅退出(gracefullly exit)的支持,对应的新增方法为:
func (srv *Server) Shutdown(ctx context.Context) error
和server.Close在调用时瞬间关闭所有active的Listeners和所有状态为New、Active或idle的connections不同,server.Shutdown首先关闭所有active Listeners和所有处于idle状态的Connections,然后无限等待那些处于active状态的connection变为idle状态后,关闭它们并server退出。如果有一个connection依然处于active状态,那么server将一直block在那里。因此Shutdown接受一个context参数,调用者可以通过context传入一个Shutdown等待的超时时间。一旦超时,Shutdown将直接返回。对于仍然处理active状态的Connection,就任其自生自灭(通常是进程退出后,自动关闭)。通过Shutdown的源码我们也可以看出大致的原理:
// $GOROOT/src/net/http/server.go
func (srv *Server) Shutdown(ctx context.Context) error {
atomic.AddInt32(&srv.inShutdown, 1)
defer atomic.AddInt32(&srv.inShutdown, -1)
srv.mu.Lock()
lnerr := srv.closeListenersLocked()
srv.closeDoneChanLocked()
srv.mu.Unlock()
ticker := time.NewTicker(shutdownPollInterval)
defer ticker.Stop()
if srv.closeIdleConns() {
return lnerr
case &-ctx.Done():
return ctx.Err()
case &-ticker.C:
我们来编写一个例子:
// go18-examples/stdlib/graceful/server.go
"net/http"
"os/signal"
func main() {
exit := make(chan os.Signal)
signal.Notify(exit, os.Interrupt)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("Handle a new request:", *r)
time.Sleep(10 * time.Second)
log.Println("Handle the request ok!")
io.WriteString(w, "Finished!")
srv := &http.Server{
Handler: http.DefaultServeMux,
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Printf("listen: %s\n", err)
&-exit // wait for SIGINT
log.Println("Shutting down server...")
// Wait no longer than 30 seconds before halting
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
err := srv.Shutdown(ctx)
log.Println("Server gracefully stopped:", err)
在上述例子中,我们通过来拦截Linux Interrupt信号并处理。我们通过context给Shutdown传入30s的超时参数,这样Shutdown在退出之前会给各个Active connections 30s的退出时间。下面分为几种情况run一下这个例子:
当前无active connections
在这种情况下,我们run上述demo,ctrl + C后,上述demo直接退出:
$go run server.go
^C 15:13:16 Shutting down server...
15:13:16 Server gracefully stopped: &nil&
b) 当前有未处理完的active connections,ctx 超时
为了模拟这一情况,我们修改一下参数。让每个request handler的sleep时间为30s,而Shutdown ctx的超时时间改为10s。我们再来运行这个demo,并通过curl命令连接该server(curl
-v http://localhost:8080),待连接成功后,再立即ctrl+c停止Server,待约10s后,我们得到如下日志:
$go run server.go
15:15:57 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} &nil& 0 [] false localhost:8080 map[] map[] &nil& map[] [::1]:52590 / &nil& &nil& &nil& 0xc}
^C 15:15:59 Shutting down server...
15:15:59 listen: http: Server closed
15:16:09 Server gracefully stopped: context deadline exceeded
c) 当前有未处理完的active connections,ctx超时之前,这些connections处理ok了
我们将上述demo的参数还原,即request handler sleep 10s,而Shutdown ctx超时时间为30s,运行这个Demo后,通过curl命令连接该server,待连接成功后,再立即ctrl+c停止Server。等待约10s后,我们得到如下日志:
$go run server.go
15:19:56 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} &nil& 0 [] false localhost:8080 map[] map[] &nil& map[] [::1]:52605 / &nil& &nil& &nil& 0xc}
^C 15:19:59 Shutting down server...
15:19:59 listen: http: Server closed
15:20:06 Handle the request ok!
15:20:06 Server gracefully stopped: &nil&
可以看出,当ctx超时之前,request处理ok,connection关闭。这时不再有active connection和idle connection了,Shutdown成功返回,server立即退出。
4、Mutex Contention Profiling
Go 1.8中runtime新增了对Mutex和RWMutex的profiling(剖析)支持。golang team成员,负责从go user角度去看待go team的work是否满足用户需求的在其个人站点上写了一篇,这里借用一下其中的Demo:
//go18-examples/stdlib/mutexprofile/mutexprofile.go
package main
"net/http"
_ "net/http/pprof"
func main() {
var mu sync.Mutex
var items = make(map[int]struct{})
runtime.SetMutexProfileFraction(5)
for i := 0; i & ; i++ {
go func(i int) {
defer mu.Unlock()
items[i] = struct{}{}
http.ListenAndServe(":8888", nil)
运行该程序后,在浏览器中输入:http://localhost:8888/debug/pprof/mutex,你就可以看到有关该程序的mutex profile(耐心等待一小会儿,因为数据的采样需要一点点时间^0^):
--- mutex:
cycles/second=
sampling period=5
6 @ 0x106c4d1 0x13112ab 0x1059991
构建该程序,然后通过下面命令:
go build mutexprofile.go
./mutexprofile
go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1
可以进入pprof交互界面,这个是所有用过的们所熟知的:
$go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1
Fetching profile from http://localhost:8888/debug/pprof/mutex?debug=1
Saved profile in /Users/tony/pprof/pprof.mutexprofile.localhost:8888.contentions.delay.003.pb.gz
Entering interactive mode (type "help" for commands)
(pprof) list
Total: 12.98s
ROUTINE ======================== main.main.func1 in /Users/tony/Test/GoToolsProjects//bigwhite/experiments/go18-examples/stdlib/mutexprofile/mutexprofile.go
12.98s (flat, cum)
100% of Total
defer mu.Unlock()
items[i] = struct{}{}
http.ListenAndServe(":8888", nil)
ROUTINE ======================== runtime.goexit in /Users/tony/.bin/go18rc2/src/runtime/asm_amd64.s
12.98s (flat, cum)
100% of Total
2194:// The top-most function running on a goroutine
2195:// returns to goexit+PCQuantum.
2196:TEXT runtime·goexit(SB),NOSPLIT,$0-0
runtime·goexit1(SB)
// does not return
// traceback from goexit1 must hit code range of goexit
2202:TEXT runtime·prefetcht0(SB),NOSPLIT,$0-8
ROUTINE ======================== sync.(*Mutex).Unlock in /Users/tony/.bin/go18rc2/src/sync/mutex.go
12.98s (flat, cum)
100% of Total
// Grab the right to wake someone.
new = (old - 1&&mutexWaiterShift) | mutexWoken
pareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema)
old = m.state
(pprof) top10
1.29s of 1.29s total (
sync.(*Mutex).Unlock
main.main.func1
runtime.goexit
go pprof的另外一个用法就是在go test时,mutexprofile同样支持这一点:
go test -mutexprofile=mutex.out
go tool pprof &test.binary& mutex.out
5、其他重要改动
Go 1.8标准库还有两个值得注意的改动,一个是:crypto/tls,另一个是database/sql。
在逐渐成为主流的今天,各个编程语言对HTTPS连接的底层加密协议- 支持的成熟度日益被人们所关注。Go 1.8给广大Gophers们带来了一个更为成熟、性能更好、更为安全的TLS实现,同时也增加了对一些TLS领域最新协议规范的支持。无论你是实现TLS Server端,还是Client端,都将从中获益。
Go 1.8在crypto/tls中提供了基于ChaCha20-Poly1305的cipher suite,其中ChaCha20是一种stream cipher算法;而Poly1305则是一种code authenticator算法。它们共同组成一个TLS suite。使用这个suite,将使得你的web service或站点,这是因为传统的AES算法实现在没有的情况下cost更多。因此,如果你在使用tls时没有指定cipher suite,那么Go 1.8会根据硬件支持情况(是否有AES的硬件支持),来决定是使用ChaCha20还是AES算法。除此之外,crypto/tls还实现了更为安全和高效的密钥交换算法等。
以来,database/sql包的变化很小,但对于该包的feature需求却在与日俱增。终于在Go 1.8这个dev cycle中,的作者在的“指导”下,开始对database/sql进行“大规模”的改善。在Go 1.8中,借助于context.Context的帮助,database/sql增加了Cancelable Queries、SQL Database Type、Multiple Result Sets、Database ping、Named Parameters和Transaction Isolation等新Features。在的Advent 2016系列文章中,我们可以看到,文章针对Go 1.8 database/sql包新增的features作了详细解释。
三、Go工具链(Go Toolchain)
在目前市面上的主流编程语言中,如果说Go的工具链在成熟度和完善度方面排第二,那没有语言敢称自己是第一吧^_^。Go 1.8在Go Toolchain上继续做着持续地改进,下面我们来逐一看看。
1、Plugins
Go在1.8版本中提供了对Plugin的初步支持,并且这种支持仅限于。plugin这个术语在不同语言、不同情景上下文中有着不同的含义,那么什么是Go Plugin呢?
Go Plugin为Go程序提供了一种在运行时加载代码、执行代码以改变运行行为的能力,它实质上由两个部分组成:
go build -buildmode=plugin xx.go 构建xx.so plugin文件
利用plugin包在运行时动态加载xx.so并执行xx.so中的代码
C程序员看到这里肯定会有似曾相识的赶脚,因为这和传统的在概念上十分类似:
go build -buildmode=plugin xx.go 类似于 gcc -o xx.so -shared xx.c
go plugin包 类似于 linux上的dlopen/dlsym或windows上的LoadLibrary
我们来看一个例子!我们先来建立一个名为foo.so的go plugin:
//go18-examples/gotoolchain/plugins/foo.go
package main
import "fmt"
func init() {
fmt.Println("init function in plugin foo")
func Foo(in string) string {
return "Hello, " + in
func foo(in string) string {
return "hello, " + in
通过go build命令将foo.go编译为foo.so:
# go build -buildmode=plugin foo.go
# ldd foo.so
linux-vdso.so.1 =&
(0x00007ffe47f67000)
libpthread.so.0 =& /lib/x86_64-linux-gnu/libpthread.so.0 (0xf4b000)
libc.so.6 =& /lib/x86_64-linux-gnu/libc.so.6 (0xb82000)
/lib64/ld-linux-x86-64.so.2 (0xcfcf000)
# nm foo.so|grep Foo
0010 t local.plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a.Foo
0010 T plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a.Foo
a0dc D type..namedata.Foo.
我们看到go plugin的.so文件就是一个标准的Linux动态共享库文件,我们可以通过nm命令查看.so中定义的各种符号。接下来,我们来load这个.so,并查找并调用相应符号:
//go18-examples/gotoolchain/plugins/main.go
package main
func init() {
fmt.Println("init in main program")
func loadPlugin(i int) {
fmt.Println("load plugin #", i)
var err error
fmt.Println("before opening the foo.so")
p, err := plugin.Open("foo.so")
if err != nil {
fmt.Println("plugin Open error:", err)
fmt.Println("after opening the foo.so")
f, err := p.Lookup("Foo")
if err != nil {
fmt.Println("plugin Lookup symbol Foo error:", err)
fmt.Println(f.(func(string) string)("gophers"))
f, err = p.Lookup("foo")
if err != nil {
fmt.Println("plugin Lookup symbol foo error:", err)
fmt.Println(f.(func(string) string)("gophers"))
v, err := p.Lookup("V")
if err != nil {
fmt.Println("plugin Lookup symbol V error:", err)
fmt.Println(*v.(*int))
v, err = p.Lookup("v")
if err != nil {
fmt.Println("plugin Lookup symbol v error:", err)
fmt.Println(*v.(*int))
fmt.Println("load plugin #", i, "done")
func main() {
var counter int = 1
loadPlugin(counter)
time.Sleep(time.Second * 30)
执行这个程序:
# go run main.go
init in main program
load plugin # 1
before opening the foo.so
init function in plugin foo
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a
load plugin # 1 done
load plugin # 2
before opening the foo.so
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a
load plugin # 2 done
我们来分析一下这个执行结果!
a) foo.go中的代码也包含在main package下,但只是当foo.so被第一次加载时,foo.go中的init函数才会被执行;
b) foo.go中的exported function和variable才能被Lookup到,如Foo、V;查找unexported的变量和函数符号将得到error信息,如:“symbol foo not found in plugin”;
c) Lookup返回的是plugin.Symbol类型的值,plugin.Symbol是一个指向plugin中变量或函数的指针;
d) foo.go中的init在后续重复加载中并不会被执行。
注意:plugin.Lookup是goroutine-safe的。
在golang-dev group上,有人曾问过:buildmode=c-shared和buildmode=plugin有何差别?Go team member给出的答案如下:
The difference is mainly on the program that loads the shared library.
For c-shared, we can't assume anything about the host, so the c-shared dynamic library must be self-contained, but for plugin, we know the host program will be a Go program built with the same runtime version, so the toolchain can omit at least the runtime package from the dynamic library, and possibly more if it's certain that some packages are linked into the host program. (This optimization hasn't be implemented yet, but we need the distinction to enable this kind of optimization in the future.)
2、默认的GOPATH
Go team在Go 1.8以及后续版本会更加注重”Go语言的亲民性”,即进一步降低Go的入门使用门槛,让大家更加Happy的使用Go。对于一个Go初学者来说,一上来就进行GOPATH的设置很可能让其感到有些迷惑,甚至有挫折感,就像建立Java开发环境需要设置JAVA_HOME和CLASSPATH一样。Gophers们期望能做到Go的安装即可用。因此Go 1.8就在这方面做出了改进:支持默认的GOPATH。
在Linux/Mac系下,默认的GOPATH为$HOME/go,在Windows下,GOPATH默认路径为:%USERPROFILE%/go。你可以通过下面命令查看到这一结果:
GOARCH="amd64"
GOBIN="/home/tonybai/.bin/go18rc3/bin"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/tonybai/go"
GOROOT="/home/tonybai/.bin/go18rc3"
GOTOOLDIR="/home/tonybai/.bin/go18rc3/pkg/tool/linux_amd64"
GCCGO="gccgo"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build=/tmp/go-build -gno-record-gcc-switches"
CGO_ENABLED="1"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
BTW,在Linux/Mac下,默认的GOROOT为/usr/local/go,如果你的Go环境没有安装到这个路径下,在没有设置$GOROOT环境变量的情况下,当你执行go subcommand相关命令时,你会看到如下错误:
go: cannot find GOROOT directory: /usr/local/go
3、其他变化
Go 1.8删除了中增加的用于关闭ssa新后端的”-ssa=0” compiler flag,并且将ssa backend扩展到所有architecture中,对ssa后端也进一步做了优化。与此同时,为了将来进一步的性能优化打基础,Go 1.8还引入了一个新编译器前端,当然这对于普通Gopher的Go使用并没有什么影响。
Go 1.8还新增go bug子命令,该命令会自动使用默认浏览器打开new issue页面,并将采集到的issue提交者的系统信息填入issue模板,以帮助gopher提交符合要求的go issue,下面是go bug打开的issue page的图示:
四、性能变化(Performance Improvement)
无论是Gotoolchain、还是runtime(包括GC)的性能,一直都是Go team重点关注的领域。本次Go 1.8依旧给广大Gophers们带来了性能提升方面的惊喜。
首先,Go 后端扩展到所有architecture和新编译器前端的引入,将会给除X86-64之外架构上运行的Go代码带来约20-30%的运行性能提升。对于x86-64,虽然Go 1.7就已经开启了SSA,但Go 1.8对SSA做了进一步优化,x86-64上的Go代码依旧可能会得到10%以内的性能提升。
其次,Go 1.8持续对Go compiler和linker做性能优化,和1.7相比,平均编译链接的性能提升幅度在15%左右。虽然依旧没有达到的性能水准。不过,优化依旧在持续进行中,目标的达成是可期的。
再次,GC在低延迟方面的优化给了我们最大的惊喜。在Go 1.8中,由于消除了GC的“”,使得GC STW(stop-the-world)的时间通常低于100微秒,甚至经常低于10微秒。当然这或多或少是以牺牲“吞吐”作为代价的。因此在Go 1.9中,GC的改进将持续进行,会在吞吐和低延迟上做一个很好的平衡。
最后,defer的性能消耗在Go 1.8中下降了一半,与此下降幅度相同的还有通过cgo在go中调用C代码的性能消耗。
五、小结兼参考资料
Go 1.8的变化不仅仅是以上这些,更多变化以及详细的描述请参考下面参考资料中的“Go 1.8 Release Notes”:
以上demo中的代码在可以找到。
一月 17, 2017
第一次的 cluster目前运行良好,master node上的组件状态也始终是“没毛病”:
# kubectl get cs
controller-manager
{"health": "true"}
不过在第二次尝试时遇到的各种网络问题还是让我“心有余悸”。于是趁上个周末,对Kubernetes的网络原理进行了一些针对性的学习。这里把对Kubernetes网络的理解记录一下和大家一起分享。
Kubernetes支持、、等多种Drivers,但由于学习过程使用的是第一个cluster的Flannel网络,这里的网络原理只针对k8s+Flannel网络。
一、环境+提示
凡涉及到Docker、Kubernetes这类正在active dev的开源项目的文章,我都不得不提一嘴,那就是随着K8s以及flannel的演化,本文中的一些说法可能不再正确。提醒大家:阅读此类技术文章务必结合“环境”。
这里我们使用的环境就是我第一次建立k8s cluster的环境:
# kube-apiserver --version
Kubernetes v1.3.7
# /opt/bin/flanneld -version
# /opt/bin/etcd -version
etcd Version: 3.0.12
Git SHA: 2d1e2e8
Go Version: go1.6.3
Go OS/Arch: linux/amd64
另外整个集群搭建在上,每个ECS上的OS及kernel版本:Ubuntu 14.04.4 LTS,3.19.0-70-generic。
在我的测试环境,有两个node:master node和一个minion node。master node参与workload的调度。所以你基本可以认为有两个minion node即可。
二、Kubernetes Cluster中的几个“网络”
之前的k8s cluster采用的是默认安装,即直接使用了配置脚本中(kubernetes/cluster/ubuntu/config-default.sh)自带的一些参数,比如:
//摘自kubernetes/cluster/ubuntu/config-default.sh
export nodes=${nodes:-"root@master_node_ip root@minion_node_ip"}
export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-192.168.3.0/24}
export FLANNEL_NET=${FLANNEL_NET:-172.16.0.0/16}
从这里我们能够识别出三个“网络”:
node network:承载kubernetes集群中各个“物理”Node(master和minion)通信的网络;
service network:由kubernetes集群中的Services所组成的“网络”;
flannel network: 即Pod网络,集群中承载各个Pod相互通信的网络。
node network自不必多说,node间通过你的本地局域网(无论是物理的还是虚拟的)通信。
service network比较特殊,每个新创建的service会被分配一个service IP,在当前集群中,这个IP的分配范围是192.168.3.0/24。不过这个IP并不“真实”,更像一个“占位符”并且只有入口流量,所谓的“network”也是“名不符实”的,后续我们会详尽说明。
flannel network是我们要理解的重点,cluster中各个Pod要实现相互通信,必须走这个网络,无论是在同一node上的Pod还是跨node的Pod。我们的cluster中,flannel net的分配范围是:172.16.0.0/16。
在进一步挖掘“原理”之前,我们先来直观认知一下service network和flannel network:
Service network(看cluster-ip一列):
# kubectl get services
CLUSTER-IP
EXTERNAL-IP
192.168.3.168
kubernetes
192.168.3.1
192.168.3.179
192.168.3.196
rbd-rest-api
192.168.3.22
Flannel network(看IP那列):
# kubectl get pod -o wide
my-nginx--gpljv
172.16.99.3
{master node ip}
nginx-kit--rc8hr
172.16.57.7
{minion node ip}
三、平坦的Flannel网络
1、Kubenetes安装后的网络状态
首先让我们来看看:时对各个K8s Node都动了什么手脚!
a) 修改docker default配置
在ubuntu 14.04下,都在/etc/default/docker文件中。如果你曾经修改过该文件,那么kube-up.sh脚本方式安装完kubernetes后,你会发现/etc/default/docker已经变样了,只剩下了一行:
master node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.99.1/24 --mtu=1450"
minion node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.57.1/24 --mtu=1450"
可以看出kube-up.sh修改了Docker daemon的&#8211;bip选项,使得该node上docker daemon在该node的fannel subnet范围以内为启动的Docker container分配IP地址。
b) 在etcd中初始化flannel网络数据
多个node上的Flanneld依赖一个来做集中配置服务,etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化,实时感知集群中node的变化。
我们可以通过etcdctl查询到这些配置数据:
master node:
//flannel network配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get
//network/config
{"Network":"172.16.0.0/16", "Backend": {"Type": "vxlan"}}
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls
//network/subnets
//network/subnets/172.16.99.0-24
//network/subnets/172.16.57.0-24
//某一node上的flanne subnet和vtep配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get
//network/subnets/172.16.99.0-24
{"PublicIP":"{master node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"b6:bf:4c:81:cf:3b"}}
minion node:
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get
//network/subnets/172.16.57.0-24
{"PublicIP":"{minion node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"d6:51:2e:80:5c:69"}}
或用etcd 提供的rest api:
# curl -L http://127.0.0.1:{etcd listen port}/v2//network/config
{"action":"get","node":{"key":"//network/config","value":"{\"Network\":\"172.16.0.0/16\", \"Backend\": {\"Type\": \"vxlan\"}}","modifiedIndex":5,"createdIndex":5}}
c) 启动flanneld
kube-up.sh在每个Kubernetes node上启动了一个flanneld的程序:
# ps -ef|grep flanneld
master node:
00:02:34 /opt/bin/flanneld --etcd-endpoints=http://127.0.0.1:{etcd listen port} --ip-masq --iface={master node ip}
minion node:
00:07:05 /opt/bin/flanneld --etcd-endpoints=http://{master node ip}:{etcd listen port} --ip-masq --iface={minion node ip}
一旦flanneld启动,它将从etcd中读取配置,并请求获取一个subnet lease(租约),有效期目前是24hrs,并且监视etcd的数据更新。flanneld一旦获取subnet租约、配置完backend,它会将一些信息写入/run/flannel/subnet.env文件。
master node:
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.99.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
minion node:
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.57.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
当然flanneld的最大意义在于根据etcd中存储的全cluster的subnet信息,跨node传输flannel network中的数据包,这个后面会详细说明。
d) 创建flannel.1 网络设备、更新路由信息
各个node上的网络设备列表新增一个名为flannel.1的类型为vxlan的网络设备:
master node:
# ip -d link show
4: flannel.1: &BROADCAST,MULTICAST,UP,LOWER_UP& mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether b6:bf:4c:81:cf:3b brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local {master node local ip} dev eth0 port 0 0 nolearning ageing 300
minion node:
349: flannel.1: &BROADCAST,MULTICAST,UP,LOWER_UP& mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local
{minion node local ip} dev eth0 port 0 0 nolearning ageing 300
从flannel.1的设备信息来看,它似乎与eth0存在着某种bind关系。这是在其他bridge、veth设备描述信息中所没有的。
flannel.1设备的ip:
master node:
flannel.1 Link encap:Ethernet
HWaddr b6:bf:4c:81:cf:3b
inet addr:172.16.99.0
Bcast:0.0.0.0
Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST
RX packets:5993274 errors:0 dropped:0 overruns:0 frame:0
TX packets:5829044 errors:0 dropped:292 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes: (1.6 GB)
TX bytes: (1.1 GB)
minion node:
flannel.1 Link encap:Ethernet
HWaddr d6:51:2e:80:5c:69
inet addr:172.16.57.0
Bcast:0.0.0.0
Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST
RX packets:6294640 errors:0 dropped:0 overruns:0 frame:0
TX packets:5755599 errors:0 dropped:25 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes: (989.3 MB)
TX bytes: (1.8 GB)
可以看到两个node上的flannel.1的ip与k8s cluster为两个node上分配subnet的ip范围是对应的。
下面是两个node上的当前路由表:
master node:
# ip route
172.16.0.0/16 dev flannel.1
proto kernel
scope link
src 172.16.99.0
172.16.99.0/24 dev docker0
proto kernel
scope link
src 172.16.99.1
minion node:
# ip route
172.16.0.0/16 dev flannel.1
172.16.57.0/24 dev docker0
proto kernel
scope link
src 172.16.57.1
以上信息将为后续数据包传输分析打下基础。
e) 平坦的flannel network
从以上kubernetes和flannel network安装之后获得的网络信息,我们能看出flannel network是一个flat network。在flannel:172.16.0.0/16这个大网下,每个kubernetes node从中分配一个子网片段(/24):
master node:
--bip=172.16.99.1/24
minion node:
--bip=172.16.57.1/24
root@node1:~# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls
//network/subnets
//network/subnets/172.16.99.0-24
//network/subnets/172.16.57.0-24
用一张图来诠释可能更为直观:
这个是不是有些像x86-64的虚拟内存寻址空间啊(同样是平坦内存地址访问模型)!
在平坦的flannel network中,每个pod都会被分配唯一的ip地址,且每个k8s node的subnet各不重叠,没有交集。不过这样的subnet分配模型也有一定弊端,那就是可能存在ip浪费:一个node上有200多个flannel ip地址(xxx.xxx.xxx.xxx/24),如果仅仅启动了几个Pod,那么其余ip就处于闲置状态。
2、Flannel网络通信原理
这里我们模仿flannel官方的那幅原理图,画了一幅与我们的实验环境匹配的图,作为后续讨论flannel网络通信流程的基础:
如上图所示,我们来看看从pod1:172.16.99.8发出的数据包是如何到达pod3:172.16.57.15的(比如:在pod1的某个container中ping -c 3 172.16.57.15)。
a) 从Pod出发
由于k8s更改了docker的DOCKER_OPTS,显式指定了&#8211;bip,这个值与分配给该node上的subnet的范围是一致的。这样一来,docker引擎每次创建一个Docker container,该container被分配到的ip都在flannel subnet范围内。
当我们在Pod1下的某个容器内执行ping -c 3 172.16.57.15,数据包便开始了它在flannel network中的旅程。
Pod是Kubernetes调度的基本unit。Pod内的多个container共享一个。kubernetes在创建Pod时,首先先创建pause容器,然后再以pause的network namespace为基础,创建pod内的其他容器(&#8211;net=container:xxx),这样Pod内的所有容器便共享一个network namespace,这些容器间的访问直接通过localhost即可。比如Pod下A容器启动了一个服务,监听8080端口,那么同一个Pod下面的另外一个B容器通过访问localhost:8080即可访问到A容器下面的那个服务。
在之前的《》一文中,我相信我已经讲清楚了单机下Docker容器数据传输的路径。在这个环节中,数据包的传输路径也并无不同。
我们看一下Pod1中某Container内的路由信息:
# docker exec ba75f81455c7 ip route
default via 172.16.99.1 dev eth0
172.16.99.0/24 dev eth0
proto kernel
scope link
src 172.16.99.8
目的地址172.16.57.15并不在直连网络中,因此数据包通过default路由出去。default路由的路由器地址是172.16.99.1,也就是上面的docker0 bridge的IP地址。相当于docker0 bridge以“三层的工作模式”直接接收到来自容器的数据包(而并非从bridge的二层端口接收)。
b) docker0与flannel.1之间的包转发
数据包到达docker0后,docker0的内核栈处理程序发现这个数据包的目的地址是172.16.57.15,并不是真的要送给自己,于是开始为该数据包找下一hop。根据master node上的路由表:
master node:
# ip route
172.16.0.0/16 dev flannel.1
proto kernel
scope link
src 172.16.99.0
172.16.99.0/24 dev docker0
proto kernel
scope link
src 172.16.99.1
我们匹配到“172.16.0.0/16”这条路由!这是一条直连路由,数据包被直接送到flannel.1设备上。
c) flannel.1设备以及flanneld的功用
flannel.1是否会重复docker0的套路呢:包不是发给自己,转发数据包?会,也不会。
“会”是指flannel.1肯定要将包转发出去,因为毕竟包不是给自己的(包目的ip是172.16.57.15, vxlan设备ip是172.16.99.0)。
“不会”是指flannel.1不会走寻常套路去转发包,因为它是一个vxlan类型的设备,也称为vtep,virtual tunnel end point。
那么它到底是怎么处理数据包的呢?这里涉及一些Linux内核对vxlan处理的内容,详细内容可参见本文末尾的参考资料。
flannel.1收到数据包后,由于自己不是目的地,也要尝试将数据包重新发送出去。数据包沿着网络协议栈向下流动,在二层时需要封二层以太包,填写目的mac地址,这时一般应该发出arp:”who is 172.16.57.15&#8243;。但vxlan设备的特殊性就在于它并没有真正在二层发出这个arp包,因为下面的这个内核参数设置:
master node:
# cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit
而是由linux kernel引发一个”L3 MISS”事件并将arp请求发到用户空间的flanned程序。
flanned程序收到”L3 MISS”内核事件以及arp请求(who is 172.16.57.15)后,并不会向外网发送arp request,而是尝试从etcd查找该地址匹配的子网的vtep信息。在前面章节我们曾经展示过etcd中Flannel network的配置信息:
master node:
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls
//network/subnets
//network/subnets/172.16.99.0-24
//network/subnets/172.16.57.0-24
# curl -L http://127.0.0.1:{etcd listen port}/v2//network/subnets/172.16.57.0-24
{"action":"get","node":{"key":"//network/subnets/172.16.57.0-24","value":"{\"PublicIP\":\"{minion node local ip}\",\"BackendType\":\"vxlan\",\"BackendData\":{\"VtepMAC\":\"d6:51:2e:80:5c:69\"}}","expiration":"T09:46:20.Z","ttl":21496,"modifiedIndex":2275460,"createdIndex":2275460}}
flanneld从etcd中找到了答案:
subnet: 172.16.57.0/24
public ip: {minion node local ip}
VtepMAC: d6:51:2e:80:5c:69
我们查看minion node上的信息,发现minion node上的flannel.1 设备mac就是d6:51:2e:80:5c:69:
minion node:
#ip -d link show
349: flannel.1: &BROADCAST,MULTICAST,UP,LOWER_UP& mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 10.46.181.146 dev eth0 port 0 0 nolearning ageing 300
接下来,flanned将查询到的信息放入master node host的arp cache表中:
master node:
#ip n |grep 172.16.57.15
172.16.57.15 dev flannel.1 lladdr d6:51:2e:80:5c:69 REACHABLE
flanneld完成这项工作后,linux kernel就可以在arp table中找到 172.16.57.15对应的mac地址并封装二层以太包了。
到目前为止,已经呈现在大家眼前的封包如下图:
不过这个封包还不能在物理网络上传输,因为它实际上只是vxlan tunnel上的packet。
d) kernel的vxlan封包
我们需要将上述的packet从master node传输到minion node,需要将上述packet再次封包。这个任务在backend为vxlan的flannel network中由linux kernel来完成。
flannel.1为vxlan设备,linux kernel可以自动识别,并将上面的packet进行vxlan封包处理。在这个封包过程中,kernel需要知道该数据包究竟发到哪个node上去。kernel需要查看node上的fdb(forwarding database)以获得上面对端vtep设备(已经从arp table中查到其mac地址:d6:51:2e:80:5c:69)所在的node地址。如果fdb中没有这个信息,那么kernel会向用户空间的flanned程序发起”L2 MISS”事件。flanneld收到该事件后,会查询etcd,获取该vtep设备对应的node的”Public IP“,并将信息注册到fdb中。
这样Kernel就可以顺利查询到该信息并封包了:
master node:
# bridge fdb show dev flannel.1|grep d6:51:2e:80:5c:69
d6:51:2e:80:5c:69 dst {minion node local ip} self permanent
由于目标ip是minion node,查找路由表,包应该从master node的eth0发出,这样src ip和src mac地址也就确定了。封好的包示意图如下:
e) kernel的vxlan拆包
minion node上的eth0接收到上述vxlan包,kernel将识别出这是一个vxlan包,于是拆包后将flannel.1 packet转给minion node上的vtep(flannel.1)。minion node上的flannel.1再将这个数据包转到minion node上的docker0,继而由docker0传输到Pod3的某个容器里。
3、Pod内到外部网络
我们在Pod中除了可以与pod network中的其他pod通信外,还可以访问外部网络,比如:
master node:
# docker exec ba75f81455c7 ping -c
(180.149.132.47): 56 data bytes
64 bytes from 180.149.132.47: icmp_seq=0 ttl=54 time=3.586 ms
64 bytes from 180.149.132.47: icmp_seq=1 ttl=54 time=3.752 ms
64 bytes from 180.149.132.47: icmp_seq=2 ttl=54 time=3.722 ms
ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 3.586/3.687/3.752/0.072 ms
这个通信与vxlan就没有什么关系了,主要是通过docker引擎在iptables的POSTROUTING chain中设置的MASQUERADE规则:
mastre node:
#iptables -t nat -nL
Chain POSTROUTING (policy ACCEPT)
prot opt source
destination
MASQUERADE
172.16.99.0/24
docker将容器的pod network地址伪装为node ip出去,包回来时再snat回容器的pod network地址,这样网络就通了。
四、”不真实”的Service网络
每当我们在k8s cluster中创建一个service,k8s cluster就会在&#8211;service-cluster-ip-range的范围内为service分配一个cluster-ip,比如本文开始时提到的:
# kubectl get services
CLUSTER-IP
EXTERNAL-IP
192.168.3.168
kubernetes
192.168.3.1
192.168.3.179
192.168.3.196
rbd-rest-api
192.168.3.22
这个cluster-ip只是一个虚拟的ip,并不真实绑定某个物理网络设备或虚拟网络设备,仅仅存在于iptables的规则中:
Chain PREROUTING (policy ACCEPT)
prot opt source
destination
KUBE-SERVICES
/* kubernetes service portals */
# iptables -t nat -nL|grep 192.168.3
Chain KUBE-SERVICES (2 references)
prot opt source
destination
KUBE-SVC-XGLOHA7QRQ3V22RZ
192.168.3.182
/* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:80
KUBE-SVC-NPX46M4PTMTKRN6Y
192.168.3.1
/* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-AU252PRZZQGOERSG
192.168.3.22
/* default/rbd-rest-api: cluster IP */ tcp dpt:8080
KUBE-SVC-TCOU7JCQXEZGVUNU
192.168.3.10
/* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-BEPXDJBUHFCSYIC3
192.168.3.179
/* default/my-nginx: cluster IP */ tcp dpt:80
KUBE-SVC-UQGS7H
192.168.3.196
/* default/nginx-kit: cluster IP */ tcp dpt:80
KUBE-SVC-ERIFXISQEP7F7OF4
192.168.3.10
/* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
可以看到在PREROUTING环节,k8s设置了一个target: KUBE-SERVICES。而KUBE-SERVICES下面又设置了许多target,一旦destination和dstport匹配,就会沿着chain进行处理。
比如:当我们在pod网络curl 192.168.3.22
8080时,匹配到下面的KUBE-SVC-AU252PRZZQGOERSG target:
KUBE-SVC-AU252PRZZQGOERSG
192.168.3.22
/* default/rbd-rest-api: cluster IP */ tcp dpt:8080
沿着target,我们看到”KUBE-SVC-AU252PRZZQGOERSG”对应的内容如下:
Chain KUBE-SVC-AU252PRZZQGOERSG (1 references)
prot opt source
destination
KUBE-SEP-I6L4LR53UYF7FORX
/* default/rbd-rest-api: */ statistic mode random probability 0.
KUBE-SEP-LBWOKUH4CUTN7XKH
/* default/rbd-rest-api: */
Chain KUBE-SEP-I6L4LR53UYF7FORX (1 references)
prot opt source
destination
KUBE-MARK-MASQ
172.16.99.6
/* default/rbd-rest-api: */
/* default/rbd-rest-api: */ tcp to:172.16.99.6:8080
Chain KUBE-SEP-LBWOKUH4CUTN7XKH (1 references)
prot opt source
destination
KUBE-MARK-MASQ
172.16.99.7
/* default/rbd-rest-api: */
/* default/rbd-rest-api: */ tcp to:172.16.99.7:8080
Chain KUBE-MARK-MASQ (17 references)
prot opt source
destination
MARK or 0x4000
请求被按5:5开的比例分发(起到负载均衡的作用)到KUBE-SEP-I6L4LR53UYF7FORX 和KUBE-SEP-LBWOKUH4CUTN7XKH,而这两个chain的处理方式都是一样的,那就是先做mark,然后做dnat,将service ip改为pod network中的Pod IP,进而请求被实际传输到某个service下面的pod中处理了。
五、参考资料
建议用google翻译将网页从日文翻译成英文再看^0^。
一月 11, 2017
由于2016年年中的原因,对容器网络的研究中断过一段时间。随着当前项目对应用的深入,我感觉之前对于已经不够了,容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始,也是对之前浅表理解的一个补充。
我还是先从容器网络入手,虽然Docker与Kubernetes采用了不同的网络模型:K8s是模型,而Docker则采用的是模型。而要了解Docker容器网络,理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念,并手工模拟Docker容器网络模型的部分实现,包括中的容器与主机连通、容器间连通以及等。
一、Docker的CNM网络模型
Docker通过实现了CNM网络模型。libnetwork设计doc中对CNM模型的简单诠释如下:
CNM模型有三个组件:
Sandbox(沙盒):每个沙盒包含一个容器网络栈(network stack)的配置,配置包括:容器的网口、路由表和DNS设置等。
Endpoint(端点):通过Endpoint,沙盒可以被加入到一个Network里。
Network(网络):一组能相互直接通信的Endpoints。
光看这些,我们还很难将之与现实中的Docker容器联系起来,毕竟是抽象的模型不对应到实体,总有种漂浮的赶脚。文档中又给出了CNM模型在Linux上的参考实现技术,比如:沙盒的实现可以是一个;Endpoint可以是一对;Network则可以用或实现。
这些实现技术反倒是比较接地气。之前我们在使用Docker容器时,了解过Docker是用linux network namespace实现的容器网络隔离的。使用docker时,在物理主机或虚拟机上会有一个docker0的linux bridge,brctl show时能看到 docker0上“插上了”好多veth网络设备:
# ip link show
3: docker0: &BROADCAST,MULTICAST,UP,LOWER_UP& mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff
19: veth4559467@if18: &BROADCAST,MULTICAST,UP,LOWER_UP& mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether a6:14:99:52:78:35 brd ff:ff:ff:ff:ff:ff link-netnsid 3
$ brctl show
bridge name
STP enabled
interfaces
veth4559467
模型与现实终于有点接驳了!下面我们将进一步深入对这些术语概念的理解。
二、Linux Bridge、VETH和Network Namespace
,即Linux网桥设备,是Linux提供的一种虚拟网络设备之一。其工作方式非常类似于物理的网络交换机设备。Linux Bridge可以工作在二层,也可以工作在三层,默认工作在二层。工作在二层时,可以在同一网络的不同主机间转发以太网报文;一旦你给一个Linux Bridge分配了IP地址,也就开启了该Bridge的三层工作模式。在Linux下,你可以用工具包或brctl命令对Linux bridge进行管理。
VETH(Virtual Ethernet )是Linux提供的另外一种特殊的网络设备,中文称为虚拟网卡接口。它总是成对出现,要创建就创建一个pair。一个Pair中的veth就像一个网络线缆的两个端点,数据从一个端点进入,必然从另外一个端点流出。每个veth都可以被赋予IP地址,并参与三层网络路由过程。
关于Linux Bridge和VETH的具体工作原理,可以参考IBM developerWorks上的这篇文章《》。
Network namespace,网络名字空间,允许你在Linux创建相互隔离的网络视图,每个网络名字空间都有独立的网络配置,比如:网络设备、路由表等。新建的网络名字空间与主机默认网络名字空间之间是隔离的。我们平时默认操作的是主机的默认网络名字空间。
概念总是抽象的,接下来我们将在一个模拟Docker容器网络的例子中看到这些Linux网络概念和网络设备到底是起到什么作用的以及是如何操作的。
三、用Network namespace模拟Docker容器网络
为了进一步了解network namespace、bridge和veth在docker容器网络中的角色和作用,我们来做一个demo:用network namespace模拟Docker容器网络,实际上Docker容器网络在linux上也是基于network namespace实现的,我们只是将其“自动化”的创建过程做成了“分解动作”,便于大家理解。
我们在一台物理机上进行这个Demo实验。物理机安装了Ubuntu 16.04.1,内核版本:4.4.0-57-generic。Docker容器版本:
API version:
Go version:
Git commit:
Thu Aug 18 05:33:38 2016
linux/amd64
API version:
Go version:
Git commit:
Thu Aug 18 05:33:38 2016
linux/amd64
另外,环境中需安装了和brctl工具。
我们来模拟一个拥有两个容器的容器桥接网络:
对应的用手工搭建的模拟版本拓扑如下(由于在同一台主机,模拟版本采用172.16.0.0/16网段):
3、创建步骤
a) 创建Container_ns1和Container_ns2 network namespace
默认情况下,我们在Host上看到的都是default network namespace的视图。为了模拟容器网络,我们新建两个network namespace:
sudo ip netns add Container_ns1
sudo ip netns add Container_ns2
$ sudo ip netns list
Container_ns2
Container_ns1
创建的ns也可以在/var/run/netns路径下看到:
$ sudo ls /var/run/netns
Container_ns1
Container_ns2
我们探索一下新创建的ns的网络空间(通过ip netns exec命令可以在特定ns的内部执行相关程序,这个exec命令是至关重要的,后续还会发挥更大作用):
$ sudo ip netns exec Container_ns1 ip a
1: lo: &LOOPBACK& mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
$ sudo ip netns exec Container_ns2 ip a
1: lo: &LOOPBACK& mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
$ sudo ip netns exec Container_ns2 ip route
可以看到,新建的ns的网络设备只有一个loopback口,并且路由表为空。
b) 创建MyDocker0 bridge
我们在default network namespace下创建MyDocker0 linux bridge:
$ sudo brctl addbr MyDocker0
$ brctl show
bridge name
STP enabled
interfaces
给MyDocker0分配ip地址并生效该设备,开启三层,为后续充当Gateway做准备:
$ sudo ip addr add 172.16.1.254/16 dev MyDocker0
$ sudo ip link set dev MyDocker0 up
启用后,我们发现default network namespace的路由配置中增加了一条路由:
$ route -n
内核 IP 路由表
10.11.36.1
172.16.0.0
255.255.0.0
0 MyDocker0
c) 创建VETH,连接两对network namespaces
到目前为止,default ns与Container_ns1、Container_ns2之间还没有任何瓜葛。接下来就是见证奇迹的时刻了。我们通过veth pair建立起多个ns之间的联系:
创建连接default ns与Container_ns1之间的veth pair &#8211; veth1和veth1p:
$sudo ip link add veth1 type veth peer name veth1p
$sudo ip -d link show
21: veth1p@veth1: &BROADCAST,MULTICAST,M-DOWN& mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff promiscuity 0
veth addrgenmode eui64
22: veth1@veth1p: &BROADCAST,MULTICAST,M-DOWN& mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 56:cd:bb:f2:10:3f brd ff:ff:ff:ff:ff:ff promiscuity 0
veth addrgenmode eui64
将veth1“插到”MyDocker0这个bridge上:
$ sudo brctl addif MyDocker0 veth1
$ sudo ip link set veth1 up
$ brctl show
bridge name
STP enabled
interfaces
8000.56cdbbf2103f
将veth1p“放入”Container_ns1中:
$ sudo ip link set veth1p netns Container_ns1
$ sudo ip netns exec Container_ns1 ip a
1: lo: &LOOPBACK& mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: veth1p@if22: &BROADCAST,MULTICAST& mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0
这时,你在default ns中将看不到veth1p这个虚拟网络设备了。按照上面拓扑,位于Container_ns1中的veth应该更名为eth0:
$ sudo ip netns exec Container_ns1 ip link set veth1p name eth0
$ sudo ip netns exec Container_ns1 ip a
1: lo: &LOOPBACK& mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: eth0@if22: &BROADCAST,MULTICAST& mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0
将Container_ns1中的eth0生效并配置IP地址:
$ sudo ip netns exec Container_ns1 ip link set eth0 up
$ sudo ip netns exec Container_ns1 ip addr add 172.16.1.1/16 dev eth0
赋予IP地址后,自动生成一条直连路由:
sudo ip netns exec Container_ns1 ip route
172.16.0.0/16 dev eth0
proto kernel
scope link
src 172.16.1.1
现在在Container_ns1下可以ping通MyDocker0了,但由于没有其他路由,包括默认路由,ping其他地址还是不通的(比如:docker0的地址:172.17.0.1):
$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.254
PING 172.16.1.254 (172.16.1.254) 56(84) bytes of data.
64 bytes from 172.16.1.254: icmp_seq=1 ttl=64 time=0.074 ms
64 bytes from 172.16.1.254: icmp_seq=2 ttl=64 time=0.064 ms
64 bytes from 172.16.1.254: icmp_seq=3 ttl=64 time=0.068 ms
--- 172.16.1.254 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.064/0.068/0.074/0.010 ms
$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
connect: Network is unreachable
我们再给Container_ns1添加一条默认路由,让其能ping通物理主机上的其他网络设备或其他ns空间中的网络设备地址:
$ sudo ip netns exec Container_ns1 ip route add default via 172.16.1.254
$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0
proto kernel
scope link
src 172.16.1.1
$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.068 ms
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.076 ms
64 bytes from 172.17.0.1: icmp_seq=3 ttl=64 time=0.069 ms
--- 172.17.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.068/0.071/0.076/0.003 ms
不过这时候,如果想在Container_ns1中ping通物理主机之外的地址,比如:,那还是不通的。为什么呢?因为ping的icmp的包的源地址没有做snat(docker是通过设置规则实现的),导致出去的以172.16.1.1为源地址的包“有去无回”了^0^。
接下来,我们按照上述步骤,再创建连接default ns与Container_ns2之间的veth pair &#8211; veth2和veth2p,由于步骤相同,这里就不列出那么多信息了,只列出关键操作:
$ sudo ip link add veth2 type veth peer name veth2p
$ sudo brctl addif MyDocker0 veth2
$ sudo ip link set veth2 up
$ sudo ip link set veth2p netns Container_ns2
$ sudo ip netns exec Container_ns2 ip link set veth2p name eth0
$ sudo ip netns exec Container_ns2 ip link set eth0 up
$ sudo ip netns exec Container_ns2 ip addr add 172.16.1.2/16 dev eth0
$ sudo ip netns exec Container_ns2 ip route add default via 172.16.1.254
至此,模拟创建告一段落!两个ns之间以及它们与default ns之间连通了!
$ sudo ip netns exec Container_ns2 ping -c 3 172.16.1.1
PING 172.16.1.1 (172.16.1.1) 56(84) bytes of data.
64 bytes from 172.16.1.1: icmp_seq=1 ttl=64 time=0.101 ms
64 bytes from 172.16.1.1: icmp_seq=2 ttl=64 time=0.083 ms
64 bytes from 172.16.1.1: icmp_seq=3 ttl=64 time=0.087 ms
--- 172.16.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.083/0.090/0.101/0.010 ms
$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.2
PING 172.16.1.2 (172.16.1.2) 56(84) bytes of data.
64 bytes from 172.16.1.2: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.092 ms
64 bytes from 172.16.1.2: icmp_seq=3 ttl=64 time=0.089 ms
--- 172.16.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.053/0.078/0.092/0.017 ms
当然此时两个ns之间连通,主要还是通过直连网络,实质上是MyDocker0在二层起到的作用。以在Container_ns1中ping Container_ns2的eth0地址为例:
Container_ns1此时的路由表:
$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0
proto kernel
scope link
src 172.16.1.1
ping 172.16.1.2执行后,根据路由表,将首先匹配到直连网络(第二条),即无需gateway转发便可以直接将数据包送达。arp查询后(要么从arp cache中找到,要么在MyDocker0这个二层交换机中泛洪查询)获得172.16.1.2的mac地址。ip包的目的ip填写172.16.1.2,二层数据帧封包将目的mac填写为刚刚查到的mac地址,通过eth0(172.16.1.1)发送出去。eth0实际上是一个veth pair,另外一端“插”在MyDocker0这个交换机上,因此这一过程就是一个标准的二层交换机的数据报文交换过程, MyDocker0相当于从交换机上的一个端口收到以太帧数据,并将数据从另外一个端口发出去。ping应答包亦如此。
而如果是在Container_ns1中ping某个docker container的地址,比如172.17.0.2。当ping执行后,根据Container_ns1下的路由表,没有匹配到直连网络,只能通过default路由将数据包发给Gateway: 172.16.1.254。虽然都是MyDocker0接收数据,但这次更类似于“数据被直接发到 Bridge 上,而不是Bridge从一个端口接收(这块儿与我之前的文章中的理解稍有差异)”。二层的目的mac地址填写的是gateway 172.16.1.254自己的mac地址(Bridge的mac地址),此时的MyDocker0更像是一块普通网卡的角色,工作在三层。MyDocker0收到数据包后,发现并非是发给自己的ip包,通过主机路由表找到直连链路路由,MyDocker0将数据包Forward到docker0上(封装的二层数据包的目的MAC地址为docker0的mac地址)。此时的docker0也是一种“网卡”的角色,由于目的ip依然不是docker0自身,因此docker0也会继续这一转发流程。通过traceroute可以印证这一过程:
$ sudo ip netns exec Container_ns1
traceroute 172.17.0.2
traceroute to 172.17.0.2 (172.17.0.2), 30 hops max, 60 byte packets
172.16.1.254 (172.16.1.254)
172.17.0.2 (172.17.0.2)
$ sudo ip netns exec Container_ns1
ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=63 time=0.084 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.101 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=63 time=0.098 ms
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.084/0.094/0.101/0.010 ms
现在,你应该大致了解docker engine在创建单机容器网络时都在背后做了哪些手脚了吧(当然,这里只是简单模拟,docker实际做的要比这复杂许多)。
四、基于userland proxy的容器端口映射的模拟
让位于容器中的service可以将服务范围扩展到主机之外,比如:一个运行于container中的可以通过宿主机的9091端口对外提供http server服务:
$ sudo docker run -d -p 9091:80 nginx:latest
8eef60e3d7b24eebefabd5730ac2f
$ curl 10.11.36.15:9091
&!DOCTYPE html&
&title&Welcome to nginx!&/title&
font-family: Tahoma, Verdana, Arial, sans-
&h1&Welcome to nginx!&/h1&
&p&If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&/p&
&p&For online documentation and support please refer to
&a href="http://nginx.org/"&nginx.org&/a&.&br/&
Commercial support is available at
&a href="/"&&/a&.&/p&
&p&&em&Thank you for using nginx.&/em&&/p&
容器的端口映射实际是通过docker engine的docker proxy功能实现的。默认情况下,docker engine(截至docker 1.12.1版本)采用userland proxy(&#8211;userland-proxy=true)为每个expose端口的容器启动一个proxy实例来做端口流量转发:
$ ps -ef|grep docker-proxy
00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9091 -container-ip 172.17.0.2 -container-port 80
docker-proxy实际上就是在default ns和container ns之间转发流量而已。我们完全可以模拟这一过程。
我们创建一个fileserver demo:
//testfileserver.go
package main
import "net/http"
func main() {
http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))
我们在Container_ns1下启动这个Fileserver service:
$ sudo ip netns exec Container_ns1 ./testfileserver
$ sudo ip netns exec Container_ns1 lsof -i tcp:8080
TYPE DEVICE SIZE/OFF NODE NAME
testfiles 3605 root
IPv4 297022
TCP *:http-alt (LISTEN)
可以看到在Container_ns1下面,8080已经被testfileserver监听,不过在default ns下,8080端口依旧是avaiable的。
接下来,我们在default ns下创建一个简易的proxy:
//proxy.go
containerport string
func main() {
flag.StringVar(&host, "host", "0.0.0.0", "host addr")
flag.StringVar(&port, "port", "", "host port")
flag.StringVar(&container, "container", "", "container addr")
flag.StringVar(&containerport, "containerport", "8080", "container port")
flag.Parse()
fmt.Printf("%s\n%s\n%s\n%s", host, port, container, containerport)
ln, err := net.Listen("tcp", host+":"+port)
if err != nil {
// handle error
log.Println("listen error:", err)
log.Println("listen ok")
conn, err := ln.Accept()
if err != nil {
// handle error
log.Println("accept error:", err)
log.Println("accept conn", conn)
go handleConnection(conn)
func handleConnection(conn net.Conn) {
cli, err := net.Dial("tcp", container+":"+containerport)
if err != nil {
log.Println("dial error:", err)
log.Println("dial ", container+":"+containerport, " ok")
go io.Copy(conn, cli)
_, err = io.Copy(cli, conn)
fmt.Println("communication over: error:", err)
在default ns下执行:
./proxy -host 0.0.0.0 -port 9090 -container 172.16.1.1 -containerport 8080
172.16.1.1
/11 17:26:10 listen ok
我们http get一下宿主机的9090端口:
$curl 10.11.36.15:9090
&a href="proxy"&proxy&/a&
&a href="proxy.go"&proxy.go&/a&
&a href="testfileserver"&testfileserver&/a&
&a href="testfileserver.go"&testfileserver.go&/a&
成功获得file list!
proxy的输出日志:
17:26:16 accept conn &{{0xc}}
17:26:16 dial
172.16.1.1:8080
communication over: error:&nil&
由于每个做端口映射的Container都要启动至少一个docker proxy与之配合,一旦运行的container增多,那么docker proxy对资源的消耗将是大大的。因此docker engine在docker 1.6之后(好像是这个版本)提供了基于iptables的端口映射机制,无需再启动docker proxy process了。我们只需修改一下docker engine的启动配置即可:
在使用systemd init system的系统中如果为docker engine配置&#8211;userland-proxy=false,可以参考《》这篇文章。
由于这个与network namespace关系不大,后续单独理解^0^。
六、参考资料
这里是的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片。
如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!
如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
著名主机提供商Linode 10$优惠码:linode10,在即可免费获得。
阿里云推荐码:1WFZ0V,立享9折!

我要回帖

更多关于 golang struct 初始化 的文章

 

随机推荐