这篇文章会随时更新
镜像源 1 2 go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct
1 2 3 4 5 6 7 8 go install -v golang.org/x/tools/gopls@latest go install -v github.com/cweill/gotests/gotests@latest go install -v github.com/fatih/gomodifytags@latest go install -v github.com/josharian/impl@latest go install -v github.com/haya14busa/goplay/cmd/goplay@latest go install -v github.com/go-delve/delve/cmd/dlv@latest go install -v honnef.co/go/tools/cmd/staticcheck@latest go install -v golang.org/x/tools/cmd/godoc@latest
vscode调试golang程序 launch.json
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "version" : "0.2.0" , "configurations" : [ { "name" : "Launch Package" , "type" : "go" , "request" : "launch" , "mode" : "debug" , "program" : "${workspaceFolder}/cmd/<app name>" , "env" : { "GOARCH" : "amd64" , "DEBUG" : "true" , } , "args" : [ ] , "showLog" : true } ] }
golang 项目结构布局 golang 项目基本布局可以参考Standard Go Project Layout ,尽管非官方标准,是社区总结的,可以参考这个布局来安排自己项目的库代码、私有代码、主干
基本规则:
go 文件命名不使用驼峰法,而是以 _
下划线划分单词 golang 项目中的每个 go 文件都必定从属于某个package 同一目录层级下的所有 go 文件应该处于同一个 package 中 ,不允许存在不同名的 package,否则会出现编译错误,也不是种好的设计习惯 对于上一条规则,有一个例外,那就是包的测试代码、示例代码,这些代码与包在同一个目录,包名规则是 <package name>_test
,文件名规则分别为 <测试目标>_test.go
、example_<目标-可选>_test.go
。总结本条和上一条规则,换句话说,对于所有处于同一个文件夹内的非测试、非示例代码,必须使用同一个包名 项目中每个目录层级下使用的 package 名应该与文件夹同名 (项目根目录情况不同) 如果项目是作为一个库发布 的话,项目根目录层级 下的 package 不能为 main,这种情况最好 pakcage 与项目同名 如果希望项目可以作为一个库发布 ,也可以做为独立可执行文件发布 的话,可以将 main 包,放在 cmd/<项目名>/main.go
或 cli/cmd/<项目名>/main.go
如果仅作为独立可执行文件发布 的话,可以考虑直接将 main 包放在项目根目录 import
的基本规则:
以目录结构的格式 import
import
其它第三方项目的包时,最好仅 import
其 pkg
的包,因为这是显式指示为公开的包不能 import
其它项目的 main
、internal
、cli
、cmd
等包 对于仅需要包内初始化(init
),而不用直接使用的包,可以匿名方式 import
,类似 import _ "foo.com/bar"
例子 项目仅以库形式发布 1 2 3 4 5 6 7 8 9 # github.com/username/xpack # 目录结构 ./pkg/handler # package pkg/handler ./go.mod ./pack_unpack.go # package xpack # import import "github.com/username/xpack" import "github.com/username/xpack/handler"
项目以库和独立可执行文件形式发布 1 2 3 4 5 6 7 8 9 10 # github.com/username/xpack # 目录结构 ./cli/cmd/xpack/main.go ./pkg/handler # package pkg/handler ./go.mod ./pack_unpack.go # package xpack # import import "github.com/username/xpack" import "github.com/username/xpack/handler"
项目以多个独立可执行文件形式发布 1 2 3 4 5 6 7 # github.com/username/xpack # 目录结构 ./cli/cmd/xpack/main.go ./cli/cmd/xunpack/main.go # or ./cmd/xpack/main.go ./cmd/xunpack/main.go
使用自定义包 有两种方式:
禁用 GoModule
时,将项目安排在 GOPATH
或 GOROOT
中 Go v1.11 之后新版本支持 GoModule
,默认启用,最好使用这种方式 引用放置在 GOPATH 中的自定义包 最好别用这种方法,应该使用 GoModule
对于标准库的包,编译器会从 GOROOT
目录中寻找,而非标准库的包在 GoModule
未启用(即 GO111MODULE="on"
)时,编译器会从 GOPATH
目录中寻找,利用这个特性,可以将自定义的包安排在 GOPATH
中。
首先需要确认 GOPATH
的位置,需要注意的是,不应该为了自己项目的安排而去修改 GOPATH
,比较好的做法是将自定义项目放进这里来
1 2 3 4 5 6 7 8 9 mkdir packcd packgo mod init github.com/username/pack cd ..mkdir -p $GOPATH /src/github.com/usernamemv pack/ $GOPATH /src/github.com/username
将项目按包名结构拷贝进 $GOPATH/src
中后,旧可以在项目中使用包名来 import
这个项目了
使用 GoModule 的方式来引用自定义包 1 2 go env -w GO111MODULE='on'
那么对于本项目的使用,对于程序员得角度,可以忽略掉 GOPATH
了,项目可以存放在任意可访问的位置中,在新项目中,启用 GoModule
是更好的做法,项目的依赖关系由 go.mod
和 go.sum
来管理。GOPATH
并不是没有用了,只是交给编译器自己安排,拉回来的库,编译器会缓存在 $GOPATH/pkg/mod
中。
可以用 go mod edit ...
命令来修改 go.mod
,最好不要手动去编辑 go.mod
模块必须指定一个版本号,对于 git 仓库,版本号与 tag 绑定,模式为 v<major_number>.<minor_number>.<patch_number>
引用本地自定义库 1 2 3 4 5 6 7 8 9 10 go mod edit --require github.com/example/[email protected] go mod edit --replace github.com/example/foobar=../foobar cat go.mod
1 2 3 4 5 6 7 8 9 file: go .mod module example.com/pkgname go 1.20 require github.com/example/foobar v0.1 .0 replace github.com/example/foobar => ../foobar
这时候可以直接用包名引用模块了,由于是在本地,所以不会为 foobar 在 go.sum
中添加条目,不过这种做法最好是用于本地开发或测试场景,不建议在生产环境中使用
公有仓库发布 对于上面本地的例子,如果认为该库足够稳定可以发布了,那么可以为其按照 v<major_number>.<minor_number>.<patch_number>
模式打一个 tag
并推送到远程公有仓库中,这样就可以直接通过 go get github.com/example/foobar
来将模块拉回来了,go
会将模块缓存在 $GOPATH/pkg/mod/github.com/example/foobar
中,并在 go.sum
中添加条目。
私有仓库发布 如果希望推送到私有仓库中,基本操作与公有仓库发布方式一样,首先还是以版本号打tab
并推送到远程中,但是现在 go get
是访问不了私人仓库的,需要做一些额外的操作。
这里以 github
的仓库为例,首先确保本机的 ssh
公钥已经加入到 github
账号中,然后修改本机的 git
配置
可参考:Why does "go get" use HTTPS when cloning a repository?
这时候已经可以 go get
私人仓库了,但是在执行时需要指明 GOPRIVATE
环境变量,显式让 go get
知道哪些包来自私人仓库,有临时指定和持久修改两种做法:
1 2 3 4 5 6 7 8 9 10 11 12 GOPRIVATE=github.com/example/foobar go get -u github.com/example/foobar GOPRIVATE=github.com/12CrazyPaul21/xblg go mod tidy GOPRIVATE="github.com/username/pirv2,github.com/username/pirv1" go mod tidy GOPRIVATE="github.com/username/*" go mod tidy
1 2 go env -w GOPRIVATE='github.com/username/*,gitlab.com/username/*'
golang 注释风格 golang 的注释风格推崇简洁,尽可能仅留下必要的注释,公开内容尽量给出比较详细的注释,私有内容可随意些。
golang 的注释规范可以参考(也可以直接参考golang的源码):
包文档注释最好使用 /**/
,而其它注释使用 //
私有内容的注释不会出现在库文档中 example 也会包含在文档中,但不会包含 test 分行使用空白注释行分隔,否则会被当做同一行处理 以 tab
开始代码块(block) 可以使用 #
来开始一个标题(前后需要插入空白行) [path/filepath]
可以创建指定特定包的 link
[package_name.xxx]
可以创建指向特定包的某个成员的 link
超链接可以直接插入 -
可以插入无序列表(前面加入一个 tab
),改为 数字.
的话,则为有序列表例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 file: ./pack/handler.go package packconst ( C1 = 0 C2 ) const ( C3 = iota C4 ) var ( V1 int = 0 V2 string = "public V2" ) type private_struct struct {} type Handler struct {} func (h Handler) Hello() {} func (h Handler) Handle() {} func (h *Handler) GiveUp() {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 file: ./pack/example_test.go package pack_testimport ( "testing" . "phantom.com/tdoc/pack" ) func ExampleHandler () { h := Handler{} h.Hello() } func ExampleHandler_GiveUp () { h := Handler{} h.GiveUp() } func TestHandle (t *testing.T) { h := Handler{} h.Handle() }
godoc
的输出如下(这里展现不出实际的效果):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 type Handler Handler是公开的 独立行 // 代码段 h := Handler{} 结束代码段 标题 h := Handler{} type Handler struct { } ▾ Example Example<类型>_<字段> Code: h := Handler{} h.Hello() func (*Handler) GiveUp func (h *Handler) GiveUp() give up ▾ Example Handler.GiveUp's example Code: // give up example h := Handler{} h.GiveUp() func (Handler) Handle func (h Handler) Handle() 这是一个无序列表 条目1 条目2 这是一个有序列表 Item 1. Item 2. Item 3. func (Handler) Hello func (h Handler) Hello() say hello
package 文档注释 邻接在 package <name>
前面的注释(与 package <name>
之间没有空白行)为package
的文档注释。如果有 copyright
描述或者 //go:<xxx>
指示器的话,使用空白行跟文档注释分隔。 如果内容比较多的话,可以在包文件夹内用一个独立的 doc.go
文件来存放该包的文档注释 如果包内其它 go 文件也存在包文档注释,那么会根据文件名的字母顺序汇总 起来,不过最好不要对汇总的顺序做假设,如果存在 doc.go
的话,就最好不要在其它 go 文件中添加包文档注释 包文档注释的概要(synopsis) 采用文档注释的第一行 文档注释使用一个独立的空白行 注释来分行 例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 file: ./pack/doc.go package pack
1 2 3 4 5 6 7 8 9 file: ./pack/e.go package pack
1 2 3 4 5 6 7 8 9 file: ./pack/f.go package pack
通过 godoc
查看上面这个包的文档注释,能看到概要(Synopsis)是下面这样的:
1 pack pack包的文档注释(Overview)开头,这段属于概要(Synopsis),直接用//也是可以的
概述(Overview)则是下面这样(这里展现不出实际的效果):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 pack包的文档注释(Overview)开头,这段属于概要(Synopsis),直接用//也是可以的 //和/**/可以混用 但是它们之间不能出现空白行 不过上面这种空白的注释行是没问题的 上面这样使用/**/的空白行也行 hello world 上面两行在文档注释中跟本段处于同一行,如果需要分行,用用空白行注释行分隔 hello world hello world doc.go内的文档注释 标题1 内容 标题2 内容 同为pack包内e.go文件的包文档注释,因为文件名的字母顺序在doc.go后面,所以接在doc.go的后面 同为pack包内f.go文件的包文档注释,因为文件名的字母顺序在e.go后面,所以接在e.go的后面
go doc godoc 1 2 godoc -http=localhost:6060
Example 与 Test example
和 test
的写法类似,都是在写在特定包文件夹内
文件名以 _test.go
后缀结尾 对于 example
用例的文件名,最好使用 example_<补充>_test.go
格式,test
用例则可以针对特定目标命名 使用的包名是 <原始包名>_test
test
方法以 Test
开头,测试工具会自动识别方法名以 Example
开头的自动识别为 example
example
方法的命名格式是 Example<类型>_<字段>
,这主要供 doc
自动在文档上跟目标进行绑定1 2 3 4 5 6 7 8 9 file: ./pack/handler.go package packtype Handler struct {}func (h Handler) Hello() {}func (h Handler) Handle() {}func (h *Handler) GiveUp() {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 file: ./pack/exmaple_test.go 或者 ./pack/example_handler_test.go package pack_testimport . "phantom.com/tdoc/pack" func ExampleHandler () { h := Handler{} h.Hello() } func ExampleHandler_GiveUp () { h := Handler{} h.GiveUp() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 file: ./pack/handler_test.go package pack_testimport ( "testing" . "phantom.com/tdoc/pack" ) func TestHandler (t *testing.T) { h := Handler{} h.Handle() }
1 2 3 4 go test ./pack go test ./...
init 执行顺序 从 main
包为入口,会先递归执行完所有的 import
指令,每次递归会完成 包常量
、包变量
、包函数
、包init()
初始化,当回到 main
包之后,也是按照这个顺序初始化,之后进入到 main.main
中。
如果途中有重复的包,则只 import
一次 如果同一个包中,有多个 go
文件有 init
方法,则按照 go
文件名的字母顺序调用 可以通过 GODEBUG=inittrace=1 go run .
分析 init
顺序 golang init 很经典的一张图,from:《init() in Go Programming》 例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package packimport "fmt" const p_a string = "pack a init" func ainfo () { fmt.Println(p_a) } func init () { ainfo() } package packimport "fmt" const p_b string = "pack b init" func binfo () { fmt.Println(p_b) } func init () { binfo() } package packimport "fmt" const p_c string = "pack c init" func cinfo () { fmt.Println(p_c) } func init () { cinfo() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package mainimport "fmt" const m_a string = "main a init" func ainfo () { fmt.Println(m_a) } func init () { ainfo() } package mainimport "fmt" const m_f string = "main f init" func finfo () { fmt.Println(m_f) } func init () { finfo() } package mainimport ( "fmt" _ "phantom.com/tinit/pack" ) const m_m string = "main main init" func maininfo () { fmt.Println(m_m) } func init () { maininfo() } func main () { fmt.Println("main" ) }
命名函数返回值 1 2 3 4 5 6 7 8 9 10 func generate () (name string ) { name = "hello" return } func generate () (id int , name string ) { id = 1 name = "hello" return }
有缓冲通道与无缓冲通道 1 2 3 4 5 unbuffered := make (chan int ) buffered := make (chan int , 1 )
无缓冲通道在初始化后,缓冲区是空的,对于这种通道,golang 会保证一对发送者和接收者,在就绪后同一时间进行数据交换,否则一方处于阻塞中,而有缓冲通道则没有这种保证
单向 channel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func send (ch chan <- int , n int ) { ch <- n } func receive (ch <-chan int ) int { return <- ch } func main () { ch := make (chan int ) var sender chan <- int = ch var receiver <-chan int = ch go func () { send(sender, 2 ) }() fmt.Println(receive(receiver)) }
单向 channel 常见的地方,如定时器
1 2 3 4 5 6 7 8 9 func main () { var timeout <-chan time.Time = time.After(2 * time.Second) select { case <- timeout: fmt.Println("timeout" ) } }
select 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "fmt" "os" "os/signal" "time" ) var ( interrupt chan os.Signal timeout <-chan time.Time complete chan string ) func main () { interrupt = make (chan os.Signal, 1 ) timeout = time.After(3 * time.Second) complete = make (chan string ) signal.Notify(interrupt, os.Interrupt) go func () { var input string fmt.Printf("what's your first name: " ) fmt.Scanln(&input) complete <- input }() select { case <-interrupt: fmt.Println("\ninterrupt" ) case <-timeout: fmt.Println("\ntimeout" ) case str := <-complete: fmt.Println("your first name:" , str) } }
WaitGroup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "sync" "time" ) func main () { w := sync.WaitGroup{} for i := 0 ; i < 5 ; i++ { w.Add(1 ) go func (i int , w *sync.WaitGroup) { defer w.Done() time.Sleep(time.Second) fmt.Println(i) }(i, &w) } w.Wait() }
灵活的switch 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 switch generate() {case 1 : fmt.Println("1" ) case 10 : fmt.Println("10" ) fallthrough case 11 : fmt.Println("11" ) } switch generate() {case 1 , 10 , 13 : fmt.Println("1 or 10 or 13" ) default : fmt.Println("unknwon" ) } var i interface {}i = "hello" switch i.(type ) {case string : fmt.Println("string" ) } d := 10 s := "hello" switch {case s == "world" : fmt.Println("s == world" ) case d < 10 : fmt.Println("d < 10" ) case d > 10 : fmt.Println("d > 10" ) default : fmt.Println("d == 10" ) }
值接收器与指针接收器 1 2 3 4 5 6 7 type human struct {}func (h human) speak() {}func (h *human) notify() {}
接收器(receiver)是与类型相关的方法进行绑定的隐式传送的参数,值接收器传送的是类型值的副本,而指针接收器则传送的是本体的指针
方法集(method sets) 方法集(method sets)定义了一组关联到 给定类型的值 或指针 的方法 ,方法在定义时使用的接收器(receiver)决定了该方法关联的是值、指针或者两者都关联。
golang 规范定义的关联规则如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import ( "fmt" "reflect" ) type A struct {}func (a A) ValueMethod() {}func (a *A) PointerMethod() {}func printMethodSet (v interface {}) { t := reflect.TypeOf(v) fmt.Printf("%s method set count : %d\n" , t.String(), t.NumMethod()) for i := 0 ; i < t.NumMethod(); i++ { m := t.Method(i) fmt.Printf(" %s\n" , m.Name) } } func main () { printMethodSet(A{}) printMethodSet(&A{}) }
接口(interface)与方法集 golang 的接口是非侵入式的,也没有限定使用的是值接收器还是指针接受器,只需要接口定义的方法出现在类型的方法集中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type A struct {}func (a A) f1() {}func (a *A) f2() {}type I1 interface { f1() } type I2 interface { f2() } func callI1 (i1 I1) {}func callI2 (i2 I2) {}func main () { callI1(A{}) callI1(&A{}) callI2(&A{}) }
嵌入类型(type embedding) 嵌入(匿名)类型有嵌入值类型 和嵌入指针类型 两种方式,嵌入者称为外部类型 ,被嵌入者称为内部类型 ,同一个内部类型不能同时以两种方式进行嵌入。
嵌入后等于将内部类型的方法或变量也嵌入到了外部类型中,这个过程称为内部类型的提升(promotion) 。内部类型的方法(promoted method)被调用时,这些内部类型方法的接收器仍然是内部类型 ,而非外部类型。不过需要注意,对于这两种嵌入方式,它们方法集(method sets)的提升规则是不一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Com struct {}type A struct { Com } type B struct { *Com } type C struct { com Com }
不同点 初始化方式不一样 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type A struct { v string } type B struct { A } type C struct { *A } func main () { b := B{} c := C{&A{}} c.A = &A{} ec := C{} ec.v = "hello" }
内部类型方法集提升规则不一样 提升之后的方法可加入方法集,这部分对于接口的适用范围与普通类型是一样的
S T (t T) *S T (t T) and (t *T) S *T (t T) and (t *T) *S *T (t T) and (t *T)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import ( "fmt" "reflect" ) type A struct {}func (a A) ValueMethod() {}func (a *A) PointerMethod() {}type B struct { A } type C struct { *A } func printMethodSet (v interface {}) { t := reflect.TypeOf(v) fmt.Printf("%s method set count : %d\n" , t.String(), t.NumMethod()) for i := 0 ; i < t.NumMethod(); i++ { m := t.Method(i) fmt.Printf(" %s\n" , m.Name) } } func main () { printMethodSet(B{}) printMethodSet(&B{}) printMethodSet(C{}) printMethodSet(&C{}) }
相同点 对于内部类型字段的访问方式一样 假设内部类型与外部类型没有重名字段,那么可以直接通过外部类型来访问内部类型的变量或者访问(编译器会自动处理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 type A struct { s string } func (a A) f1() {}func (a *A) f2() {}type B struct { A } type C struct { *A } func main () { b := B{A{}} c := C{&A{}} b.s = "" b.f1() b.f2() c.s = "" c.f1() c.f2() }
相同点:对于内部与外部有重名字段的访问方式一样 两种嵌入方式,如果内部类型与外部类型有重名字段,那么内部类型的字段会被屏蔽,要想访问的话需要显式引用内部类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 type A struct { v string } func (a A) f1() { fmt.Println("A f1" ) } func (a *A) f2() { fmt.Println("A f2" ) } type B struct { A v string } func (b B) f1() { fmt.Println("B f1" ) } func (b *B) f2() { fmt.Println("B f2" ) } type C struct { *A v string } func (c C) f1() { fmt.Println("C f1" ) } func (c *C) f2() { fmt.Println("C f2" ) } func main () { b := B{A{v: "A v" }, "B v" } c := C{&A{v: "A v" }, "C v" } fmt.Println(b.v) b.f1() b.f2() fmt.Println(c.v) c.f1() c.f2() fmt.Println(b.A.v) b.A.f1() b.A.f2() fmt.Println(c.A.v) c.A.f1() c.A.f2() }
都不能访问其它包嵌入对象的私有字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 file: ./pack1/foo.go package pack1type bar struct { V string } type Foo struct { bar v1 string V2 string } func (f Foo) f1() {}func (f *Foo) f2() {}func (f Foo) F1() {}func (f *Foo) F2() {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 file ./main.go package mainimport . "pack1" type A struct { Foo } type B struct { *Foo } func main () { var a A b := B{&Foo{}} a.V = "" b.V = "" a.v1 = "" a.V2 = "" a.f1() a.f2() a.F1() a.F2() b.v1 = "" b.V2 = "" b.f1() b.f2() b.F1() b.F2() }
goroutine 的用户栈 常规的 goroutine 使用的是用户栈,栈空间可动态伸缩,可有效避免栈溢出
对于 golang 中返回局部变量的指针是否安全? 在 golang 中将局部变量的指针返回是安全的,因为运行时和 GC 会自动进行维护,不过要这样写时,最好避免出现数据竞争(race),而且也可以考虑使用 new
来代替这种做法
golang 是静态还是动态语言? golang 虽然是编译型的静态语言,但是支持很多动态语言的特性
引用类型 golang 有六种引用类型:
切片(slice) 映射(map) 通道(channel) 接口(interface) 指针(pointer) 函数(func) 除了上面之外的,基本都属于是值类型,值类型大多直接在栈中分配,而引用类型的本体大多存储在堆中
数组与切片 数组是固定长度的值类型,而切片是长度可变的引用类型,所以在函数传递时,数组的开销比切片要大。
数组在初始化时必须指定大小,因为它容量是固定的
1 2 a := [2]int{1, 2} a := [...]int{1, 2, 3}
切片初始化时不需要指定大小
1 2 3 4 5 a := []int {1 , 2 , 3 } a := make ([]int , 3 , 6 )
从数组或切片创建切片的方法是 arr[起始索引:结束索引+1:新切片容量]
,其中新切片容量不能不能小于结束索引+1
,也不能大于原本数组或切片的容量,如果起始索引
或 结束索引
省略,那么表示 从头
或者 到尾
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 a := make ([]int , 4 , 10 ) for i := 0 ; i < 4 ; i++ { a[i] = i } sa := a[:] sa = a[:3 ] sa = a[3 :] sa = a[1 :3 :10 ] sa = a[1 :3 :8 ] sa = a[1 :3 :3 ] sa = a[1 :3 :11 ]
需要注意的是,这种创建切片的方法,切片的元素还是原来的实体,而不是一个副本,需要脱离关系的话,请使用 copy
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 a := [...]string {"one" , "two" , "three" } s1 := a[1 :2 ] s2 := make ([]string , 3 , 6 ) copy (s2, a[:])fmt.Println(&s1[0 ]) fmt.Println(&a[1 ]) fmt.Println(&s2[1 ])
数组和切片基本操作是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 s := [...]string {"one" , "two" , "three" } for index, value := range s { } for i := 0 ; i < len (s); i++ { } len (str)cap (str)a := [...]string {"one" , "two" , "three" } s := append (a[:], "four" ) s = append (s, "five" ) index := 3 s = append (s[:index], s[index + 1 :]...)
map golang 的 map 定义方式很容易忘记,方式为:map[Key]value
1 2 3 4 5 6 7 8 9 10 m := make (map [string ]int , 10 ) m["key" ] = 1 for k, v := range m { } delete (m, "key" )
make 与 new 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 file: src/builtin/builtin.go func make (t Type, size ...IntegerType) Typefunc new (Type) *Type
从定义可知,make
和 new
的区别:
make
返回的是实体对象 ,new
则是对象指针 make
主要用于 **切片(slice)、映射(map) 和 通道(channel) 等引用类型,new
应用更广,但主要是用于值类型make
和 new
都是在堆上进行分配(不过最好不要肯定回答,而是不要去关心这个问题,让 golang 运行时自己安排)make
会给对象初始化,而 new
分配完内存并将内存块清零(零值)后不会按照类型做相应的初始化,对于引用类型,最好使用 make
interface 与 interface golang 支持非侵入式 的鸭子类型(duck typing) ,通过 interface
实现,不需要类型显式的声明实现了接口,只需要接口的方法集是类型的方法集子集即可。形象点说就是,如果“它”具有鸭子的所有行为,那么“它”就是一只鸭子,鸭子类型通常出现在动态语言中,尽管 golang 是静态语言,不够它确实采用这种设计风格。
interface{}
是一个没有方法的空接口(empty interface) ,任何类型都至少零个方法,也就是说任何类型都实现了一个空接口,那么如果一个函数接受一个 interface{}
类型的参数,意味着可以将任何类型(包括数组、切片...)传给它,这种函数通常可以配合反射(reflect)或类型断言来处理。
1 2 3 4 file: src/builtin/builtin.go type any = interface {}
以下两点需要留意一下:
对于接收空接口参数的函数来说,仍然不能说它接受的类型是任意类型,它是有一个准确的类型的,那就是空接口(interface{}
) 一个类型的空指针不是一个空接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type I interface { foo() } type T struct {}func main () { var ei interface {} var i I var p *T ei == nil i == nil p == nil ei == i p == ei }
空接口反射 1 2 3 4 5 6 import "reflect" func printTypeName (v interface {}) { t := reflect.TypeOf(v) fmt.Printf("type: %s\n" , t.String()) }
变参函数与类型断言(type assertion) 变参函数通常配合 interface{}
定义(比如 fmt.Printf
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func myprint (args ...interface {}) { for idx, v := range args { switch v.(type ) { case string : fmt.Printf("%d string value: %v\n" , idx, v) case int : fmt.Printf("%d int value: %v\n" , idx, v) default : fmt.Println("unknown" ) } } } func main () { myprint("hello" , "world" , 1234 , 9234.0 ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type animal interface { speak() } type human struct {}func (h human) speak() {}func action (a animal) { if h, ok := a.(human); ok { h.speak() } } func main () { action(human{}) }
panic 与 recover 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 var ( ErrNormal = errors.New("this is an error" ) ) func main () { defer func () { if p := recover (); p != nil { fmt.Println(p) } }() panic (ErrNormal) } func main () { defer func () { switch p := recover (); p { case nil : fmt.Println("no panic" ) case ErrNormal: fmt.Println("error: " , p) default : panic (p) } }() panic (ErrNormal) }
Tag 的反射 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type human struct { Name string `mytype:"name" myattr:"12"` Age int `mytype:"age"` foo string } func main () { h := human{"phantom" , 27 , "foobar" } t := reflect.TypeOf(h) v := reflect.ValueOf(h) for i := 0 ; i < t.NumField(); i++ { field := t.Field(i) if tag := field.Tag.Get("mytype" ); len (tag) != 0 { fmt.Printf("%s = %v\n" , tag, v.FieldByName(field.Name)) if attr := field.Tag.Get("myattr" ); len (attr) != 0 { fmt.Println(" attr:" , attr) } } } }
编译指令(compiler directives) golang 支持编译指令(compiler directives) ,这是一种特殊的注释,编译器会根据编译指令在编译期间执行一些处理,这种指令大多使用//go:<directive>
这样的前缀。这里列出一部分,并说明它们的功能
可参考:
//go:generate [command args...] 命令行执行go generate后会执行代码中//go:generate [command args...]的命令,主要用于生成文件 //go:systemstack 指示函数需要进入系统栈执行 //go:noescape 跳过逃逸检查 //go:norace 跳过竞争检查 //go:nosplit 跳过stack overflow检查 //go:noinline 禁止内联 //go:linkename [localname] [importpath.name] 将localname链接到目标中,这可以将一些internal的方法导出来 //go:build [tags] 这个指令的旧语法是 // +build [tags]
,用于指定 build tag,满足条件的时候才会构建,详细看 build constraints //go:embed [patterns] 嵌入内容(可以是单个文件、多个文件或者目录),详细看 embed package //extern [extern name] 指示导出的外部名 //line [filename:line:col] 修改当前的行号和列号(其中 filename
和 col
可以省略) // #cgo 可用于指示 cgo
的编译和链接开关,例如:// #cgo LDFLAGS: -lsocket -lnsl -lsendfile //go:cgo_xxx_xxx 包括上面那条,有一系列与 cgo
相关的编译指令,比如 //go:cgo_import_dynamic
、//go:cgo_dynamic_linker
,可以参考:Command cgo 、C? Go? Cgo! //export [name] 将 go 函数导出为 C,生成 xxx_cgo_export.h
文件
//go:generate 例子 1 2 3 4 5 6 7 package mainfunc main () { }
//go:linkname 例子 1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" _ "unsafe" ) func MyFastrand () uint32 func main () { fmt.Println(MyFastrand()) }
//go:embed 例子 1 2 3 file: ./hello.txt hello world
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( _ "embed" "fmt" ) var hello_info string func main () { fmt.Println(hello_info) }
go build 构建参数 -gcflags '[pattern=]arg list'
指定编译选项,go build -gcflags -help
查看详细帮助-ldflags '[pattern=]arg list'
指定链接选项,go build -ldflags -help
查看详细帮助上面的 pattern=
是可选的,指的是针对的 package
类型,可以通过 go help packages
来查看,主要有:
如果不确定的话,可以不使用 pattern=
,或者使用 all=
具体的 arg
细节就不列出来了,可以直接看 help
文档,这里给出几个常见的构建模式
1 2 3 4 5 6 7 8 9 go build -gcflags 'all=-N -l' . go build -ldflags '-s -w' .
cgo 1 2 ># 确保启用cgo >go env -w CGO_ENABLED=1
插入 C 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "C" import "unsafe" func main () { cstr := C.CString("hello, cgo" ) defer C.free(unsafe.Pointer(cstr)) C.print (cstr) }
导出为 C 模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 file: ./main.go package mainimport "C" import "fmt" func SayHello () { fmt.Println("hello, cgo" ) } func main () { }
1 2 3 4 5 6 7 8 9 file: ./ctest/main.c #include "tcgo.h" int main (int argc, char ** argv) { SayHello(); return 0 ; }
静态模块 1 2 3 4 5 6 7 8 9 10 go build -buildmode=c-archive gcc ctest/main.c tcgo.a -I./ -o ct ./ct
动态模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 go build -buildmode=c-shared -o tcgo.so gcc ctest/main.c -I./ -L./ ./tcgo.so -o ct readelf -d ct ./ct