Go语言笔记

2016-12-20

本博客所有文章采用的授权方式为 自由转载-非商用-非衍生-保持署名 ,转载请务必注明出处,谢谢。

声明:
本博客欢迎转发,但请保留原作者信息!
新浪微博:@Lingxian_kong
博客地址:孔令贤的博客
微信公众号:飞翔的尘埃
知识星球:飞翔的尘埃
内容系本人学习、研究和总结,如有雷同,实属荣幸!

教程

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.tomlGopkg.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
    • 如果要测试自己的 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 路径与项目代码的实际存放路径解耦

变量

  • 浮点数的类型有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 的方式迭代数组会更安全,每轮迭代对数组元素访问时可以不用判断下标越界。

  • 声明切片:var slice1 []type,不需要说明长度,初始化:var slice1 []type = arr1[start:end],切片是引用类型。切片类似于Python中的list,切片使用make() 创建。切片的cap就相当于数组的长度。切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。当相关数组还没有定义时,可以使用 make() 函数来创建一个切片同时创建好相关数组:var slice1 []type = make([]type, len)

  • map

    // 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()这里会发生阻塞,直到队列中所有的任务结束就会解除阻塞
    }
    

测试

channel

  • 博客

  • 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
    }
    
  • 博客

时间

  • 计时器(循环触发)。如果处理慢怎么办?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"
    
  • strings介绍

    tagsString := strings.Join(tags, ",")
    
  • 使用fmt.Printf,换行需要加\n%v代表使用类型的默认输出格式,%#v 提供最典型的调试信息,%t表示布尔型,%g表示浮点型,%p表示指针。int 不能使用 %s 打印,必须是 %dfmt.Errorf 字符串首字母不能大写

  • python 里面的原生字符串(比如r('xxx'))在 go 中用反引号表示。

  • 什么时候用 string 什么时候用*string,参见这里。简单说,使用 string,在 unmarshal 时如果字段 missing 则赋值为空字符串,而如果使用*string,缺失的字段值为 nil。类似的讨论在 gophercloud 也有

    在 api 的 update 操作中,如果一个字段定义:

    bool,omitempty,则 client 不能设置 false

    string,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("")
  • 博客

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

一些技巧

  • 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 的一些心得:

      构造结构体,最后要有逗号
      在函数里面的变量是函数内作用域
      短变量声明最少声明一个新变量,否则要使用等号赋值
    
  • 反射:https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.6.md#%E5%8F%8D%E5%B0%84

  • select
  • 装饰器的效果

      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

第三方库

日志

  • 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

  • 文档地址:http://www.gorillatoolkit.org/pkg/mux

  • 这个库的牛逼之处在于有很多高级的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)
# 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)
    }
}

文章赞赏

赞赏码

文章评论

comments powered by Disqus


章节列表