Go语言笔记
本博客所有文章采用的授权方式为 自由转载-非商用-非衍生-保持署名 ,转载请务必注明出处,谢谢。
声明:
本博客欢迎转发,但请保留原作者信息!
新浪微博:@Lingxian_kong
博客地址:孔令贤的博客
微信公众号:飞翔的尘埃
知识星球:飞翔的尘埃
内容系本人学习、研究和总结,如有雷同,实属荣幸!
教程
- 查看标准库列表:
- 几个不错的翻译教程:
- 新手想学 golang,可以先到这里感受一下:https://play.golang.org/
Go 命令
有一个go可执行程序,如何run?go build会在当前目录生成可执行程序(或者-o <exe路径>
),如果是go install,会在$GOPATH/bin/下创建可执行程序。
如果是普通包,go install 会在本机安装包,在别的代码中就可以import了。而go build不会产生任何文件。
goimports 比 gofmt 多一个自动排序 import 的功能
go clean,代码提交前清除当前源码包和关联源码包里面编译生成的文件。 -i 清除关联的安装的包和可运行文件,也就是通过go install安装的文件 -n 把需要执行的清除命令打印出来,但是不执行
测试某个类:
go test -v -test.run TestMain`
go test -timeout 30s github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/testing -run ^TestRequiredRuleCreateOpts$
安装和试用
这里有一个安装脚本,安装 go 1.9以及 glide
更新本文时,Go的最新版是1.10,我直接在Ubuntu虚拟机中,下载Go的二进制包,并设置我的环境变量。
apt-get purge -y golang-go # 不能自动跟后面的命令一起执行
mkdir -p /opt/go
export GOROOT=/opt/go # go 的安装目录
export GOPATH=$HOME/go # GOPATH 目录用于存放src/bin/pkg,从1.8版本开始默认是~/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin/
sudo touch /etc/profile.d/go.sh
sudo tee /etc/profile.d/go.sh >/dev/null <<'EOF'
export GOROOT=/opt/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin/
EOF
# 到 https://golang.org/dl/ 查看版本
version=1.13
source ~/.bashrc && cd /opt && sudo curl -SL# https://storage.googleapis.com/golang/go${version}.linux-amd64.tar.gz -o golang.tar.gz
sudo tar xzf golang.tar.gz && sudo rm -f golang.tar.gz
# 我执行了这一步,不知道是不是必须的,有个文档说Go 的工具链是用 C 语言编写的,因此在安装 Go 之前你需要先安装相关的 C 工具
apt-get install -y bison ed gawk gcc libc6-dev make
cd -
在 ubuntu 上安装 go 的脚本:
sudo curl -SL# https://gist.github.com/lingxiankong/ad724e785abd133632cc604210b45659/raw/4d6849bb3388ec0c8638b2d9422d6471c53ba646/install_go.sh -o ~/install_go.sh && source ~/install_go.sh 1.13
或者借助 gimme 安装:
eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=1.10.4 bash)"
升级 golang:
curl --silent "https://storage.googleapis.com/golang/$(curl --silent https://golang.org/doc/devel/release.html | grep -Eo 'go[0-9]+(\.[0-9]+)+' | sort -V | uniq | tail -1).$(uname -s | tr '[:upper:]' '[:lower:]')-amd64.tar.gz" | sudo tar -vxz --strip-components 1 -C $(dirname $(dirname $(which go)))
# 比如 mac
curl --silent https://storage.googleapis.com/golang/go1.13.4.darwin-amd64.tar.gz | sudo tar -vxz --strip-components 1 -C $(dirname $(dirname $(which go)))
安装完毕,就这么简单。然后就可以编写Go代码了。一个最简单的hello world如下:
package main
func main() {
println("Hello", "world")
}
保存为hello.go文件,命令行运行go run hello.go,就会看到输出。一些教程还会举个稍微高级的例子,打印Go的版本号:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("%s\n", runtime.Version())
}
很多教程上来就说Go如何好,但我觉得,如果你真有阅读Go代码的需求(比如我是为了阅读Swarm源码),可略过那些章节,直接学习如何写Go语言即可。因为在很熟悉一门编程语言并有与之相关的项目经验前,那些东西除了吹嘘,没有任何实质意义。
再贴一个最简单的用标准库实现http web server:
package main
import (
"io"
"log"
"net/http"
"os"
)
func HelloServer(w http.ResponseWriter, req *http.Request) {
hostname, _ := os.Hostname()
log.Println(hostname)
io.WriteString(w, "Hello, world! I am "+hostname+" :)\n")
}
func main() {
http.HandleFunc("/", HelloServer)
log.Fatal(http.ListenAndServe(":12345", nil))
}
如下是我自己看教程的一些笔记,没有过目不忘的本事,只能靠反复看笔记了。
先谈谈我自己的感受,大概扫了一遍Go语言基础教程,发现Go其实融合了Java、C、Objective-C、Python等语言的一些特性,学Go的过程中,脑子里一直有好几个小人在打架,是并行的几个线程自我否定的过程,比较痛苦。但多掌握一门编程语言不是坏事,就算是锻炼自己脑子了。如果你先前没有其他编程语言经验,那么恭喜,对于Go语言你会很快上手的。另外,一门编程语言真正强大的是它的库函数,所以教程中有关库函数的讲解其实也可以忽略,因为你真正要用的时候,最好还是翻阅权威的官方文档(Go自带package文档参阅这里,国内用户可以访问这里)。
升级 golang
-
mac
sudo rm -rf /usr/local/go # download new binary package from https://golang.org/dl/ sudo tar -C /usr/local -xzf /home/nikhita/Downloads/go1.8.1.linux-amd64.tar.gz echo $PATH | grep "/usr/local/go/bin"
-
ubuntu,删除 /opt/go 文件夹,重新下载解压
笔记
点比较散。有些东西当熟悉了Go语言之后再回过头来看,可能会比较简单。
Go 把 runtime 嵌入到了每一个可执行文件当中,所以go可执行文件的运行不需要依赖任何其它文件,它只需要一个单独的静态文件即可(所以导致可执行文件比较大)。
包/依赖
如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。
main包中的main函数既没有参数,也没有返回类型(与C家族中的其它语言恰好相反)
每一个源文件都可以包含且只包含一个 init 函数,init 函数是不能被调用的。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性;也经常被用在当一个程序开始之前调用后台执行的 goroutine. 需要注意,如果某个init
函数内部用 go 关键字启动了新的 goroutine,这个 goroutine 只有在 main.main 函数之后才能被执行到。
可以用import _ <package path>
引入一个包,仅仅是为了调用init()函数,而无法通过包名来调用包中的其他函数。
而 import . <package path>
的意思是可以直接引用该包中的函数而不需要包名。
依赖管理
-
vendor,vendor 标准化了项目依赖的第三方库的存放位置。从 go 1.5 开始出现官方的 vendor 支持。对vendor目录的解释,2015.12:https://blog.gopheracademy.com/advent-2015/vendor-folder/,vendor 使用时要注意交叉依赖的问题,即不同的包依赖同一个包的不同版本。但 vendor 仅仅是提供一个目录,缺乏管理机制(比如如何更新)
-
glide, deprecated,比如 fn/fission 项目的依赖管理就是用 glide,关于 glide 的介绍:https://blog.bluerain.io/p/Golang-Glide.html
- dep,官方出品,引入两个文件
Gopkg.toml
和Gopkg.lock
,Gopkg.toml 定义都依赖哪些包/branch/version,Gopkg.lock 定义中多了 revision 信息,即具体的 commit id- 安装
go get -u github.com/golang/dep/cmd/dep
- 介绍:
- 命令
- 如果一个新的 project 要用 dep,
dep init -v
dep ensure
,根据配置文件下载安装依赖包dep status
查看当前的依赖情况dep ensure -add github.com/golang/glog
dep ensure -update
,比如修改了Gopkg.toml,该命令更新 lock 文件和 vendor 下的数据使其满足约束。或者直接指定更新的库,dep ensure -update github.com/spf13/pflag
- 如果一个新的 project 要用 dep,
- 如果要测试自己的 repo代替 Gopkg.toml 中的依赖,在
constraint
中指定 source 为自己的 repo 以及branch,name不变 - 如果要修改一个间接依赖,要用
override
- 安装
-
Go 1.11 新推出了 go mod 命令,在 1.13 成为默认依赖包管理工具(在 Go1.11 和 Go1.12 中,Go Modules 只能在
$GOPATH
外部使用)。依赖包统一收集到$GOPATH/pkg/mod
,不在 src 中也不在 vendor 中,并且将 import 路径与项目代码的实际存放路径解耦-
$GOPATH/pkg/mod/cache/download/
中有原始代码的缓存,避免重复下载,多个项目可以共享 -
在项目文件夹创建两个文件
- go.mod,记录依赖的 repo 和 tag,注释 indirect 表示该 module 未被使用
- go.sum,记录完整依赖
- 介绍:
- 命令
go env -w GO111MODULE=on # 已有项目,然后 go get ./... go mod init example.com/lingxiankong/test-go # 新的项目 mkdir go-modules-example; cd go-modules-example go mod init github.com/lingxiankong/projectname # 直接下载指定 version 的依赖 go get github.com/lijiaocn/glib@v0.0.2 go list -m all # 默认情况下,go 自动删除未被使用的依赖,需要手动执行 go mod tidy
-
变量
-
浮点数的类型有float32和float64两种(没有float类型),默认是float64,zero value 是 0
-
var _ I = T{}
,这个语句其实在测试结构体T是否实现了接口I,下划线忽略变量名 - reflect
- 在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型
reflect.ValueOf(s)
获取对象的值
-
声明数组:
var arr1 [5]int
,数组的初始化有很多种方式(比如自动计算长度:var arr1 [...]int{1,2,3}
),数组是值类型,数组的长度的固定的,可以通过传递数组的引用在函数中修改数据。用 for…range 的方式迭代数组会更安全,每轮迭代对数组元素访问时可以不用判断下标越界。- append 数组:
newlist := append(oldList, addedList...)
- shuffle array: https://www.calhoun.io/how-to-shuffle-arrays-and-slices-in-go/
- append 数组:
-
声明切片:
var slice1 []type
,不需要说明长度,初始化:var slice1 []type = arr1[start:end]
,切片是引用类型。切片类似于Python中的list,切片使用make() 创建。切片的cap就相当于数组的长度。切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。当相关数组还没有定义时,可以使用 make() 函数来创建一个切片同时创建好相关数组:var slice1 []type = make([]type, len)
-
// map 是引用类型,用 make() 创建 var map1 map[string]int map1 = make(map[string]int) // 或者直接 map1 := make(map[string]int) // 判断一个map中是否存在某个key if val1, isPresent := map[key1]; isPresent { } // 删除一个 key delete(map, "route") // 遍历。遍历时删除 key 是安全的 for key, value := range map { } // clear map for key := range map { delete(map, key) }
- map 不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制
- 直接初始化一个 map
a := map[string]string{"key1": "value1", "key2": "value2"}
-
new 和 make 的区别:make 只能用于 slice,map,channel 三种内建引用类型。new适用于值类型如数组和结构体的内存分配。new(T) 分配零值填充的T类型的内存空间,返回的是 T 的指针,
var x interface{} = nil
函数
-
函数支持可变参数,
func myFunc(a, b, args ...int) {}
,这里 args 其实是一个切片。如果是传递数组,可以这样myFunc(1, 2, arr...)
,这样其实是传递数组中每个元素。如果可变参数中值的类型都不一样,可以使用struct或空接口。 -
函数参数默认值:不支持。Go语言追求显式的表达,避免隐含
-
实现类似于 python
if...in...
import sort func findString(a string, list []string) bool { sort.Strings(list) index := sort.SearchStrings(list, a) if index < len(list) && list[index] == a { return true } return false }
程序结构
Go支持的代码结构语句:
-
if-else
定义函数时,最好额外返回一个执行成功与否的变量,如下写法会被经常用到:
if value, ok := readData(); ok { }
-
for/for-range
for就类似于C语言中的结构了。Go不支持while语句,无限循环可以用for { } 代替。 for-range结构是Go独有的,类似于有些语言的for-each,写法是
for idx, val := range coll {}
,需要注意,val 始终为集合中对应索引的值拷贝,是只读的。对 channel 的遍历:
for inValue := range in { }
-
switch
python 没有 switch。switch无需使用break退出分支,默认行为是匹配到一个分支运行结束后,就退出switch语句。如果希望继续执行,使用 fallthrough。
package main import ( "fmt" "runtime" ) func main() { fmt.Print("Go runs on ") switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: // freebsd, openbsd, // plan9, windows... fmt.Printf("%s.\n", os) } }
-
select
channel轮询
-
对数组的遍历两种方式:
// 第一种 for i:=0; i < len(arr1); i++ { arr1[i] = 1 } // 第二种 for index, value := range arr1 { }
- recover 只能在 defer 修饰的函数中使用:用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。
- Go支持标签和goto语法,但尽可能少用。
结构体
-
P := new(person)
返回的 P 是一个指针 -
类型对应的方法必须和类型定义在同一个包中。因此无法直接给 int 等类型添加自定义方法。但可以通过自定义类型的方式,比如:
type ages int
-
如果在 struct 的定义中引用了另一个 struct 的类型,会继承这个 struct 的属性和方法
-
获取一个接口变量varI真正的值,
value, ok := varI.(T)
,简化形式:value := element.(T)
。也可以用switch t := varI.(type)
对接口变量的类型进行判断。 -
测试一个变量v是否实现了某个接口:
sv, ok := v.(varI)
-
测试一个 struct 是空的
// 要求 myType 中的元素实现了 == 方法,复杂 struct 不适用 mytype := myType{} fmt.Printf("is equal: %v", mytype == (myType{})) // 对于复杂 struct,需要使用 struct 中某个元素作为标识
-
结构体和 json
-
定义可转化为 json 的结构体时,json 字段必须是导出的,如果结构体变量名称和 json 字段不同,可使用成员标签定义
-
json.Marshal
把结构体转换为[]byte
数组type Move struct { Title string Year int `json:"released"` Color bool `json:"color,omitempty"` Actors []string NotParse bool `json:"-"` // 不解析 } var movies = []Movie{...} data, err := json.Marshal(movies) // 或者是 MarshalIndent(movies, "", " ")
-
同步
-
为了保证同一时刻只有一个线程会修改一个struct,通常的做法是在struct的定义中加入sync.Mutex类型的互斥锁变量。在函数中,可以通过lock和unlock函数加锁和解锁。在 sync 包中还有一个 RWMutex 读写锁:他能通过 RLock() 来允许同一时间多个线程对变量进行读操作,但是只能一个线程进行写操作。如果使用 Lock() 将和普通的 Mutex 作用相同。
-
sync.WaitGroup 的用法是程序需要并发执行多个goroutine,但需要等所有goroutine都结束才能继续。通常配合 context,signal 使用,signal 接收停止指令,通过 context 通知到 goroutine,主进程通过 waitgroup 等待所有 goroutine 返回。
package main import ( "fmt" "sync" ) func Afunction(waitgroup *sync.WaitGroup, shownum int) { defer waitgroup.Done() fmt.Println(shownum) } func main() { var waitgroup *sync.WaitGroup for i := 0; i < 10; i++ { waitgroup.Add(1) //每创建一个goroutine,就把任务队列中任务的数量+1 go Afunction(waitgroup, i) } waitgroup.Wait() //.Wait()这里会发生阻塞,直到队列中所有的任务结束就会解除阻塞 }
测试
-
中文教程:https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/11.3.md
-
代码
func TestSum(t *testing.T) // 定义测试函数 testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过
channel
-
博客
- https://zhuanlan.zhihu.com/p/59295820
- 如何优雅的关闭channel:http://www.tapirgames.com/blog/golang-channel-closing
-
NOTE
- 一般在协程中往 channel 发送数据,因为 channel 的发送和接收都是阻塞操作且无缓冲,发送操作在接收者准备好之前是阻塞的。发送完数据,发送端就可以关闭 channel,不影响接收者接收数据,因为关闭 channel 之后仍然可以读
- 在 select 中,如果多个可以处理,则随机选择一个
- 总是在发送端关闭 channel
-
有时候一个函数仅发送数据或接收数据,定义时可以:
func source(ch chan<- int){ for { ch <- 1 } }
-
如果不管通道中存放的是什么,如果只关注长度,可以创建如下通道
type Empty interface {} type semaphore chan Empty sem := make(semaphore, N)
-
示例代码
// 基本定义,无缓冲 channel,不限容量 ch := make(chan int) // 有缓冲 channel ch := make(chan string, 100) // 等待接收数据。如果 channel 关闭了 ok == false if v, ok := <- ch; ok { process(v) } // 或者使用for 语句,直到通道被关闭 for input := range ch { process(input) } // 防止 channel 超时 package main import ( "fmt" "time" ) func main() { c := make(chan int) o := make(chan bool) go func() { for { select { case i := <-c: fmt.Println(i) case <-time.After(time.Duration(3) * time.Second): fmt.Println("timeout") o <- true break } } }() <-o }
-
测试通道关闭,设置超时
package main import ( "fmt" "time" ) func prepareData(ch chan string) { for _, lb := range []string{"lingxian", "kong", "catalyst"} { ch <- lb time.Sleep(3 * time.Second) } close(ch) fmt.Println("channel closed!") } func main() { ch := make(chan string) go prepareData(ch) for i := 0; i < 2; i++ { go func(ch <-chan string, i int) { for { select { case s, ok := <-ch: if !ok { fmt.Printf("goroutine %d exits\n", i) return } fmt.Printf("goroutine %d: %s\n", i, s) time.Sleep(1 * time.Second) case <-time.After(time.Duration(3) * time.Second): fmt.Printf("goroutine %d timeout\n", i) break } } }(ch, i) } time.Sleep(20 * time.Second) }
signal
-
NOTE:
- 使用场景:如果希望捕获操作系统信号并做相应的处理(比如优雅停止等)
- 你只需定义关心哪些信号,如果不设置表示监听所有的信号
-
示例:
-
基本使用
package main import ( "os" "os/signal" ) func main() { doneCh := make(chan struct{}) signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, os.Interrupt) go receive(signalCh, doneCh) <-doneCh } func receive(signalCh chan os.Signal, doneCh chan struct{}) { for { select { // Example. Process to receive a message // case msg := <-receiveMessage(): case <-signalCh: doneCh <- struct{}{} } } }
-
context
-
在父子线程间传递,父线程终止时传递信息给子线程也终止,在多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就停止当前正在执行的工作并返回。
-
Background 和 TODO 方法在某种层面上看其实也只是互为别名,两者没有太大的差别,不过 context.Background() 是上下文中最顶层的默认值,所有其他的上下文都应该从 context.Background() 演化出来。我们应该只在不确定时使用 context.TODO(),在多数情况下如果函数没有上下文作为入参,我们往往都会使用 context.Background() 作为起始的 Context 向下传递。
-
与 signal 配合的一个例子:
package main import ( "context" "fmt" "os" "os/signal" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) exitCh := make(chan struct{}) go func(ctx context.Context) { for { fmt.Println("In loop. Press ^C to stop.") // Do something useful in a real usecase. // Here we just sleep for this example. time.Sleep(time.Second) select { case <-ctx.Done(): fmt.Println("received done, exiting in 500 milliseconds") time.Sleep(500 * time.Millisecond) exitCh <- struct{}{} return default: } } }(ctx) signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, os.Interrupt) go func() { select { case <-signalCh: cancel() return } }() <-exitCh }
-
博客
-
2016.08,http://lanlingzi.cn/post/technical/2016/0802_go_context/
-
避免在context中存储自定义数据:https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them/
// 在父线程中 ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 调用 cancel 后,在子线程中 select { case <- ctx.Done(): return -1 default: // 没有结束 ... 执行 ... }
时间
-
计时器(循环触发)。如果处理慢怎么办?Ticker 会等待处理完成,这里有一个证明。
- 使用 time.Ticker, https://golang.org/pkg/time/#NewTicker
-
定时器(触发一次),
time.After
,见下面的代码示例 -
cronjob 的 lib:https://godoc.org/github.com/robfig/cron
-
Time 和 string 转换
fmt.Println(time.Now().Format("2006-01-02 15:04:05")) myTime, err := time.Parse("2006-01-02 15:04:05", myTimeStr)
字符串 string
-
k8s 提供的一个 sets 库,有一个 String 类型,好使! k8s-keystone-auth 有用
import "k8s.io/apimachinery/pkg/util/sets"
-
tagsString := strings.Join(tags, ",")
-
使用fmt.Printf,换行需要加
\n
,%v
代表使用类型的默认输出格式,%#v
提供最典型的调试信息,%t
表示布尔型,%g
表示浮点型,%p
表示指针。int 不能使用%s
打印,必须是%d
。fmt.Errorf
字符串首字母不能大写 -
python 里面的原生字符串(比如
r('xxx')
)在 go 中用反引号表示。 -
什么时候用
string
什么时候用*string
,参见这里。简单说,使用 string,在 unmarshal 时如果字段 missing 则赋值为空字符串,而如果使用*string
,缺失的字段值为 nil。类似的讨论在 gophercloud 也有。在 api 的 update 操作中,如果一个字段定义:
bool,omitempty
,则 client 不能设置 falsestring,omitempty
,则不能设置空字符串*bool或*string
,如果没有设置,则发送 null,如果设置 false 或空串,则在 body 中会体现*bool,omitempty
,如果没有设置,则发送的 body 体中不包含这个字段[]string
是一样的道理,如果该 field 可以被更新成空 list,则应该定义为*[]string
-
字符串拼接除了 Sprintf(返回字符串),还可以使用
strings.Join([]string{s,"ewrwer"},"")
-
Println
s := fmt.Sprintf("vip port: %s", lb.VipPortID) fmt.Println(s)
-
string 转 int:https://gobyexample.com/number-parsing
newPort, err := strconv.Atoi(string)
-
string 转 bool
val, err := strconv.ParseBool("true") if err != nil { }
-
实现 Contains 方法
import "sort" // StringContains searches if a string list contains the given string or not. func StringContains(list []string, a string) bool { sort.Strings(list) index := sort.SearchStrings(list, a) if index < len(list) && list[index] == a { return true } return false } // 或者简单一点 // Contains searches if a string list contains the given string or not. func Contains(list []string, strToSearch string) bool { for _, item := range list { if item == strToSearch { return true } } return false }
错误处理 Error
- 任何实现 Error() 方法的 struct 都可以作为一个 Error。也可以用
errors.New("")
返回一个 Error 对象。或者是fmt.Errorf("")
- 博客
- 如何优雅的处理 error,打印调用堆栈
json/yaml
- json
src/k8s.io/apimachinery/pkg/util/json/json.go
- yaml
k8s.io/apimachinery/pkg/util/yaml/decoder.go
HTTP 客户端
-
博客
-
示例
-
发送 https 请求
package main import ( "fmt" "io/ioutil" "log" "net/http" "crypto/tls" ) func main() { req, err := http.NewRequest("GET", "https://127.0.0.1:6443/healthz", nil) if err != nil { log.Fatal("Error reading request. ", err) } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{Transport: tr} resp, err := client.Do(req) if err != nil { log.Fatal("Error reading response. ", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatal("Error reading body. ", err) } fmt.Printf("%s\n", body) }
-
gRPC
- 资料
- 2019.07,Go gRPC Hello World
一些技巧
-
uuid,
src/k8s.io/apimachinery/pkg/util/uuid/uuid.go
-
go build
- 可以在编译时设置变量:
CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s -X 'github.com/lingxiankong/openstackcli-go/cmd.version=1.0.0" -o mycli main.go
。参见这里
- 可以在编译时设置变量:
-
自己写 project 的一些心得:
构造结构体,最后要有逗号 在函数里面的变量是函数内作用域 短变量声明最少声明一个新变量,否则要使用等号赋值
- select
- select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的
- http://yanyiwu.com/work/2014/11/08/golang-select-typical-usage.html
-
装饰器的效果
func errorHandler(fn fType1) fType1 { return func(a type1, b type2) { defer func() { if e, ok := recover().(error); ok { log.Printf(“run time panic: %v”, err) } }() fn(a, b) } }
- debug
-
示例:
```shell go get -u github.com/go-delve/delve/cmd/dlv dlv debug main.go break main.main # 函数名和文件名 b break <函数名> b <function>[:<line>] continue # c next # 后续可以直接按回车,Delve会重复上一条命令, n print var # p # 对于已经运行的程序 dlv attach PID ```
- 在 golang 项目中集成 release,goreleaser
第三方库
-
CLI: https://github.com/urfave/cli fn 在用,fission CLI 也用 从help message构造CLI解析, fission server 使用: https://github.com/docopt/docopt.go cobra,见下
-
https://github.com/spf13/pflag 增强的flag库
- 配置项管理:
- Go Config 支持配置项动态更新,同时支持命令行参数,环境变量,配置文件,https://github.com/micro/go-config,一篇介绍
- https://github.com/spf13/viper 这里有个配置文件对应 struct 定义的例子,unmarshal 时的 annotation 不是 json,而是
mapstructure:
- 如果只是 ini 文件:https://github.com/go-ini/ini
- 解析配置文件: https://blog.gopheracademy.com/advent-2014/reading-config-files-the-go-way/
-
https://github.com/boltdb/bolt 简单键值对数据库
-
cron job runner: https://godoc.org/github.com/robfig/cron
-
openstack go client http://gophercloud.io,由 rackspace 维护,最近 commit 在2016年
-
webservice,k8s 使用:https://godoc.org/github.com/emicklei/go-restful
-
debug https://github.com/derekparker/delve GDB https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/11.2.md
-
Version 操作,比如比较,因为 2.12>2.2. 可以使用:
// 不支持 2.12 的形式,必须遵循 Semantic version package main import ( "github.com/coreos/go-semver/semver" "fmt" ) func main () { vA := semver.New("2.12.3") vB := semver.New("2.2.2") if vA.Compare(vB) < 0 { fmt.Printf("%s is less than %s", vA, vB) } } // 或者,支持 2.12 的形式,这个有很高的容错性,可以加 v 也可以不加,都能识别 package main import ( version "github.com/hashicorp/go-version" "fmt" ) func main () { vA, _ := version.NewVersion("1.2") vB, _ := version.NewVersion("1.12") if vA.GreaterThanOrEqual(vB) { fmt.Printf("%s is GreaterThanOrEqual %s", vA, vB) } else { fmt.Printf("%s is LessThan %s", vA, vB) } }
日志
-
https://github.com/sirupsen/logrus 在日志中方便的显示变量
-
Debug, Info, Warn, Error, Fatal, Panic,Fatal 函数会调用
os.Exit(1)
-
不管 Log rotation
-
示例
import ( log "github.com/sirupsen/logrus" ) log.SetOutput(os.Stdout) log.SetLevel(log.InfoLevel) log.SetFormatter(&log.TextFormatter{}) // 一般打印 log.WithFields(log.Fields{"error": err}).Fatal("Unable to decode the configuration") // 会退出程序 log.WithFields(log.Fields{"error": err}).Error("Unable to decode the configuration") log.WithFields(log.Fields{"error": err}).Info("Unable to decode the configuration") log.WithFields(log.Fields{ "event": event, "topic": topic, "key": key, }).Info("Failed to send event") // 或者把变量固定 requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip}) requestLogger.Info("")
-
glog,k8s 使用
import glog "github.com/golang/glog" glog.V(2).Infoln("Processed", nItems, "elements") glog.Fatalf("Initialization failed: %s", err)
HTTP 服务 Gorilla Mux
-
https://github.com/gorilla/mux 定义URL到handler的映射,兼容http.ServeMux
-
这个库的牛逼之处在于有很多高级的match模式,matcher 可以写在一起
r := mux.NewRouter() r.HandleFunc("/products", ProductsHandler). Host("www.example.com"). Methods("GET"). Schemes("http")
-
如果很多routes 有一些重复的 matcher,可以使用 subrouter
s := r.Host("www.example.com").Subrouter() s.HandleFunc("/products/", ProductsHandler)
-
有了 router,可以结合http.Server
srv := &http.Server{ Handler: r, Addr: "127.0.0.1:8000", // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Fatal(srv.ListenAndServe())
-
middleware 的定义
type MiddlewareFunc func(http.Handler) http.Handler // 比如 func simpleMw(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Do stuff here log.Println(r.RequestURI) // Call the next handler, which can be another middleware in the chain, or the final handler. next.ServeHTTP(w, r) }) } r.Use(simpleMw)
层级命令行 cobra
https://github.com/spf13/cobra 编写 CLI 程序,可以自动生成 CLI 框架,推荐!
- 基本用法
// 定义 rootcmd
var rootCmd
var getProjectsCmd = &cobra.Command{}
rootCmd.AddCommand(getProjectsCmd)
// 也可以同时 add 多个
rootCmd.AddCommand(cmdPrint, cmdEcho)
-
参数约束
-
https://github.com/spf13/cobra#positional-and-custom-arguments
-
示例
Args: cobra.ExactArgs(1), // 定义 positional 参数个数 cmd.PersistentFlags().StringVar(&socketpath, "socketpath", "", "Barbican KMS Plugin unix socket endpoint") cmd.MarkPersistentFlagRequired("socketpath")
-
-
脚手架
# in .bashrc
export GOPATH=$HOME/go_project
export PATH=$PATH:$GOPATH/bin
# Avoid the error 'Unknown SSL protocol error in connection to gopkg.in'
Lingxians-MAC[~]$ git config --global http.sslVerify true
Lingxians-MAC[~]$ go get -u -v github.com/spf13/cobra/cobra
Lingxians-MAC[~]$ cobra init $GOPATH/src/tests/runit
Lingxians-MAC[~]$ cd $GOPATH/src/tests/runit
Lingxians-MAC[~/go_project/src/tests/runit]$ ll
total 32
-rw-r--r-- 1 konglingxian staff 11358 Jan 6 13:01 LICENSE
drwxr-xr-x 3 konglingxian staff 102 Jan 6 13:01 cmd
-rw-r--r-- 1 konglingxian staff 677 Jan 6 13:01 main.go
Lingxians-MAC[~/go_project/src/tests/runit]$ cobra add os
os created at /Users/konglingxian/go_project/src/tests/runit/cmd/os.go
# 注意这里不能是‘cobra add get-projects’,cobra 暂时不支持
Lingxians-MAC[~/go_project/src/tests/runit]$ cobra add getProjects -p 'osCmd'
getProjects created at /Users/konglingxian/go_project/src/tests/runit/cmd/getProjects.go
Lingxians-MAC[~/go_project/src/tests/runit]$ tree
.
|____cmd
| |____getProjects.go
| |____os.go
| |____root.go
|____LICENSE
|____main.go
# 然后你自己可以编写逻辑了……
gin
实现 HTTP server 和 API 路径绑定(router),可以与 http.Server 配合实现 http 服务
https://github.com/gin-gonic/gin
fn 和 fission 项目都使用
router := gin.Default()
// 路径参数
router.GET("/user/:name", getUser)
// 可选路径参数
router.GET("/user/:name/*action", userAction)
gophercloud
http://gophercloud.io/docs/compute/#setup
- 输入是鉴权信息,获取一个 provider
- 根据 provider 获取各个 service client
- 导入各个资源的包,调用包的导出函数(service client 为参数)
- 这种结构很怪,感觉是函数式编程
代码示例
http 服务器
- handler,任何实现 ServeHTTP 方法的对象都可以作为 Handler,但http.HandleFunc简化了 handler 的编写
- 预定义了一些通用的 handler,比如FileServer,404的NotFoundHandler,重定向的RedirectHandler
- 其实就算代码没用 ServerMux,net/http 在后台也会默认创建使用 DefaultServeMux
- 使用ServerMux时要注意:/images 会匹配 /images/cute-cat.jpg
- ServerMux 不支持带参数的路由,不能捕获 URL 中的变量,不支持 HTTP 方法匹配。所以才有Gorilla Mux或httprouter库
// 最原始的写法
package main
import (
"io"
"net/http"
)
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}
func main() {
http.Handle("/", &helloHandler{})
http.ListenAndServe(":12345", nil)
}
// 上述写法要为每个 handler 都定义一个结构体和实现ServeHTTP方法,很不方便,于是net/http提供了HandleFunc方法,允许直接把特定类型的函数作为 handler。其实 http.HandleFunc 第二个参数的定义如下,所以任何满足该参数定义方式的函数都能作为 handler:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, req *http.Request) {
// io.WriteString(w, "hello, world!\n")
// w.Write([]byte("Hello, world!"))
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// 或者使用 ServerMux,允许定义多个 URL 到 handler 的映射。http.ListenAndServe函数第二个参数就是 mux,因为它是个特殊的 handler,也实现了ServeHTTP方法
package main
import (
"net/http"
"io"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/bye", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "byebye")
})
mux.HandleFunc("/hello", sayhello)
http.ListenAndServe(":8080", mux)
}
func sayhello(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "hello world")
}
类似于 python 中的 if…in…
import "sort"
files := []string{"Test.conf", "util.go", "Makefile", "misc.go", "main.go"}
target := "Makefile"
sort.Strings(files)
i := sort.SearchStrings(files, target) // 行为比较怪异,如果 target 不在列表中,则返回 target 应该 insert 的 index
if i < len(files) && files[i] == target {
fmt.Printf("found \"%s\" at files[%d]\n", files[i], i)
}
// 写成函数
func FindString(list []string, a string) bool {
if a == "" || len(list) == 0 {
return false
}
sort.Strings(list)
index := sort.SearchStrings(list, a)
if index < len(list) && list[index] == a {
return true
}
return false
}
定时器
-
k8s 的
k8s.io/apimachinery/pkg/util/wait/wait.go
有很多 utility 函数-
func Until(f func(), period time.Duration, stopCh <-chan struct{})
定时(不计算函数的执行时间)循环执行一个函数,直到 stopCh close -
func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error
,重复调用 condition 函数,如果遇到 error 或 condition 函数返回 true 时停止。时间间隔以指数方式递增。 -
func Poll(interval, timeout time.Duration, condition ConditionFunc) error
,循环调用 condition 函数,直到 return true、error 或者 timeout -
func PollUntil(interval time.Duration, condition ConditionFunc, stopCh <-chan struct{}) error
,循环调用函数直到 return true、error 或者 stopCh close -
另一个 gophercloud 提供的函数,每隔 1 秒钟,写死的
// WaitForLoadBalancerState will wait until a loadbalancer reaches a given state or ERROR. func (os *OpenStack) WaitForLoadBalancerState(lbID, status string, secs int) error { return gophercloud.WaitFor(secs, func() (bool, error) { current, err := loadbalancers.Get(os.Octavia, lbID).Extract() if err != nil { if httpStatus, ok := err.(gophercloud.ErrDefault404); ok { if httpStatus.Actual == 404 { if status == "DELETED" { return true, nil } } } return false, err } if current.ProvisioningStatus == "ERROR" { return false, fmt.Errorf("loadbalancer %s goes to ERROR", lbID) } if current.ProvisioningStatus == status { return true, nil } return false, nil }) }
-
-
示例
// Tick 是循环发送,After 只发送一次
tick := time.Tick(1*time.Second)
boom := time.After(1*time.Second)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(1*time.Second)
}
}