Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Go语言学习笔记

17
Jul
2016

Go语言学习笔记

By Alex
/ in Go
0 Comments
简介

Go是一门开源的、表现力强的、简洁的、静态编译型语言。它在语言级别支持协程(Coroutine),让你轻松的编写并发性代码。Go新颖的类型系统便于构建灵活的、模块化的应用程序。Go能够快速的编译为机器码,同时支持垃圾回收(并发标记清除)、运行时反射等现代语言特性。

多年来系统级编程语言没有出现新成员,然而计算领域发生重大的变化:

  1. 计算机的速度极大的增快了,而软件开发速度的变化不大
  2. 当今的软件开发中,依赖管理是一项重要的工作。C风格语言的基于头文件的依赖难以进行清晰的依赖分析
  3. 传统静态类型语言,例如Java、C++笨重的类型系统让人反感,很多团队转而使用动态类型语言,例如Python、JavaScript
  4. 流行的系统级语言不支持垃圾回收、并行计算等基础概念

Go致力于解决解决上面几点中提及的问题,它能保证大型程序的快速编译、简化依赖管理、完全支持垃圾回收和并行计算。

Go提供了一个运行时,此运行时作为每个Go应用程序的一部分,提供语言的基础服务,例如垃圾回收、并发、栈管理。这个运行时更像是libc而非JVM,Go运行时不提供虚拟机。

编程元素
包

任何Go应用程序都是由包构成:

  1. 程序的执行入口必须是main包,在构建时go自动为main包创建可执行程序
  2. 程序可以导入一个或者多个包
  3. 包的名字通常和导入路径的最后一个目录名一致,也就是go源文件中声明的包名和文件所在目录名一样
  4. 包可以包含多个源文件

代码示例: 

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明当前所属包
package main
 
// 导入包,以便使用其中的成员
import (
    "fmt"
    "math/rand"
    "time"
)
// 除了用圆括号一起导入多个包,还可以多次使用import语句分别导入单个包
import "fmt"
import "math/rand"
 
func main() {
    rand.Seed( time.Now().UnixNano() )
    fmt.Println("Random number: ", rand.Intn(100) )
}
名称导出

包中的任何名称 —— 定义在全局作用域下的变量或者函数,只要以大写字母开始,即为导出名称(Exported names)。

只有导出名称才能够在包外部(import包的地方)访问。 对于A.B这样的表达式,只有A被导出了B才可能被包外部访问。

没有导出的名称,只能在包内访问,各源文件都可以自由访问其它源文件中定义的名称。

包导入

要使用一个包,必须使用import关键字将其导入。Go在根据导入路径查找包时,遵循以下优先级:

  1. 首先在$GOROOT下寻找
  2. 然后在$GOPATH下寻找
  3. 仍然找不到,尝试进行远程包下载、导入

在导入包的时候,可以赋予其别名,例如: import util "gmem.cc/util"。如果想导入包但又不使用其export出的成员,可以将其别名为 _,这种用法仅仅为了执行包中的init函数。

init函数

每个源文件都可以定义init函数(因此一个包可以有多个init函数),进行必要的状态初始化。init函数的执行时机是:

  1. init所在源文件所有导入的包被初始化后
  2. 包中声明的所有变量的初始化式被估算后
  3. 如果目标包中有多个init函数,则根据init所在源文件的名称的字典序,依次执行

例如下面这个源文件:

Go
1
2
3
4
5
6
7
func init() {
    fmt.Println("Initializing...")
}
 
func IsBlank(str string) bool {
    return len(strings.Trim(str, " ")) == 0
}

只要其所在的包被使用(如果是main包则一定被使用),则init函数一定会执行,并且肯定在IsBlank被调用之前。

一个较为复杂的Go程序中,包初始化顺序如下:

Shell
1
2
3
4
5
6
7
8
9
  go run *.go
# ├── 主包被执行
# ├── 初始化所有导入包
# |  ├── 初始化自己之前,初始化所有导入的包
# |  ├── 然后初始化自己的变量
# |  └── 然后调用自己的init
# └── 主包初始化
#    ├── 初始化全局变量
#    └── 按源文件名称依次执行init函数
函数

Go的函数可以接收0-N个参数:

Go
1
2
3
func add(x int, y int) int {
    return x + y
}

可以注意到,变量的类型、函数的返回值类型,都是后置声明的。 这种与众不同的语法风格,在进行复杂声明时能够保持可读性。

如果连续多个形参的类型相同,则可以仅仅为最后一个声明类型:  func add(x, y int) int {} 

多值返回

Go函数可以返回多个值:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
 
import "fmt"
// 声明多个返回值时,需要用括号包围
func swap(x, y string) (string, string) {
    return y, x
}
 
func main() {
    // 短声明,只能在函数体内使用
    a, b := swap("Wong", "Alex")
    fmt.Println(a, b)  // Alex Wong
}
返回值命名

一般语言的返回值只能声明类型,Go的返回值还可以具有名称:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
 
import (
    "fmt"
    "strings"
)
                        // 返回值具有名称,相当于定义了局部变量
func split(name string) (first, last string) {
    na := strings.Split(name," ")
    // 直接为返回值赋值
    first = na[0]
    last = na[1]
    // 空白的return语句意味着依次返回,相当于 return first, last
    return
}
 
func main() {
    fmt.Println(split("Alex Wong"))
}
作为值使用

函数可以像任何值一样,被传递来传递去:

Go
1
2
3
4
5
6
7
8
9
10
11
12
// 函数作为形参,不需要声明其参数、返回值的名字
func add(x, y int, adder func(int, int) int) int {
    return adder(x, y)
}
func main() {
    // 函数作为变量
    adder := func(x, y int) int {
        return x + y
    }
    // 函数作为实参
    fmt.Println(add(1, 2, adder))
}
闭包

所谓闭包,是指一个函数值(作为值的函数,Function Value)引用其外部作用域的变量。这导致即使外部作用域生命周期结束,被引用的变量仍然存活。

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
func genCounter() func() int {
    count := 0
    return func() int {
        count++ // 每次调用导致被封闭的变量值增加
        return count
    }
}
func main() {
    counter := genCounter();
    for i := 0; i < 10; i += 1 {
        fmt.Println(counter())
    }
}
可变参数

Go函数支持不定数量的参数,例如:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
import "fmt"
 
func print(args ...interface{}) {
    for index, value := range args {
        fmt.Printf("%v = %T %v\n", index, value, value)
    }
    // 0 = int 1
    // 1 = int 2
    // 2 = string 一
}
func main() {
    print(1, 2, "一")
}

切片可以展开为可变参数列表:

Go
1
2
3
4
5
6
7
8
func printAll(strs ...string) {
    for _, str := range strs {
        fmt.Printf("%s ", str)
    }
}
func main() {
    printAll([]string{"A", "B", "C"}...) // 展开
}
变量

使用关键字var可以定义一个变量,或者变量的列表。变量可以定义在包级别、函数级别:

Go
1
2
3
4
5
6
7
8
9
10
11
// 包级别的变量列表声明
var c, python, java bool
 
// 声明并赋值
var gender bool = false;
 
func main() {
    // 函数内的单个变量声明
    var i int
    fmt.Println(i, c, python, java)
}
变量的初始化

你可以在变量列表声明之后紧跟着初始化列表:

Go
1
2
3
4
// 数量必须一致
var i, j int = 1, 2
// 变量类型可以省略,从初始化表达式中推导
var c, python, java = true, false, "no!"
短声明变量 

使用 :=操作符,可以在函数体内部声明变量并初始化,而不需要var关键字和变量类型说明(自动推导): 

Go
1
c, python, java := true, false, "no!"

这种语法不能用在函数体外,函数外的每个语句都必须以关键字开头(例如var、func)。 

块级作用域

Go支持块级作用域,可以覆盖父块的变量声明:

Go
1
2
3
4
5
6
7
8
x := 1
fmt.Println(x)     // 打印1
{
    fmt.Println(x) // 使用父作用域的变量,打印1
    x := 2         // 在子作用域重新声明变量
    fmt.Println(x) // 打印2
}
fmt.Println(x)     // 打印1,看不到子作用域的变量
变量的重复声明

单个变量不能重复声明:

Go
1
2
i := 0
i := 1 // 错误

但是多变量声明时,只要其中一个是新变量,就可以重复声明:

Go
1
2
// 重复声明i
i, j := 1, 0  
零值

如果变量在定义时没有明确的初始化,则自动初始化为零值:数值类型初始化为0;字符串类型初始化为"";布尔型初始化为false;interface/map/func/slice/chan初始化为nil。

字符串不能赋值为nil,它的零值只能是空串:

Go
1
var x string = nil // 出错

你可以向零值的slice中添加元素:

Go
1
2
var s []int
s = append(s,1)

但是不能向零值的map添加键值对。 

类型转换

使用语法 T(v)可以将v转换为T类型:

Go
1
2
3
i := 42
f := float64(i)
u := uint(f)

需要注意的是,Go语言在不同的类型之间进行赋值时,需要显式的类型转换。

类型推导

当定义变量而不显式指定其类型时,变量的类型由赋值符号右侧的子表达式推导:

Go
1
2
3
4
var i = 0          // 推导为int
j := i             // 推导为int
f := 3.142         // float64
g := 0.867 + 0.5i  // complex128
常量

常量类似于变量,但是使用 const关键字声明,常量不支持 :=语法:

Go
1
2
3
const Pi = 3.14
const World = "Hello"
const Truth = true
基本类型
类型 说明
bool 布尔型,取值true / false
string

字符串,双引号包围

多行字符串使用反引号包围

定长整数

对应不同的位数(1、2、4、8字节):

有符号类型:int8 int16 int32 int64
无负号类型:uint8 uint16 uint32 uint64

rune

字面意思是“符文”,实际上是int32的别名,代表一个Unicode代码点(Code Point)

代码点和字符唯一性的对应,例如U+2318(十六进制2318)对应字符⌘

rune的直接量语法和Java中的char类型一致:

Go
1
2
var r rune = '⌘'
fmt.Printf("%v %c %x", r, r, r)  // 8984 ⌘ 2318 
不定长整数 int、uint、uintptr在32bit的系统上通常为32位;在64bit的系统上通常为64位
浮点数 float32、float64
复数

complex64、complex128

示例:

Go
1
2
3
var z complex128 = cmplx.Sqrt(-5 + 12i)
const f = "%T(%v)\n"
fmt.Printf(f, z, z)  // complex128((2+3i))
字符串

Go字符串的本质是一个byte切片(以及一些额外的属性)。字符串是不可变的。如果需要修改字符串的某个字符,可以使用byte切片:

Go
1
2
3
4
x := "text"
xbytes := []byte(x)
xbytes[0] = 'T'
string(xbytes)

但是需要注意,单个字符可能存放在多个byte中。因此更新一段字符串,使用rune的slice可能更好,尽管单个字符也可能占用多个rune(例如包含重音符号的字符)。

和byte切片的转换

当字符串转换为byte切片,或者byte切片转换为字符串时,你都会得到原始数据的拷贝。这和通常的Cast语义不同。

获取长度

len()调用获取的是字符串包含的byte数量。要获取字符数量,可以:

Go
1
2
import "unicode/utf8"
utf8.RuneCountInString(data)

实际上RuneCountInString获取的是rune而非char数量,包含重音符号的字符,可能占据两个rune。

获取字符

对字符串进行[]操作,得到的是byte而非rune。要获得rune,可以使用for range操作,或者利用unicode/utf8、golang.org/x/exp/utf8string等包:

Go
1
2
3
import "golang.org/x/exp/utf8string"
str := utf8string.NewString("你好")
str.At(0)
不一定是UTF8 
Go
1
2
3
// 可以使用转义序列引入任意数据
data := "A\xfeC"
utf8.ValidString(data) // false
指针

Go语言支持指针,指针保存变量的地址。取地址、解引用的操作符也被支持:

Go
1
2
3
4
5
6
7
8
var i int = 10
// 取地址,获得变量的指针
var ip *int = &i
// 解引用
// 通过指针来写变量
*ip = 5
// 通过指针来读变量
fmt.Print(*ip) // 5

但是,Go语言不支持指针的运算。

注意:返回局部变量的指针是安全的,函数返回后此变量仍然存活,这和C语言不同。

传值和传引用

如果函数的入参是结构,而非结构的指针,则实际传递的是结构的副本,所有字段均被浅拷贝。

通过make、new、& 获得的对象,你不需要担心拷贝副本的开销或修改无效。也就是说,传递“引用”的类型包括切片、map、通道、指针、函数。string类型是不可变的,也按引用传递。数字、bool、结构等都是按值传递。

数组本身是传值的,作为函数参数时,你通常使用切片而非数组。

结构

Go语言支持结构体,也就是字段的集合:

Go
1
2
3
4
5
6
7
8
9
type Vector3 struct {
    X int
    Y int
    Z int
}
 
func main() {
    fmt.Print(Vector3{1, 2, 3}) // {1 2 3}
}

要访问结构中的字段,使用点号:

Go
1
2
v := Vector3{1, 2, 3}
fmt.Print(v.X)

可以通过结构的指针来访问字段,语法和上面一样:

Go
1
2
3
v := Vector3{1, 2, 3}
pv := &v
fmt.Print(pv.X)

声明结构的不同方式:

Go
1
2
3
4
5
6
7
8
9
10
// 列出所有字段
v1 := Vector3{1, 2, 3}
// 不列出任何字段
v2 := Vector3{}
// 仅仅列出部分字段
v3 := Vector3{Z: 3}
// 对直接量取地址
pv4 := &Vector3{Z: 3}
fmt.Printf("%v %v %v %v", v1, v2, v3, *pv4)
// {1 2 3} {0 0 0} {0 0 3} {0 0 3}
字段标签

使用标签(Tag)你可以为结构的字段添加元数据。这些元数据可以通过反射调用获得。在决定如何存储结构到数据库,或者串行化为JSON/XML等格式时,常常利用字段标签。

字段标签通常是 key1:"value1" key2:"value2"这种形式的键值对,例如:

Go
1
2
3
type User struct {
    Name string `json:"userName" xml:"user-name"`
}

如果值部分包含更多信息,可以使用逗号分隔:

Go
1
Name string `json:"name,omitempty"`

经常使用的标签键包括:

标签键 说明
json 包encoding/json使用此键,具体查看json.Marshal()
xml 包encoding/xml使用此键,具体查看xml.Marshal()
bson 包gobson使用此键,具体查看bson.Marshal()
protobuf 包github.com/golang/protobuf/proto使用此键
yaml 包gopkg.in/yaml.v2使用此键,具体查看yaml.Marshal()
db 包github.com/jmoiron/sqlx使用此键
orm 包github.com/astaxie/beego/orm使用此键
valid 包github.com/asaskevich/govalidator使用此键
schema 包github.com/gorilla/schema使用此键
csv 包github.com/gocarina/gocsv使用此键
空结构

struct{}的特点是占用内存空间为零:

Go
1
2
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // 0

因此可以用它作为占位符:

Go
1
2
chan struct{}        // 仅仅用来传递信号,而非读写数据
map[string]struct{}  // 实现Set结构 
数组

类型 [n]T表示具有n个元素的T类型的数组。 数组的长度是其类型的组成部分,这和C语言类似:

Go
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
var a [2]string
fmt.Println(a[0], a[1]) //  空白
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1]) //  Hello World
fmt.Println(a)          //  [Hello World]
 
//  直接量语法
a = [2]string{"Hello","World"}
 
// 只初始化指定的索引
array := [10]int{1:3,3:1}
// 自动推断数组长度
array:=[...]int{1,2,3,4,5}
 
// 遍历数组的两种方式
for i:=0; i<len(array); i++{
    fmt.Println(array[i])
}
for index,value := range array{
    ...
}
 
// 指针的数组
 
var ia [10]*int{9:new(int)}  // 为索引9分配内存
*ia[9] = 10   // 解引用并赋值,已经分配内存的索引才能被赋值

关于数组,需要注意它的长度是一定的,这和切片不同。元素类型相同、长度也相同的数组,它们的类型才是一样的。

你可以对数组元素进行取地址操作:

Go
1
2
// 避免复制
container := &dep.Spec.Template.Spec.Containers[0]

数组在传参、赋值时是传值,不过参数通常都会用切片。

数组是数值

在C++中,数组即指针。函数的入参是数组时,函数内外引用的是相同内存区域。

但是在Go中,数组是值,向函数传递数组时,函数得到的是原数组的拷贝。也就是说你无法修改原始数组。如果需要修改原始数据,则需要传递其指针:

Go
1
2
3
4
5
6
x := [3]int{1,2,3}
 
func(arr *[3]int) {
  // 解引用
  (*arr)[0] = 0
}()

或者,你可以使用切片。尽管切片本身是传值的,但是其底层数组在函数内外共享:

Go
1
2
3
4
x := []int{1,2,3}
func(arr []int) {
  arr[0] = 7
}(x)

但需注意,这种方法不能用于增加切片元素,因为其底层数组可能被换掉。 

切片

[]T表示类型为T的切片。切片类似于数组,实际上它的底层存储就是数组。切片可以用来实现动态长度的数组。

切片是一个很简单的数据结构,它包括三个成员:指向底层数组的指针;切片长度;切片容量。

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 直接量语法
s := []int{1, 2, 3, 4, 5}
// 切片具有长度、容量两个属性
// 长度是切片包含元素的数量
fmt.Println(len(s))   // 5
fmt.Println(s[4])     // 5
// 容量则是切片的底层数组的长度
fmt.Printlkn(cap(s))  // 5
 
// 通过make函数创建切片,长度5,底层数组长度(容量)10
s := make([]int, 5, 10)
 
// 切片也支持仅初始化指定索引的值
s := []int{3:4}

 切片元素可以是任何类型,包括切片:

Go
1
2
3
4
5
6
7
8
9
10
11
matrix := [][]int{
    []int{1, 2, 3},
    []int{4, 5, 6}, // 如果花括号写在下一行,这此行尾部需要有逗号
}
for i := 0; i < len(matrix); i++ {
    row := matrix[i]
    for j := 0; j < len(row); j++ {
        fmt.Printf("%v ", row[j])
    }
    fmt.Println()
}

Go支持类似于Python的切片操作, s[lo:hi]表示产生从索引lo(包含)到hi(不包含)的新切片。需要注意,进行切片操作后,新产生的切片会共享底层数组:

Go
1
2
3
4
5
6
7
8
9
10
11
s := []int{1, 2, 3, 4, 5}
fmt.Println(s[2:3])  // [3]
// 省略hi则切到尾部
fmt.Println(s[2:]) // [3 4 5]
 
var a [10]int
# 以下表达式等价
a[0:10]
a[:10]
a[0:]
a[:]

要构造切片,可以调用make函数: 

Go
1
2
3
// 构造初始长度为10,容量为100的切片
s := make([]int, 10, 100)
fmt.Print(len(s)) // 10

切片的零值是nil,nil切片的长度、容量皆为0。另外空切片的长度容量也都是0:

Go
1
2
3
4
// nil切片,底层数组的指针为nil
var nilSlice []int
// 空切片,底层数组的指针为一个地址
slice:=[]int{}

要向切片的尾部添加元素,可以调用append函数。这个函数有可能导致创建新的底层数组。

Go
1
2
3
4
5
6
7
// 向切片s添加1-N个元素,返回包含s的原元素和所有新元素的切片
// 如果s的底层数组太小,会自动分配一个大的数组,返回的切片会指向这个新数组
func append(s []T, vs ...T) []T
 
s := []int{1}
s1 := append(s, 2, 3, 4)
fmt.Print(s1) // [1 2 3 4]

在创建新切片时,最好让切片的长度和容量一致,这样append操作总会产生新的底层数组,这可以避免因为共享底层数组导致的奇怪问题。

for...range格式的循环,可以用来迭代切片(以及map):

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s := []int{1, 2, 3, 4, 5}
//  索引,值
for i, v := range s {
    fmt.Printf("%d=%d ", i, v)
}
// 如果不希望迭代索引或值,可以用 _ 代替之
for i, _ := range s {
    fmt.Printf("%d", i)
}
// 如果不希望迭代值,可以直接省略 _
for v := range s {
    fmt.Printf("%d", v)
}
 
// 注意,值是拷贝出来的,不支持引用,要修改切片元素本身,需要
for i,_ := range s {
    e := &s[i]
    e.field = value
}
迭代

不管是切片,还是数组、映射,for range操作获取的都是元素的拷贝。要想修改集合元素,需要使用索引:

Go
1
2
3
for i,_ := range data {
    data[i] *= 10
}

如果元素存放的是指针则可以直接解引用并修改。 

映射
Go
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
type LatLng struct {
    Lat, Lng int
}
 
func main() {
    // 声明一个映射:map[KeyType]ValueType
    var locations map[string]LatLng;
 
    // 使用make函数实例化
    locations = make(map[string]LatLng)
 
    // PUT操作
    locations["Alex"] = LatLng{38, 100}
 
    // GET操作
    fmt.Println(locations["Alex"].Lat)
 
    // 测试键是否存在,如果不存在第一个返回值为零值,第二个false
    ll, exist := locations["Alex"]
    fmt.Printf("%v %v", ll, exist) // {38 100} true
 
    // DEL操作
    delete(locations, "Alex")
 
    // 直接量语法
    locations = map[string]LatLng{
        "Alex": LatLng{38, 100},
        "Meng": LatLng{38, 110},
    }
    // for...range循环。注意,遍历的顺序没有任何保证
    for key,value := range locations{
        fmt.Println(key,value)
    }
}
支持的键类型

布尔值、数字、字符串、指针、通道、接口类型、结构,以及这些类型的数组,都可以作为映射的键。切片、映射、函数不可以作为映射的键。

不像Java,Go的map不支持自定义equals/hashCode。两个键是否相等,规则如下:

  1. 指针:当指针指向同一变量时它们相等,nil指针是相等的
  2. 通道:两个通道是由同一次make调用产生的,则它们相等,nil通道是相等的
  3. 接口:具有相同的运行时类型,且运行时对象是相等的,则接口相等
  4. 结构:如果两个结构的,相同名称的非空字段都相等,则结构相等
  5. 数组:如果每个元素都相等
线程安全

注意,Go中map的操作不是原子的,原因是map的典型应用场景下不牵涉到跨Goroutine的安全访问。

要保证操作的原子性,建议使用Goroutine+Channel,或者使用sync包。

不支持对值取地址

注意:不支持对映射的值进行取地址操作:

Go
1
2
3
4
users := map[string]User{
    "Alex": User{"Alex"},
}
alex := &users["Alex"] // 编译错误 
迭代时删除

注意:在迭代映射的过程中删除键值是安全的:

Go
1
2
3
4
5
for key := range m {
    if key.expired() {
        delete(m, key)
    }
} 
方法

尽管没有类(Class)的概念,Go却支持为类型定义方法。Go不支持方法重载,这意味着两个方法不得具有相同的名字。

方法是一种函数,它具有特殊的接收者(Receiver)参数:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (receiver Receiver) methodName(args Args) rv ReturnValues{}
 
 
type LatLng struct {
    latitude, longitude float64
}
// 接收者位于func关键字和方法名之间,不和普通的方法参数放在一个列表中
func (ll LatLng) isNorthHemisphere() bool {
    return ll.latitude > 0;
}
func (ll LatLng) isEastHemisphere() bool {
    return ll.longitude > 0;
}
 
func main() {
    jazxPos := LatLng{39.9601241068, 116.4405512810}
    fmt.Println(jazxPos.isEastHemisphere())
}

注意:方法仅仅是具有接收者的函数而已。

你也可以为其它(非struct)类型定义方法,但是你只能为当前包中声明的类型定义方法:

Go
1
2
3
4
5
6
7
8
9
10
11
// 类似于C语言的typedef
type Float float64
 
func (f Float) Abs() Float {
    // 注意下面这种在Float和float64之间转换的语法
    return Float(math.Abs(float64(f)))
}
func main() {
    f := Float(-100)
    fmt.Println(f.Abs())
}
值作为接收者

这种情况下,你无法修改源对象,因为传入的是拷贝。

指针作为接收者

可以为指针定义方法,也就是将指针作为方法的接收者。接收者声明为*T也是为T类型定义方法,但是T本身不能是指针。 

基于指针接收者定义方法,你就可以修改接收者的状态,这种行为类似于C语言。

Go
1
2
3
4
5
6
7
8
9
10
11
12
// 传值
func (ll LatLng) isEastHemisphere() bool {
    // 此修改对于调用者不可见
    ll.longitude = 0
    return ll.longitude > 0;
}
// 传引用
func (ll *LatLng) isEastHemisphere() bool {
    // 此修改对调用者可见
    ll.longitude = 0
    return ll.longitude > 0;
}

由于指针修改接收者状态的能力,第二种形式的方法定义要常用的多,此外第二种形式还避免了不必要的值拷贝。一般来说,一个类型上的方法,通常都使用指针接收者,或者值接收者,而不会混用。

调用方法时,你不需要对接收者进行取地址操作,这和普通函数调用不一样。普通函数的参数如果要求指针,则你必须传入指针。

*T支持的方法集是:以*T为接收者的方法集 + 以T为接收者的方法集。因此*T支持的方法可能比T多。

接口

接口是Go中进行方法动态绑定(多态)的唯一途径。针对结构或者其它具体类型的方法调用,其绑定都发生在编译时。

接口这种类型定义一组方法签名:

Go
1
2
3
4
5
type GeoOps interface {
    // 接口中的方法不需要 func关键字
    IsNorthHemisphere() bool
    isEastHemisphere() bool
}

可以将实现了接口中定义了的方法的类型赋值给接口:

Go
1
2
3
4
5
// 以接口类型声明值(Interface Value)
var gops GeoOps
// 是否需要取地址,取决于实现方法的是T还是*T
gops = &jazxPos
fmt.Println(gops.IsNorthHemisphere())
实现接口

Go语言不需要显式的implements声明,类型只需要实现接口中规定的方法即可(鸭子类型识别)。这种设计让接口和它的实现完全解耦。 

需要注意:

  1. 实际类型以值接收者实现接口的时候,不管是实际类型的值,还是实际类型值的指针,都实现了该接口
  2. 实际类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口
nil接口值

如果接口值指向nil变量,调用方法时,接收者是nil。在很多语言中,调用nil的方法会导致空指针异常,但是在Go语言中,你可以优雅的实现如何处理nil值的逻辑。

注意nil接口值和指向nil变量的接口值:

Go
1
2
3
4
5
6
7
8
9
var gops GeoOps;
 
// nil接口值(未赋值给实际变量)导致运行时错误
gops.isEastHemisphere() // panic: runtime error: invalid memory address or nil pointer dereference
 
// 但是让接口值指向nil变量则不会出现上述问题
var ll LatLng
gops = &ll
gops.isEastHemisphere()

还有一个陷阱需要注意:

Go
1
2
3
4
5
6
var e *E = nil
var ei error = e
var ni error = nil
println(e == nil)  // true    nil
println(ei == nil) // false   (error, nil)   这三个都是判断变量本身是不是nil,即使将nil变量赋值给指针,指针也不是nil,因为它获得了类型信息
println(ni == nil) // true    nil

需要注意:

  1. 如果指针实现了接口,则这种指针的零值,可以赋值给接口 —— 这个赋值过程赋予了接口type和value
  2. 尽管可以将零值赋给接口,但是接口变量 != nil。

第2点很容易造成问题,当将指针类型的变量传递给形参类型为接口的方法时,判断nil值很容易出现问题。这种反直觉行为的原因是,接口本质上是 (type, value)对,当你将接口和 nil指针比较时,自然不相等。

解决办法:

  1. 如果知道接口的type,可以进行类型断言,再判断nil:
    Go
    1
    2
    if i.(bool) == nil {
    } 
  2. 否则,可以通过反射:
    Go
    1
    2
    if reflect.ValueOf(i).IsNil() {
    }
空接口

定义了零个方法的接口被称为空接口: interface{}

任何类型的变量都可以赋值给空接口,因为任何类型都实现它的全部方法。空接口用于处理未知类型的值。例如标准库中的fmt.Println方法:

Go
1
2
3
func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}
一些实践

如果一个包仅暴露一个接口,可以将此接口直接命名为Interface:

Shell
1
2
3
4
5
package user
 
type Interface interface {
    GetName() string
}

不要将接口的指针作为入参,只需要使用接口本身:

Shell
1
2
3
func sayHelloTo(user *user.Interface) { // NOT OK
    println((*user).GetName())
}

要避免拷贝,只需要让类型的指针作为接口方法的接收者即可。

子接口类型的实参,可以传递给父接口类型的形参。所谓父子接口,就是体现在方法集的包含关系上。

操作符
自增自减

仅仅支持 i++,不支持++i。而且你不能在表达式中使用自增自减操作符:

Go
1
2
++i       // 错误
data[i++] // 错误
位操作

^作为一元操作符,按位取反,作为二元操作符,异或。

Go
1
2
3
4
5
6
7
8
9
10
var a uint8 = 0x82
var b uint8 = 0x02
fmt.Printf("%08b [A]\n", a) // 10000010 [A]
fmt.Printf("%08b [B]\n", b) // 00000010 [B]
fmt.Printf("%08b (NOT B)\n", ^b) // 11111101 (NOT B)
fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n", b, 0xff, b^0xff) // 00000010 ^ 11111111 = 11111101 [B XOR 0xff]
fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n", a, b, a^b) // 10000010 ^ 00000010 = 10000000 [A XOR B]
fmt.Printf("%08b & %08b = %08b [A AND B]\n", a, b, a&b) // 10000010 & 00000010 = 00000010 [A AND B]
fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n", a, b, a&^b) // 10000010 &^00000010 = 10000000 [A 'AND NOT' B]
fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n", a, b, a&(^b)) // 10000010&(^00000010)= 10000000 [A AND (NOT B)]
类型断言

使用下面的语法可以进行类型断言,将接口类型强制转换为真实类型,或者将接口转换为另外一个接口:

Go
1
2
3
4
5
6
7
8
ll := LatLng{39, 100}
var gops GeoOps = &ll
// 语法:t,ok = i.(T)
llp := gops.(*LatLng)
 
type IstioObject interface { ... }
var object interface{} = ...
item, ok := object.(IstioObject)

如果指定两个变量来接收返回值,则:

  1. 如果断言成功,t为真实类型变量,ok为true
  2. 如果断言失败,t为真实类型的nil值,ok为false 

如果指定单个变量来接收返回值,则断言失败会导致panic

switch语法

这种语法支持在switch语句中进行多次断言,直到类型匹配:

Go
1
2
3
4
5
6
switch val := gops.(type) {
    case *LatLng:
        // 执行到这里则val 是 *Latlng
    default:
 
}
new/make

Go支持两种变量分配原语:new、make,二者都是内置函数。

new

new仅仅用于分配内存,将其置零,但是不初始化变量,仅仅返回指向零值的指针:

Go
1
2
// 为类型SyncedBuffer分配对应大小的内存并清零,然后返回一个 * SyncedBuffer
p := new(SyncedBuffer)

不同类型的零值具有不同含义:例如:

  1. sync.Mutex的零值表示解锁状态
  2. bytes.Buffer的零值表示空白的缓冲区。
make

用于创建切片(slice)、映射(map)和通道(chan),并且返回已初始化的目标类型(而非指针)。这种设计的原因是,这三个类型必须在它们引用的数据结构被初始化后才能使用

空白标识符

你可以声明任何类型的空白标识符,也可以将任何表达式赋值给空白标识符。空白标识符使用符号 _表示,可用来无害的丢弃某些值。

空白标识符可以用在多赋值的表达式中,忽略不关心的那些值:

Go
1
2
3
if _, err := os.Stat(path); os.IsNotExist(err) {
    // ...
}

导入包,但是仅仅执行其init函数而不使用其导出的成员:

Go
1
import _ "net/http/pprof"

检查是否实现了指定的接口:

Go
1
2
3
if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler", val, val)
}
类型嵌入 

Go没有提供典型的、类型驱动的子类化机制。但是,你可以通过在结构、接口内部嵌入其它类型,来达到类似于子类化的效果。

注意类型嵌入和子类化的不同:

  1. 被内嵌类型的方法成为外部类型的方法,这个行为和子类化相同
  2. 当方法被调用时,其接收者是被内嵌类型(即方法所来自的那个类型),而不是外部类型,这个行为和子类化不同
  3. 被嵌入类型,对自己被嵌入这件事情毫无感知。因此从被嵌入类型中调用外部类型定义的方法是不可能的
嵌入到接口
Go
1
2
3
4
5
6
7
8
9
10
11
12
13
type Reader interface {
    Read(p []byte) (n int, err error)
}
 
type Writer interface {
    Write(p []byte) (n int, err error)
}
 
// 嵌入:ReadWriter是Reader、Writer接口的联合
type ReadWriter interface {
    Reader
    Writer
}

注意,只有接口才能被嵌入到接口中。 

嵌入到结构
Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Writer struct {
}
 
func (w *Writer) write() {}
 
type Reader struct {
}
 
func (r *Reader) read() {}
 
type ReaderWriter struct {
    *Writer
    *Reader
}
 
func main() {
    // 就像普通字段一样,内嵌的类型也需要初始化,如果不指定,则为零值
    rw := ReaderWriter{&Writer{}, &Reader{}}
    // 可以直接调用内嵌接口的方法
    rw.write()
    rw.read()
}

结构可以同时包含普通字段、内嵌结构:

Go
1
2
3
4
5
6
type Job struct {
    Command string
    *log.Logger
}
 
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

如果需要直接引用内嵌类型,可以使用其类型名:

Go
1
2
//  使用类型名引用,不要导入包前缀
job.Logger.Logf("%q: %s",...)

注意,结构也可以内嵌接口:

Go
1
2
3
4
type Registry struct {
    ClusterID string
    model.Controller  // 类型名Controller
}

结构嵌入接口的意义是,相当于加了个“缺省适配”,结构不必须实现任何方法,仅仅“覆盖”自己关注的方法即可。

引用内嵌字段

内嵌的接口、结构,可以命名,也可以不命名(匿名内嵌)。不命名时,直接以其类型名引用:

Go
1
2
3
4
aggregate.Registry{
    ClusterID:        clusterID,
    Controller:       kubectl,   // 直接通过类型名Controller引用
} 
名字冲突问题

嵌入可能导致接口/结构的成员名冲突(同名),Go基于以下规则解决此冲突:

  1. 越靠嵌套层次外部的成员优先级越高。例如上面的Job内嵌了Logger,如果Logger有一个字段Command,则此字段被Job.Command隐藏
  2.  同一层次下出现重名,通常是一个错误。例如上面的Job内嵌了Logger,它不应该同时有一个名为Logger的方法或者字段。即便如此,冲突的名称只要没有在类型定义外部被引用,就不会导致错误
嵌入指针还是值

根据实际情况:

  1. 如果外层对象以值的形式传来传去,而你需要的内层对象的方法是定义在指针上的,则嵌入指针
  2. 如果外层对象以指针的形式传来传去,则内层对象可以嵌入值,没有问题,你仍然可以访问内层对象的指针方法
  3. 如果内层对象的方法均是值接收者,则嵌入值

如果对象很小,可以考虑嵌入值,这样可以减少内存分配次数,并实现局部访问(内存中比较靠近)。

别名和类型定义
别名

下面的语法是定义别名,两个类型是完全一样的,可以任意替换:

Go
1
type nodeName = string
类型定义

下面的语法是定义一个新类型,两个类型不一样,必须强制转换:

Go
1
2
3
4
type nodeName string
 
var n nodeName = string("xenon")
str := string(n) 

对现有非接口类型进行类型定义,新的类型不会继承原有类型的方法:

Go
1
2
3
4
type myMutex sync.Mutex
 
var mtx myMutex
mtx.Lock() // 错误

要想继承,可以使用匿名嵌套: 

Go
1
2
3
4
5
6
type myMutex struct {  
    sync.Mutex
}
 
var mtx myMutex
mtx.Lock() // OK

但是,对于接口类型进行类型定义,方法则会被继承。 

流程控制
for

Go仅仅支持for这一种循环结构:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 不得使用圆括号包围(初始化语句; 条件表达式; 后置语句),但是循环体必须使用花括号包围
for i := 0; i < 10; i++ {
    fmt.Printf("%v", i)
}
// 初始化语句和后置语句是可选的
i := 0
for ; i < 10; {
    fmt.Printf("%v", i)
    i += 1
}
 
// 上段代码的前后分号可以省略,效果类似于C语言的while
i := 0
for i < 10 {
    fmt.Printf("%v", i)
    i += 1
}
 
// 死循环写法
for {
}
循环变量取地址

不要尝试取循环变量的地址!

在Go语言中,for语句引入的变量在整个迭代过程中是重用的,也就是说,也就是说,每次迭代中进行取地址,总是会指向同一个地址:

Go
1
2
3
4
5
6
7
8
9
10
11
var items []NodeProblemSolverConfig
 
for idx, solverConfig := range items {
    println(&solverConfig)
    createSolver(&solverConfig)
}
 
// 打印
0xc00055eea0
0xc00055eea0
0xc00055eea0

在上面的例子中,期望创建出三个不同配置的Solver对象,而实际上它们引用的Config完全一致。 

解决此问题的方法有两个:

  1. 如果希望得到值副本的指针:
    Go
    1
    2
    copied := solverConfig
    &copied
  2. 如果希望得到原始值的指针:
    Go
    1
    &solverConfig[idx] 
循环和闭包

下面的代码有问题,所有Goroutine打印的都是data最后一个元素的值:

Go
1
2
3
4
5
6
7
8
data =[]int{1,2,3,4,5}
for _,v := range data {
    go func() {
        fmt.Println(v)  // 5 5 5 5 5
    }()
    
    go v.print()    // 同样的问题,但是更加隐蔽
}

解决办法是在循环体内定义局部变量:

Go
1
2
3
4
5
6
for _,v := range data {
    v := v // 每个闭包引用的不是同一变量,这种同名覆盖变量虽然奇怪,在Go里面是惯用法
    go func() {
        fmt.Println(vc)
    }()
} 

或者直接传参给Goroutine:

Go
1
2
3
4
5
for _,v := range data {
    go func(v V) {
        fmt.Println(vc)
    }(v)
}

再次强调:整个循环过程中,for语句引入的迭代变量是的内存地址是不变的,也就是说,整个迭代过程中只有一个变量,每次都在修改同一变量的值,在循环体内,传递此变量的地址是危险的,特别注意闭包这种隐晦的传递地址的形式

if

类似于循环控制语句,分支控制语句也可以包含一个初始化语句:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 圆括号、花括号规则和for相同
// 注意可以在条件表达式之前置一个语句
if i := 0; i < 1 {
    fmt.Print(i)
}
 
i := 0;
if i < 1 {
    fmt.Print(i)
}
 
// if - else if - else
if true {
    
} else if false {
 
} else {
 
}
switch

需要注意,Go中switch语句的case,默认行为是break,而其它很多语言的默认行为是进入下一个case继续判断、执行(注意C语言中,没有break的情况下,一旦某个case匹配,后续所有其它case/default都会执行):

Go
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
// 同样可以具有前置的初始化表达式
switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("OS X")
case "linux":
    fmt.Println("Linux")
    // fallthrough则继续执行下一个分支
    fallthrough
default:
}
 
// switch也可以不带任何条件,用来编写简洁的if-then-else结构:
t := time.Now()
switch {
case t.Hour() < 12:
    fmt.Println("上午好")
case t.Hour() < 17:
    fmt.Println("下午好")
default:
    fmt.Println("晚上好")
}
 
// case合并:
switch pod.Status.Phase {
case v1.PodPending, v1.PodRunning: ...
}
defer 

这个关键字可以让后面的语句延迟执行:

Go
1
2
3
4
5
func main() {
    defer fmt.Println(" World")
    fmt.Println("Hello")
}
// Hello World

直到包围语句的函数(而非代码块)返回之前才执行,因此在循环体中通过defer来进行清理是不可以的。解决办法是把循环体封装为函数。

连续使用defer时,会形成defer栈,先入栈的语句后执行:

Go
1
2
3
4
5
6
func main() {
    for i := 0; i < 10; i += 1 {
        defer fmt.Print(i)   // 不能用这种方式实现每元素迭代后清理
    }
    // 9876543210
}

需要注意,defer的表达式求值发生在声明时,而非实际执行时:

Go
1
2
3
var i int = 1
defer fmt.Println("result =>",func() int { return i * 2 }())  // 打印2而非4
i++ 
defer的陷阱
defer函数调用时机
  1. 包裹defer的函数返回时
  2. 包裹defer的函数执行到末尾时
  3. 所在的goroutine发生panic时
资源清理先判断err再defer

如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作:

Go
1
2
3
4
5
6
7
8
resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
    return err
}
// ...
// 如果操作成功,再进行Close操作
defer resp.Body.Close()
defer函数参数估算时机

defer后面需要跟着函数调用,但是其入参可以是任何表达式。这些表达式的估算时机,是正常执行流(非Defer栈)执行到Defer语句的时候:

Go
1
2
3
4
5
6
7
var buf bytes.Buffer
Println(buf.Len())                 // 0
buf.Write(make([]byte, 10))  
Println(buf.Len())                 // 10
defer Println(buf.Len())           // 10
buf.Write(make([]byte, 10))                          
Println(buf.Len())                 // 20

上面的例子中, defer语句真正执行时使用的buf长度,是第3行write后的长度,而非第6行。

换一种写法:

Go
1
2
3
4
5
6
7
8
9
var buf bytes.Buffer
Println(buf.Len())                 // 0
buf.Write(make([]byte, 10))
Println(buf.Len())                 // 10
defer func() {
    defer Println(buf.Len())       // 20
}()
buf.Write(make([]byte, 10))
Println(buf.Len())                 // 20

这样就可以获得最终buf的长度,因为对buf.Len()的函数调用不会提前发生。

调用os.Exit时defer不会被执行

当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行:

Go
1
2
3
4
5
6
func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}

上面的defer不会执行。 

标签

可以用于goto跳转、for switch、for select跳转:

Go
1
2
3
4
5
6
7
loop:
    for {
        switch {
        case true:
            break loop
        }
    }
goto

可以实现无条件转移:

Go
1
2
3
    goto label
label:
    x := 1
常用库
标准库列表
标准库 说明
archive/tar 对Tar归档格式的支持
archive/zip 对Zip归档格式的支持
bufio 装饰io.Reader/io.Writer
bytes 操控[]byte,也就是字节切片
compress/bzip2 实现bzip2解压缩算法
compress/flate 实现DEFLATE压锁算法
compress/gzip 支持读写gzip格式
compress/lzw 支持读写lzw格式{"userName":"Alex","userAge":31}
compress/zlib 支持读写zlib格式
container/heap 为任何实现了heap接口的类型提供“堆”操作。堆(heap)是实现优先级队列的一种方式
container/list 实现双向链表
container/ring 实现环形列表
context 定义Context类型
crypto/* 加解密相关包
database/sql 为SQL数据库提供一般性接口
database/sql/driver 定义数据库驱动程序需要实现的接口
debug/* 调试相关的包
encoding 编解码(Marshaler/Unmarshaler)相关的通用接口
encoding/base64

支持Base64编码格式:

Go
1
2
// 编码
base64.StdEncoding.EncodeToString([]byte{})
encoding/hex 支持Hex编码格式
encoding/json 支持JSON格式
encoding/xml 支持XML格式
encoding/binary

在[]byte和数字之间进行转换:

errors 包含操控error的函数
flag 实现命令行选项解析的功能
fmt 格式化输入输出,类似于C语言的printf/scanf
hash/* 实现散列算法
html 用于处理HTML的转义
html/template 实现事件驱动的、生成HTML的模板,支持防代码注入
image/* 2D图形库,支持gif/jpeg/png等图像格式
index/suffixarray 基于内存中的suffix array实现对数复杂度的子串搜索
io 提供IO操作原语
io/ioutil 包含一些IO工具函数
log 一个简单的日志包
glog

Google内部C++日志框架的复制品

log/syslog 提供访问系统日志服务的功能
math 包含基本的常量和数学函数
math/big 支持任意精度的算术运算
math/bits 支持按位的计算
math/cmplx 支持复数的计算
math/rand

支持伪随机数

mime 实现了部分MIME规范
mine/multipart 支持MIME Multipart解析
net 提供可移植的网络IO接口,包括TCP/IP、UDP、Unix域套接字、DNS解析
net/http 提供HTTP客户端和服务器实现
net/http/cgi 提供CGI实现
net/http/fcgi 实现FastCGI协议
net/http/httptest 提供用于HTTP测试的工具
net/http/httptrace 在HTTP客户端请求内部追踪事件
net/http/httputil HTTP相关工具函数
net/http/pprof 收集HTTP服务器运行时信息,供pprof分析
net/mail 电子邮件解析/extend-kubernetes-with-custom-resources
net/smtp SMTP协议支持
net/rpc 支持跨越网络来访问对象导出的方法
net/rpc/jsonrpc 实现基于JSON-RPC 1.0的服务器/客户端编码方式
net/url 解析URL
os 平台独立的操作系统功能函数
os/exec 运行外部命令
os/signal

支持访问发送到当前进程的信号

os/user 根据名称或者ID来查找OS用户
path 操控基于 / 的路径
path/filepath 操控文件名路径
reflect 实现了运行时反射,允许程序操控任何类型的对象
regexp 提供正则表达式支持
runtime 包含用于和Go运行时系统进行交互的操作,例如控制Goroutine
sort 提供排序切片或者用户定义集合的原语
strconv

为基本数据类型提供到/从字符串展现的转换

Go
1
2
3
4
// bool转换为字符串
strconv.FormatBool(v)
// 字符串转换为int
strconv.Atoi("1")
strings 提供操控基于UTF-8的字符串的函数
sync 提供基本的同步原语,例如互斥量
sync/atomic 提供低级别的原子内存原语,用于设计同步算法
syscall 支持低级别的系统调用
testing 用于支持Go包的自动化测试
time 操控和访问时间
unicode 提供对Unicode的支持
unsafe 用于绕开Go的类型安全机制
built-in
true/false

这两个在Go语言中是常量,其声明如下:

Go
1
2
3
4
const (
    true  = 0 == 0
    false = 0 != 0
)
iota

用在多常量声明中,其值为当前常量值的索引:

Go
1
2
3
4
5
6
7
8
9
10
11
const (
    x = 100
    a = iota
    b = iota
    c
    d
)
 
func main() {
    fmt.Printf("%v %v %v %v %v", x, a, b, c, d) // 100 1 2 3 4
}
nil

预定义的标识符,表示指针、通道、函数、接口、映射或者切片的零值。

append

函数签名: func append(slice []Type, elems ...Type) []Type

将元素附加到切片的尾部,如果切片容量足够,它被重切已满足新元素。如果容量不足,底层数组被重新分配

支持把字符串添加到[]byte切片中:

Go
1
slice = append([]byte("hello "), "world")
cap

函数签名: func cap(v Type) int

获取数组、数组指针(指向的数组)、切片、缓冲通道的容量。

close

用于关闭通道

complex

函数签名: func complex(r, i FloatType) ComplexType

用于创建复数

函数 func imag(c ComplexType) FloatType返回复数的虚数部分

函数 func real(c ComplexType) FloatType返回复数的实数部分

copy

函数签名: func copy(dst, src []Type) int

从源切片拷贝元素到目标切片,也可以用来拷贝字符串到[]byte。返回实际拷贝的元素个数。

delete

函数签名: func delete(m map[Type]Type1, key Type)

用于从映射中删除条目。

len

函数签名: func len(v Type) int

返回数组、数组指针、切片、字符串(字节数)、缓冲通道的元素个数。如果v为nil则返回0

make

函数签名: func make(t Type, size ...IntegerType) Type

为切片、映射、通道分配内存空间并且初始化。

new

函数签名: func new(Type) *Type

为指定的类型分配内存

panic

此内置函数用于产生一个运行时异常,通常会导致程序停止运行。此函数接收一个任意类型的参数,参数的字符串形式会被打印到控制台上。

recover

当panic被调用时,不管是否显式调用,当前函数的执行会立即停止,并Unwind调用栈,调用defer函数。如果Unwinding到达Goroutine的栈顶,则程序终止。

使用内置函数recover可以从Unwinding中重新获得程序控制权,恢复正常执行流。recover函数终止Unwind并捕获panic的参数作为返回值,它仅仅能在defer函数体内调用。

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Request struct {
}
 
func (request *Request) process() {
    panic("Request process failed.")
}
 
func Worker(req *Request) {
    // 必须在defer中调用,recover类似于Java中的catch
    defer func() {
        // 如果当前没有panic,则recover返回nil
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    req.process()
}
print/println

内置的打印函数:

Go
1
2
func print(args ...Type)
func println(args ...Type)
uintptr

一个整数类型,足够大以存放任何位模式的指针。

error

这是一个内置的接口:

Go
1
2
3
type error interface {
    Error() string
}

fmt包在打印时,会检测变量是否实现了该接口。

很多函数都会返回error值,调用者应该检查该值是否为nil,从而判断调用是否成功:

Go
1
2
3
4
5
6
7
i, err := strconv.Atoi("one")
// 非零值表示调用失败
if err != nil {
    fmt.Printf("%v\n", err)
    return
}
fmt.Println(i)
flag

解析命令行参数,支持短参数(-h)和长参数(--help),支持默认值、命令帮助:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "flag"
 
var (
    masterURL string
    help      bool
)
 
func main() {
    // 注册命令行参数:存放到的目标变量的指针、参数名、默认值、帮助信息
    flag.StringVar(&masterURL, "masterURL", "", "URL of kubernetes master")
    flag.Parse() // 解析
    if help {
        flag.Usage() // 打印帮助
    }
} 
log

该包提供了基本的日志记录功能。示例用法:

Go
1
2
3
log.Printf("Process id is %v", os.Getpid())
log.Fatalf("Parent PID is %v", os.Getppid())  // 打印信息并执行panic()抛出恐慌
log.Fatal("Fatal error")                      // 打印信息并执行os.Exit(1)

输出内容比fmt.Printf多了当前时间的前缀。要配置输出格式,可以:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 前缀日期和代码位置
func init(){
    log.SetFlags(log.Ldate | log.Lshortfile)
}
// 2016/08/05 application.go:10: Process id is 10147
 
// 所有可用的标记:
const (
    Ldate = 1 << iota //日期,示例2017/01/01
    Ltime //时间,示例08:08:08
    Lmicroseconds //微秒
    Llongfile //绝对路径和行号
    Lshortfile //文件和行号
    LUTC //日期时间,UTC时间
    LstdFlags = Ldate | Ltime
)
reflect

该包为Go语言提供了反射功能。

分类识别

例如判断对象是不是一个指针:

Go
1
2
3
if reflect.ValueOf(req).Kind() != reflect.Ptr {
  
} 
类型识别
Go
1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
    Name string
    Age  uint8
}
 
var alex interface{}
alex = User{"Alex", 31}
 
// 获取任意对象的具体类型
alexType := reflect.TypeOf(alex)
intType := reflect.TypeOf(1)
fmt.Println(alexType, intType) // main.User int
值识别

Value是一个结构,它保存任何一个对象的全部信息。

调用reflect.ValueOf()可以将任意对象转换为reflect.Value

Go
1
2
3
4
5
alexValue := reflect.ValueOf(alex)
fmt.Printf("%T=%v\n", alexValue, alexValue) // reflect.Value={Alex 31}
 
// 下面的调用返回零值
ValueOf(nil)

要从Value获得原始对象,可以调用Value.Interface()方法:

Go
1
2
3
// 获得原始对象 interface{} 然后将其Cast为真实类型
alex, _ = alexValue.Interface().(User)
fmt.Printf("%T=%v\n", alex, alex)           // main.User={Alex 31} 

要判断一个值是否为零值,使用下面的代码:

Go
1
2
3
4
func IsZero(v reflect.Value) bool {
    // 零值是无效值,但是这样判断不足够, 还需要将  当前值      和   类型的零值    进行比较
    return !v.IsValid() || reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}
获取分类
Go
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
fmt.Println(alexType.Kind())  // struct
fmt.Println(alexValue.Kind()) // struct
 
// 底层分类列表
const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)
读写字段 
Go
1
2
3
4
// 遍历字段
for i := 0; i < alexType.NumField(); i++ {
    fmt.Println(alexType.Field(i).Name)
}

写入结构的字段:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type User struct {
    Name string
    Age  uint8
}
 
alex := User{"Alex", 31}
 
// 为了修改对象的字段,必须传递指针(可寻址)
alexPtrValue := reflect.ValueOf(&alex)
 
// Elem()返回interface包含的Value、或者指针指向的Value
alexValue := alexPtrValue.Elem()
if alexValue.Kind() == reflect.Struct {
    nameField := alexValue.FieldByName("Name")
    // 可设置的前提是:字段已经导出、对象可寻址
    if nameField.CanSet() {
        nameField.SetString("Alex Wong")
    }
}
 
fmt.Println(alex) // {Alex Wong 31}

写入基本类型:

Go
1
2
3
age := 31
reflect.ValueOf(&age).Elem().SetInt(32)
fmt.Println(age) // 32
调用方法

要调用方法,首先需要获取方法的Value对象:

Go
1
2
alexPtrValue := reflect.ValueOf(&alex)
sayHelloMthd := alexPtrValue.MethodByName("SayHello")

MethodByName根据名称来检索一个Value(它必须是接口、结构等支持方法的对象)的方法,方法也是Value对象,但是它可被调用: 

Go
1
2
3
4
5
6
7
8
args := []reflect.Value{
    reflect.ValueOf("Meng"),
}
 
if sayHelloMthd.IsValid() {
    rets := sayHelloMthd.Call(args)
    println ( rets[0].Interface().(string))
}

总之,方法、方法的参数、方法的返回值,都是Value。Value和实际接口/结构之间可以转换。

读取字段标签 
Go
1
2
3
alex := User{"Alex", 31}
tag := reflect.TypeOf(alex).Field(0).Tag
fmt.Println(tag) // json:"userName"

很多现有的包都会利用字段标签,例如:

Go
1
2
3
json, _ := json.Marshal(alex)
fmt.Printf("%v", string(json))
// {"userName":"Alex","userAge":31}
unsafe

使用该包可以绕过Go的内存安全机制,直接对内存进行读写。

Sizeof

此函数返回一个类型占用的内存空间大小:

Go
1
2
3
4
fmt.Println(unsafe.Sizeof(1))        // 8
fmt.Println(unsafe.Sizeof('1'))      // 4
fmt.Println(unsafe.Sizeof("1"))      // 16
fmt.Println(unsafe.Sizeof(new(int)))    // 8

注意返回值仅仅和类型有关,和值没有任何关系。

Alignof

此函数返回一个类型的对齐系数(对齐倍数):

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var b bool
var i8 int8
var i16 int16
var i64 int64
var f32 float32
var s string
var m map[string]string
var p *int32
fmt.Println(unsafe.Alignof(b))       // 1
fmt.Println(unsafe.Alignof(i8))      // 1
fmt.Println(unsafe.Alignof(i16))     // 2
fmt.Println(unsafe.Alignof(i64))     // 8
fmt.Println(unsafe.Alignof(f32))     // 4
fmt.Println(unsafe.Alignof(s))       // 8      
fmt.Println(unsafe.Alignof(m))       // 8
fmt.Println(unsafe.Alignof(p))       // 8

对齐倍数都是2的幂,一般不会超过8。你也可以基于反射来获取对齐倍数:

Go
1
reflect.TypeOf(x).Align()
Offsetof 

此函数用于获取结构中字段相对于结构起始内存地址的偏移量:

Go
1
2
3
4
5
6
7
type User struct {
    age  uint8
    name string
}
alex := User{}
fmt.Println(unsafe.Offsetof(alex.age))   // 0
fmt.Println(unsafe.Offsetof(alex.name))  // 8

你也可以基于反射来获取偏移量:

Go
1
reflect.TypeOf(u1).Field(i).Offset
Pointer 

unsafe.Pointer是一种特殊的指针,它可以指向任何的类型,类似于C语言中的void*

不同具体类型的指针是无法相互转换的:

Go
1
2
3
var uip *uint8 = new(uint8)
var fip *float32 = new(float32)
uip = fip  // cannot use fip (type *float32) as type *uint8 in assignment

但是unsafe.Pointer却可以和任何指针类型相互转换,包括uintptr:

Go
1
2
3
4
5
var uip *uint8 = new(uint8)
var fip *float32 = new(float32)
*fip = 3.14
uip = (*uint8)(unsafe.Pointer(fip))
fmt.Println(*uip)

*T不支持算术运算,但是uintptr却可以。利用unsafe.Pointer作为媒介,我们可以获得精确控制内存的能力:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
    age  uint8
    name string
}
user := new(User)
 
page := (*uint8)(unsafe.Pointer(user))
*page = 31
 
os := unsafe.Offsetof(user.name)
// 为了进行指针算术运算,必须使用uintptr
base := uintptr(unsafe.Pointer(user))
pname := (*string)(unsafe.Pointer(base + os))
*pname = "Alex"
 
fmt.Println(*user) 
encoding/json
Go
1
2
3
4
5
6
7
// 序列化,缩进4空格
b, _ := json.Marshal(result)
var out bytes.Buffer
json.Indent(&out, b, "", "    ")
out.WriteTo(writer)
// 序列化,缩进4空格
json.MarshalIndent(data, "", " ")

对通过HTTP传递来的字节流解码为JSON,数字的真实类型可能是float64:

Go
1
2
3
4
5
res := make(map[string]interface{})
err = json.Unmarshal(respBytes, &res)
// id虽然是909,但是实际上是float64
id := res["id"]
task.testId = int(id.(float64)) 
time
Go
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
n := time.Now()
// 格式化,注意layout中的时间值不能改
FMT := "2006-01-02 15:04:05"
println(n.Format(FMT))          // 2018-02-11 18:16:47
println(n.Format("2006/1/2"))   // 2018/2/11
println(n.Format("2006/01/02")) // 2018/02/11
println(n.Format("15:04:05"))   // 18:17:13
 
// 解析
n, _ = now.Parse("2018-01-02 18:16:47")
 
println(n.Format(FMT)) // 2018-01-02 00:00:00
 
// 指定解析使用的格式
time.Parse("2006-01-02T15:04:05.999999Z", str)
time.Parse("2006-01-02T15:04:05Z07:00", str)
 
// 转换为整数
println(n.Unix())               // UNIX纪元,秒
println(n.UnixNano() / 1000000) // UNIX纪元,毫秒
 
// 获取各字段 2018 January 2 18 16 47
fmt.Printf("%v %v %v %v %v %v ", n.Year(), n.Month(), n.Day(), n.Hour(), n.Minute(), n.Second())
 
// ProtoBuf时间戳
time := protobuf.Timestamp{Seconds: n.Unix()}

time.Duration表示一个时间长度:

Go
1
2
// 解析,可以使用s、m、h等单位
d, _ := time.ParseDuration("1s")

它的本质就是纳秒数:

Go
1
2
3
4
5
6
7
8
9
10
type Duration int64
 
const (
    Nanosecond Duration = 1
    Microsecond = 1000 * Nanosecond
    Millisecond = 1000 * Microsecond
    Second = 1000 * Millisecond
    Minute = 60 * Second
    Hour = 60 * Minute
)

你可以将Duration和整数进行算术运算:

Go
1
2
var d1 time.Duration = (60 * 1000) * time.Millisecond
var d2 time.Duration = 2 * time.Minute

也可以计算两个时间点之间的差距:

Go
1
var duration time.Duration = endingTime.Sub(startingTime)
math/rand

提供伪随机数的支持。

注意随机数不随机的问题:

Go
1
2
glog.Info(rand.Intn(10))
glog.Info(rand.Intn(10))

以上代码无论运行多少次,都输出1、7。原因是使用的“源”是固定的。因此,要产生每次调用都不一样的随机数,需要每次使用不同的源:

Go
1
2
3
4
5
6
7
source := rand.NewSource(time.Now().Unix())
gen := rand.New(source)
glog.Info(gen.Intn(1000))
 
// 或者,更简单的,你可以为默认源设置新的种子
rand.Seed(time.Now().UnixNano())
rand.Intn(100)
os/exec

执行一段Bash脚本:

Go
1
2
3
4
5
6
7
8
9
10
ctx, _ := context.WithTimeout(context.TODO(), s.actionTimeout)
// 支持超时控制
cmd := exec.CommandContext(ctx, s.shellPath, script)
// 可以设置环境变量
cmd.Env = os.Environ()
for key, val := range vars {
    cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val))
}
// 执行
cmd.Run()
输入输出
Go
1
2
3
4
5
6
7
cmd := exec.Command("tr", "a-z", "A-Z")
// 提供输入
cmd.Stdin = strings.NewReader("some input")
// 指定输出缓冲
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()

也可以直接将标准输出/标准错误一起作为返回值:

Go
1
2
3
4
5
cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
// 执行并收集标准输出+标准错误
stdoutStderr, err := cmd.CombinedOutput()
// 执行并收集标准输出
stdout, err := cmd.Output()
非阻塞

要非阻塞的启动命令,但是不等待其返回,可以: 

Go
1
2
cmd := exec.Command("sleep", "5")
err := cmd.Start()

你可以稍后等待其结束: 

Go
1
cmd.Wait()
os/signal

支持访问发送到当前进程的信号,示例代码:

Go
1
2
3
4
5
6
7
8
9
10
11
12
/* 将接收到的SIGINT、SIGTERM信号中继给chan os.Signal */
relayCh := make(chan os.Signal, 2)
signal.Notify(relayCh, os.Interrupt, syscall.SIGTERM)
go func() {
    <-relayCh
    close(stopCh) // stopCh供应用中其它协程读取
    <-relayCh
    // 再次收到信号,强制退出进程
    os.Exit(1)
}()
// 协程可以循环执行逻辑,直到stopCh关闭
wait.Until(ctrl.runWorker, time.Second, stopCh)
strconv

为基本数据类型提供到/从字符串展现的转换,主要函数:

Go
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
// 将布尔值转换为字符串 true 或 false
func FormatBool(b bool) string
// 将字符串转换为布尔值
// 接受真值:1, t, T, TRUE, true, True
// 接受假值:0, f, F, FALSE, false, False
// 其它任何值都返回一个错误。
func ParseBool(str string) (bool, error)
 
// 将整数转换为字符串形式。base 表示转换进制,取值在 2 到 36 之间
// 结果中大于 10 的数字用小写字母 a - z 表示
func FormatInt(i int64, base int) string
func FormatUint(i uint64, base int) string
// 将字符串解析为整数,ParseInt 支持正负号,ParseUint 不支持正负号
// base 表示进位制(2 到 36),如果 base 为 0,则根据字符串前缀判断
// 前缀 0x 表示 16 进制,前缀 0 表示 8 进制,否则是 10 进制
// bitSize 表示结果的位宽(包括符号位),0 表示最大位宽
func ParseInt(s string, base int, bitSize int) (i int64, err error)
func ParseUint(s string, base int, bitSize int) (uint64, error)
 
// 将整数转换为十进制字符串形式。即:FormatInt(i, 10)
func Itoa(i int) string
// 将字符串转换为十进制整数。即:ParseInt(s, 10, 0)
func Atoi(s string) (int, error)
 
 
// FormatFloat 将浮点数 f 转换为字符串形式
// f:要转换的浮点数
// fmt:格式标记(b、e、E、f、g、G)
// prec:精度(数字部分的长度,不包括指数部分)
// bitSize:指定浮点类型(32:float32、64:float64),结果会据此进行舍入
//
// 格式标记:
// 'b' (-ddddp±ddd,二进制指数)
// 'e' (-d.dddde±dd,十进制指数)
// 'E' (-d.ddddE±dd,十进制指数)
// 'f' (-ddd.dddd,没有指数)
// 'g' ('e':大指数,'f':其它情况)
// 'G' ('E':大指数,'f':其它情况)
//
// 如果格式标记为 'e','E'和'f',则 prec 表示小数点后的数字位数
// 如果格式标记为 'g','G',则 prec 表示总的数字位数(整数部分+小数部分)
// 参考格式化输入输出中的旗标和精度说明
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
// 将字符串解析为浮点数,使用 IEEE754 规范进行舍入
// bigSize 取值有 32 和 64 两种,表示转换结果的精度
// 如果有语法错误,则 err.Error = ErrSyntax
// 如果结果超出范围,则返回 ±Inf,err.Error = ErrRange
func ParseFloat(s string, bitSize int) (float64, error)

代码示例:

Go
1
2
strconv.ParseInt("FF", 16, 0)  // 255
strconv.ParseInt("0xFF", 0, 0) // 255
encoding/binary

在[]byte和数字之间进行转换:

Go
1
2
3
var a []byte = []byte{0, 1, 2, 3}
fmt.Println(binary.BigEndian.Uint32(a))
fmt.Println(binary.LittleEndian.Uint32(a))
glog

Google内部C++日志框架的Go语言复制品:

  1. Info/Warning/Error/Fatal函数,以及Infof等格式化版本
  2. V风格的日志控制(使用命令行选项-v和-vmodule=file=2)

代码示例:

Go
1
2
3
4
5
glog.Fatalf("执行失败: %s", err)
if glog.V(2) {
    glog.Info("执行成功")
}
glog.V(2).Infoln("处理了", nItems, "个条目")

日志被缓冲,并周期性的Flush,程序退出前你应该手工调用Flush。默认情况下,所有日志被写入到临时目录的文件中。

你可以通过命令行选项来改变glog的行为, flag.Parse应该在任何日志打印函数调用前调用:

命令行选项示例 说明
--logtostderr=false 日志被输出到标准错误而非文件
--alsologtostderr 同时输出到文件和标准错误
--stderrthreshold=ERROR 输出到标准错误的最低严重级别
--log_dir="" 日志输出目录
--v=0 输出日志的最低冗余级别
-vmodule=gopher*=3 对于gopher开头的Go文件,其最低冗余级别设置为3
encoding/json

支持JSON的串行化、反串行化。下面的代码示例将JSON字符串反串行化为map:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
import (
    "encoding/json"
    "github.com/sanity-io/litter"
    "io/ioutil"
)
 
func main() {
    data, _ := ioutil.ReadFile("configdump.json")
    config := make(map[string]interface{})
    // 转换JSON为MAP
    json.Unmarshal(data, &config)
    litter.Dump(config)
}
gopkg.in/yaml.v2

支持YAML格式的串行化、反串行化。下面的代码示例将map串行化为YAML:

Go
1
2
3
4
5
6
7
8
9
10
11
12
import (
    "gopkg.in/yaml.v2"
    "io/ioutil"
    "os"
)
 
func main() {
    config := make(map[string]interface{})
    // 转换MAP为YAML
    data, _ := yaml.Marshal(config)
    ioutil.WriteFile("configdump.yaml", data, os.ModePerm)
}
基本工具库
huandu/xstrings

包含各种各样的字符串处理函数。

elliotchance/pie

专门用于切片成立,专注于类型安全、性能、不可变性。

包含一些内置的类型定义,不需要 go generate即可使用:

Go
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
typeStrings[]string
typeFloat64s[]float64
typeInts[]int
 
 
// 示例
package main
 
import (
    "fmt"
    "strings"
 
    "github.com/elliotchance/pie/pie"
)
 
// 示例
func main() {
    name := pie.Strings{"Bob", "Sally", "John", "Jane"}.
        // 过滤
        FilterNot(func (name string) bool {
            return strings.HasPrefix(name, "J")
        }).
        // 变换
        Map(strings.ToUpper).
        // 取最后一个元素
        Last()
 
    fmt.Println(name) // "SALLY"
}

如果希望为任何自定义的类型生成上面这样的接口需要添加注释:

Go
1
2
3
4
5
6
type Car struct {
    Name, Color string
}
 
//go:generate pie Cars.*
type Cars []Car

然后执行 go generate会生成 cars_pie.go文件,其中定义了上面那样的接口。

你还可以自定义 Equality、Strings等方法,实现类似于Java的灵活性:

Go
1
2
3
4
5
6
7
8
9
type Car struct {
    Name, Color string
}
 
type Cars []*Car // ElementType is *Car
 
func (c *Car) Equals(c2 *Car) bool {
    return c.Name == c2.Name
}
thoas/go-funk

一个工具箱函数库,主要用于集合操作:

  1. 元素存在性判断、索引判断
  2. 结构转为map,map转为slice
  3. 任何可迭代对象的元素查找、过滤、迭代、去重
函数 说明
Contains 检查元素是否存在(于切片 、映射、数组)
IndexOf
LastIndexOf
获得元素的索引或 -1
ToMap 将一个字段作为pivot(键),转换结构为map
Filter 使用Predicate函数来过滤切片
Find 使用Predicate函数来查找切片元素
Map 在映射、切片之间进行相互转换
Get 使用路径导航的形式检索值: funk.Get(foo, "Bar.Bars.Bar.Name")
Keys 获取结构、映射的字段名/键数组
Values 获取结构、映射的字段值、值数组
ForEach 迭代映射、切片
ForEachRight 反向迭代映射、切片
Chunk 将切片划分为子切片,每个 子切片具有指定的大小
FlattenDeep 递归的扁平化多维数组
Uniq 数组去重
Drop 去除数组/切片的前N个元素
Initial 获取数组/切片除后N个的全部元素
Tail 获取数组/切片除前N个的全部元素
Shuffle 将指定数组进行元素重排,放到新数组中
Sum 数组元素求和
Reverse 获取反向数组
SliceOf 从单个元素创建切片
RandomInt 获得一个随机整数
RandomString 获得一个固定长度的随机字符串
mholt/archiver
压缩/解压

能够快速的压缩/解压缩.zip, .tar, .tar.gz, .tar.bz2, .tar.xz, .tar.lz4, .tar.sz等格式,还能够解压缩.rar格式。示例:

Go
1
2
3
4
5
6
import "github.com/mholt/archiver"
 
// 压缩
err := archiver.Archive([]string{"testdata", "other/file.txt"}, "test.zip")
// 解压缩
err = archiver.Unarchive("test.tar.gz", "test")

上面的代码自动根据扩展名来判断使用何种归档、压缩算法。你也可以明确指定需要使用的算法:

Go
1
2
3
4
5
6
7
8
9
10
z := archiver.Zip{
    CompressionLevel:       flate.DefaultCompression,
    MkdirAll:               true,
    SelectiveCompression:   true,
    ContinueOnError:        false,
    OverwriteExisting:      false,
    ImplicitTopLevelFolder: false,
}
 
err := z.Archive([]string{"testdata", "other/file.txt"}, "/Users/matt/Desktop/test.zip")
探看文件

下面是探看Zip中文件条目的例子:

Go
1
2
3
4
5
6
7
err = z.Walk("/Users/matt/Desktop/test.zip", func(f archiver.File) error {
    zfh, ok := f.Header.(zip.FileHeader)
    if ok {
        fmt.Println("Filename:", zfh.Name)
    }
    return nil
})
写入HTTP响应
Go
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
err = z.Create(responseWriter)
if err != nil {
    return err
}
defer z.Close()
 
for _, fname := range filenames {
    info, err := os.Stat(fname)
    if err != nil {
        return err
    }
    
    // 获取文件在归档文件中的内部名称
    internalName, err := archiver.NameInArchive(info, fname, fname)
    if err != nil {
        return err
    }
 
    // 打开文件
    file, err := os.Open(f)
    if err != nil {
        return err
    }
 
    // 写入到文件
    err = z.Write(archiver.File{
        FileInfo: archiver.FileInfo{
            FileInfo:   info,
            CustomName: internalName,
        },
        ReadCloser: file,
    })
    file.Close()
    if err != nil {
        return err
    }
}
archive/tar

处理Tar格式的归档,写示例:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var buf bytes.Buffer
var chart []byte = []byte{1, 2, 3}
w := tar.NewWriter(&buf)
// 写入条目
// 写入头
w.WriteHeader(&tar.Header{
    Name: "Chart.yaml",
    Size: int64(len(chart)),
})
// 写入体
w.Write(chart)
// 刷出
w.Close()
// 写到文件
ioutil.WriteFile("/tmp/chart.tar", buf.Bytes(), fileutil.PrivateFileMode)

读示例:

Go
1
2
3
4
5
6
7
tr := tar.NewReader(buf)
// 处理下一个条目
header, err := tr.Next()
println( header.Name )
buf := make([]byte, header.Size)
// 读取此条目的内容到缓冲区
tr.Read(buf)
compress/gzip

处理Gzip压缩格式,下面是写tar.gz(tgz)的例子:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
// 装饰
tw := tar.NewWriter(gw)
 
yamlBytes := []byte("AppVersion: 1.0.0")
tw.WriteHeader(&tar.Header{
    Name: "Chart.yaml",
    Size: int64(len(yamlBytes)),
    Mode: 0666,
})
tw.Write(yamlBytes)
tw.Close()
gw.Close()
 
ioutil.WriteFile("/tmp/chart.tgz", buf.Bytes(), 0666) 

下面是读的例子:

Go
1
2
3
4
5
6
7
gzipBuf := bytes.NewBuffer(req.Zip)
gr, err := gzip.NewReader(gzipBuf)
if err != nil {
    // 说明是无效GZIP格式
}
defer gr.Close()
tr := tar.NewReader(gr)
数据绑定/拷贝
mitchellh/mapstructure

支持将一般性的Map解码为Go结构,或者反向转换。示例:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
item := make(map[string]interface{}]
//...
// 创建一个解码器
decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
    Metadata: nil,
    Result:   &chart,
    // 定制类型转换器
    DecodeHook: func(from reflect.Type, to reflect.Type, v interface{}) (interface{}, error) {
        if to.String() == "*timestamp.Timestamp" {
            return parseTime(v.(string)), nil
        }
        // 不转换,直接返回原始值
        return v, nil
    },
})
// 执行解码
decoder.Decode(item)
mitchellh/copystructure

深拷贝(DeepCopy)对象,不管是map、slice、pointer都能递归的解引用并正确拷贝。示例:

Go
1
cs, _ := copystructure.Copy(allChartsProto)
imdario/mergo

用于合并结构、映射。非导出字段不会被合并,导出字段则会被递归的合并:

Go
1
2
3
4
5
6
7
8
// 合并
if err := mergo.Merge(&dst, src); err != nil {
}
// 将映射合并到结构
if err := mergo.Map(&dst, srcMap); err != nil {
}
// 也可以将映射合并到映射,一定要记得对映射取地址(Why?本身就是传递引用)
mergo.Map(&dstMap, srcMap)

需要注意的是:将结构合并到映射时,不会递归合并。

你还可以提供转换器,指定特定类型字段在合并之前如何转换:

Go
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
import (
    "fmt"
    "github.com/imdario/mergo"
        "reflect"
        "time"
)
 
type transfomer struct {
}
 
func (t timeTransfomer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
       // 时间类型的转换器
    if typ == reflect.TypeOf(time.Time{}) {
        return func(dst, src reflect.Value) error {
            if dst.CanSet() {
                                // 判断是否零值的方法
                isZero := dst.MethodByName("IsZero")
                result := isZero.Call([]reflect.Value{})
                if result[0].Bool() {
                    dst.Set(src)
                }
            }
            return nil
        }
    }
    return nil
}
 
type Snapshot struct {
    Time time.Time
}
 
func main() {
    src := Snapshot{time.Now()}
    dest := Snapshot{}
    mergo.Merge(&dest, src, mergo.WithTransformers(transfomer{}))
    fmt.Println(dest)
}
和C/C++交互
对C/C++的支持

你可以导入伪包“C”来使用C语言中定义的类型,此导入前面紧跟着的注释叫做Preamble。Preamble中可以包含任意C代码,包括函数、变量声明和定义:

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
32
33
package main
 
/*
typedef int (*accumulator) (int a, int b);
 
int add(int a, int b){
    return a+b;
}
int reduce(accumulator acc,int a,int b){
    return acc(a,b);
}
*/
// 伪包C
import "C"
import "fmt"
import "unsafe"
 
func main() {
    // 通过伪包C来调用C。C.accumulator获取reduce的函数指针
    f := C.accumulator(C.add)
    // 调用C函数
    fmt.Printf("%v", C.reduce(f, 1, 2)) // 3
    
    // 创建一个C字符串(char*)
    cs := C.CString("Hello World")
    // 释放C内存
    C.free(unsafe.Pointer(cs))
    
    // 将C分配的内存授予Go指针
    p := (*int)(C.malloc(4))
    // 释放C分配的内存
    C.free(unsafe.Pointer(p))
}

在构建时,一旦go发现 import "C"语句,就会寻找目录中的非go源码。c/s/S扩展名的文件会使用C编译器编译,cc/cpp/cxx文件会使用C++编译器编译。

CFLAGS, CPPFLAGS, CXXFLAGS, FFLAGS, LDFLAGS等环境变量可以通过伪指令#cgo来定义:

Go
1
2
3
4
5
6
7
8
9
10
11
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"
 
 
// 通过pkg-config获得CPPFLAGS和LDFLAGS,默认pkg-config工具可以通过PKG_CONFIG获得
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"

在构建时,CGO_CFLAGS, CGO_CPPFLAGS, CGO_CXXFLAGS,CGO_FFLAGS,CGO_LDFLAGS 等环境变量被附加到上述指令引入的环境变量之后,构成构建C代码所需要的完整变量。特定于包的标记应该通过伪指令而非环境变量设置。

包中所有CPPFLAGS、CFLAGS指令用于编译包中的C源码,所有CPPFLAGS、CXXFLAGS指令则用于编译C++源码。

程序中所有包的LDFLAGS指令会被连接在一起,并在链接阶段使用。

#cgo伪指令中可以包括一些变量,${SRCDIR}会被替换为源码目录的绝对路径:

Go
1
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
cgo

这是Go提供的用于支持C/C++的工具。在进行本地编译时,该工具默认启用;在进行交叉编译时,该工具默认禁用。

设置环境变量CGO_ENABLED可以控制是否使用cgo,设置为1启用,设置为0禁用。

Go调用C

在Go文件中,如果需要访问的C结构字段名属于Go关键字,可以前缀一个 _来访问。C结构中无法在Go语言中解释的字段被忽略(例如bitfield)。

类型映射关系

标准的C数字类型映射到的Go类型如下表:

C类型 Go类型 备注
char C.char  
signed char C.schar  
unsigned char C.uchar  
short C.short  
unsigned short C.ushort  
int C.int  
unsigned int C.uint  
long C.long  
unsigned long C.ulong  
long long C.longlong /extend-kubernetes-with-custom-resources 
unsigned long long C.ulonglong  
float C.float  
double C.double  
complex float C.complexfloat  
complex double C.complexdouble  
void* unsafe.Pointer 任何指针都可以转换为unsafe.Pointer,unsafe.Pointer也可以转换为任何类型的指针
__int128_t __uint128_t [16]byte  
char* C.CString  

此外需要注意:

  1. 如果需要直接访问C的struct、union、enum类型,为类型名字前缀struct_、union_、enum_
  2. 要获得任何C类型的尺寸,调用C.sizeof_T
  3. 由于Go不支持C的联合体,因此联合体在Go中映射为等长度的字节数组
  4. Go的结构不能包含C类型的内嵌字段
  5. cgo把C类型转换为非导出的Go类型,因此任何Go包都不能在其导出API中暴露C类型。一个包中的C类型,和另一个包中的同名C类型是不一样的
  6. 在多赋值上下文中,可以调用任何C函数,第一个变量赋值为C函数返回值,第二个变量赋值为C函数调用的errno
  7. 目前不支持调用C函数指针,但是你可以定义Go变量来引用函数指针。C函数可以调用这种来自Go的函数指针
  8. C的函数,如果参数是定长数组,实参传递指向某个数组的首元素指针。在Go中调用这类函数时,你需要明确的传递数组首元素的指针: C.f(&C.x[0])
  9. 调用C.malloc时,不会直接调用C库中的malloc,二是调用Go对malloc的包装函数。此包装函数保证C.malloc调用不会返回nil。如果内存不足,程序直接崩溃
链接到C库

可以仅仅在Go代码中声明C头文件,然后再链接到对应的C库:

Go
1
2
3
4
5
6
7
8
// 在当前目录下寻找libxxx进行链接
// #cgo LDFLAGS: -L ./ -lxxx
// #include "xxx.h"
import "C"
 
func main() {
    C.xxx()
}
C调用Go

使用注释来标注某个函数可以导出供C使用:

Go
1
2
3
4
5
//export MyFunction
func MyFunction(arg1, arg2 int, arg3 string) int64 {...}
 
//export MyFunction2
func MyFunction2(arg1, arg2 int, arg3 string) (int64, *C.char) {...}

在头文件_cgo_export.h中,可以看到如下形式的声明:

_cgo_export.h
C
1
2
3
extern int64 MyFunction(int arg1, int arg2, GoString arg3);
// 多返回值函数,其返回值被包装为结构体
extern struct MyFunction2_return MyFunction2(int arg1, int arg2, GoString arg3);
指针的传递 

Go是支持垃圾回收的语言,它需要知道每个指向Go内存的指针的具体位置,因此,在C和Go代码之间传递指针受到限制。

如果一个Go指针指向的内存不包含任何Go指针,则该Go指针可以传递给C代码。

C代码不应该存储任何Go指针(因为其由Go的垃圾回收器管理)。当将结构的某个字段的指针传递给C时,存在问题的是结构的某个字段;当将Go数组、切片的指针传递给C时,存在问题的是整个数组或切片。在调用完毕后,C代码不应该保留任何Go指针。

被C代码调用的Go函数,不得返回Go指针。但是这种函数可以:

  1. 将C指针作为入参,这些指针可以指向非指针、C指针,但是不能指向Go指针
  2. 将Go指针作为入参,但是这些指针所指向的内存,不得包含Go指针

Go代码不得在C内存中存储Go指针。C代码则可以在C内存中存储Go指针。但是在C函数返回时,必须立即停止存储Go指针。

上述规则可以在运行时动态检查。配置环境变量GODEBUG,设置其值为:

  1. cgocheck=1 执行廉价的动态检查
  2. cgocheck=0 禁用动态检查
  3. cgocheck=2 执行完整的检查,需要消耗一定的资源

要打破上述规则,可以使用unsafe包。另外,没有任何机制来阻止C代码做违反上述规则的事情。如果打破上述规则,程序很可能意外的崩溃。

常用命令
go

这是一个用于管理go源代码的工具。

注意,某些子命令的行为会受到 GO111MODULE环境变量的影响。

子命令 说明
build

编译包及其依赖,但是不安装编译结果

当编译单个main包时,结果二进制文件的basename默认和第一个源文件(go build a.go b.go输出a.exe)或者目录(go build unix/sam输出sam.exe)相同

当编译多个包或者单个非main包时,编译结果被丢弃,也就是仅仅验证编译是否可以通过

当编译包时, _test.go结尾的文件被忽略

格式:

1
go build [-o output] [-i] [build flags] [packages]

选项:

-o  仅编译单个包时可用,显式指定输出的二进制文件或对象文件的名字
-i 安装编译目标的依赖
-a 强制重新构建所有包,即使已经up-to-date
-v 编译后打印被编译包的名称
-buildmode mode 构建模式
-compiler name 使用的编译器,gccgo gc
-gccgoflags 'arg list' 编译器gccgo的标记
-gcflags 'arg list'  编译器gc的标记,参考go tool compile -help
-installsuffix suffix 包安装路径名后缀
-ldflags 'flag list'  链接标记,参考go tool link -help
-linkshared 与共享库进行链接,共享库使用 -buildmode=shared创建
-pkgdir dir  从dir安装、加载所有包,而非默认位置

构建模式:

-buildmode=archive  构建非main包为.a文件,main包被忽略
-buildmode=c-archive  构建main包以及所有它导入的包为C归档文件
-buildmode=c-shared 构建main包以及所有它导入的包为C共享库
-buildmode=shared 构建非main包为共享库,main包被忽略
-buildmode=exe 构建main包以及所有它导入的包为可执行文件
-buildmode=pie  构建为位置独立可执行文件(PIE)
-buildmode=plugin 构建为Go插件

示例:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
export GOROOT=/home/alex/Go/sdk/1.9.2
export GOPATH=/home/alex/Go/workspaces/default
pushd /home/alex/Go/workspaces/default/src/digital-app > /dev/null
export PATH=/home/alex/Go/sdk/1.9.2/bin/:$PATH
 
# 构建
go build -i -o build/digitalsrv
 
# 静态连接
# CGO_ENABLED 提示使用cgo而非go作为编译器,以便进行静态连接
# -a 重新构建所有依赖
# -ldflags '-s'减小二进制文件大小
CGO_ENABLED=0 go build -i -a -o build/digitalsrv -ldflags '-s'
tool compile

将单个Go包编译为对象文件

格式: go tool compile [flags] file...

选项:

-I dir1 -I dir2 导入包的搜索路径,在搜索$GOROOT/pkg/$GOOS_$GOARCH之后搜索
-N 禁用优化
-c int 编译时的并发度,默认1
-dynlink 允许引用共享库中的Go符号
-l 禁用内联
-lang version 设置Go语言版本,例如-lang=go1.12
-largemodel 基于大内存模型的假设来编译
-pack 生成归档而非对象文件
-shared 生成能够链接到共享库的代码
-dwarf 生成DWARF符号

tool link

从main包中读取Go归档/object,以及它们的依赖,并链接为二进制文件

格式:

Shell
1
2
3
4
go tool link [flags] main.a
 
# 从go build调用
go build -ldflags "[flags]"

选项:

-D address 设置数据段地址
-T address 设置文本段地址
-E entry 设置入口符号名称
-H type 设置可执行文件格式类型,默认类型从GOOS+GOARCH推断
-I interpreter 设置使用的ELF动态链接器
-L dir1 -L dir2 在搜索$GOROOT/pkg/$GOOS_$GOARCH之后,从dir1 dir2中搜索导入包
-X importpath.name=value设置导入路径中的字符串变量name的值为value。示例:

Shell
1
-X github.com/stefanprodan/flagger/pkg/version.REVISION=${GIT_COMMIT}"

上述选项只有当源码中定义了REVISION变量且它的值没有初始化、或者初始化为常量字符串表达式时有意义:

/home/alex/Go/workspaces/default/src/github.com/stefanprodan/flagger/pkg/version/version.go
Go
1
2
3
4
package version
 
var VERSION = "0.8.0"
var REVISION = "unknown"

-a 反汇编输出
-buildid id 设置Go工具链的build id
-buildmode mode 构建模式,默认exe
-c Dump出调用图
-compressdwarf 如果可能压缩DWARF,默认true
-cpuprofile file 将CPU profile写入到文件
-d 禁止生成动态可执行文件。控制是否生成动态头。默认情况下即使没有引用任何动态库也会生成动态头
-dumpdep Dump出符号依赖图
-extar ar 设置外部归档程序(默认ar),仅配合-buildmode=c-archive使用
-extld linker 设置外部链接器(默认clang或gcc)
-extldflags flags 空格分隔的,传递给外部链接器的选项
-installsuffix suffix 从$GOROOT/pkg/$GOOS_$GOARCH_suffix中搜索包,而非 $GOROOT/pkg/$GOOS_$GOARCH
-linkmode mode 设置链接模式,可选值internal, external, auto
-linkshared 和已经安装的Go共享库进行链接
-msan 支持C/C++内存消毒器
-n Dump出符号表
-o 将输出写入到指定文件
-r dir1:dir2... 设置ELF动态链接器的搜索路径
-race 链接到竞态检测共享库
-s 移除符号表和调试信息
-shared 生成共享对象,隐含-linkmode external
-w 禁止DWARF生成

链接模式:

internal:解析并链接主机上的对象文件(ELF/Mach-O/PE ...)到最终可执行文件里面。由于实现宿主机链接器的全部功能存在困难,因此这种模式下能够链接的对象文件种类是受限的

external:为了支持链接到任何对象文件而不需要动态库,cgo支持所谓外部链接模式。此模式下,所有Go代码被收集到go.o文件中,并通过宿主机链接器(通常gcc)将go.o以及所有依赖的非Go的代码链接到最终可执行文件里面

大部分构建同时编译代码、调用链接器来创建二进制文件。在使用cgo的情况下,编译时期已经依赖gcc,因此链接阶段再次依赖gcc没有问题

clean

移除编译后的对象文件(Object files)

格式:

go clean [-i] [-r] [-n] [-x] [build flags] [packages]

doc

显示包或者符号的文档

格式:

Shell
1
2
3
4
go doc [-u] [-c] [package|[package.]symbol[.methodOrField]]
 
# 显示fmt包的Errorf函数的文档
go doc fmt Errorf
env

打印Go相关环境变量信息,相关的环境变量包括:

GOOS,目标操作系统,支持darwin、freebsd、linux、windows、android等
GOARCH,目标体系结构,支持arm、arm64、386、amd64等

fix

针对指定的包运行Go fix命令。Fix能够查找到使用旧API的Go程序,并将其替换为新API。升级到新版本的Go之后,可以调用此命令

fmt 指定指定的包源代码运行gofmt
generate 通过处理源文件,产生Go文件
get

下载并安装包及其依赖

格式: go get [-d] [-f] [-fix] [-insecure] [-t] [-u] [build flags] [packages]

选项:

-d 仅仅下载,不安装
-f 配合-u,不去检查是否每个import语句对应的包以及从它的原始代码仓库检出
-fix  在解析依赖、构建代码之前,对下载的包运行fix tool
-t 同时下载构建测试代码所需的依赖
-insecure 允许通过非安全连接下载代码
-u 使用网络下载包及其依赖
-v 显示详细输出

install 编译并安装包及其依赖
list 列出包
run 编译并运行程序
test 测试指定的包
tool 运行指定的Go tool
依赖管理
vendor

go vendor 是go 1.5 官方引入依赖包管理机制。其基本思路是,将引用的外部包的源代码放在当前工程的vendor目录下面,go 1.6以后编译go代码会优先从vendor目录先寻找依赖包。这样,当前工程放到任何机器的$GOPATH/src下都可以通过编译。

如果不使用vendor机制,每次构建都需要go get依赖包,下载的依赖包版本可能和工程需要的版本不一致,导致编译问题。

仅可执行程序(main包)应该vendor依赖,共享的库则不应该。当你编写项目时,应该将所有传递性的依赖都扁平化到当前项目的vendor目录下。

依赖搜索的优先级:vendor目录 ⇨ $GOROOT ⇨ $GOPATH

glide

go vendor无法精确的引用外部包进行版本控制,不能指定引用某个特定版本的外部包。只是在开发时,将其拷贝过来,但是一旦外部包升级,vendor下的代码不会跟着升级,而且vendor下面并没有元文件记录引用包的版本信息,这个引用外部包升级产生很大的问题,无法评估升级带来的风险。

glide是Go语言的包管理工具,它能够管理vendor目录。

安装
Shell
1
curl https://glide.sh/get | sh
命令

格式: glide [global options] command [command options] [arguments...]

常用子命令:

子命令 说明
create 别名init,创建一个新工程,包含glide.yaml文件
cw config-wizard,自动扫描代码中的依赖,给出依赖版本的建议
get 下载一或多个包到vendor目录,并在glide.yaml中添加依赖项
rm 从glide.yaml中移除依赖,重新生成lock文件
import 从其它依赖管理系统中导入文件
nv

列出目录下所有non-vendor路径,如果不希望测试依赖的包,可以:

Shell
1
go test $(glide novendor)
install

安装项目的依赖,读取glide.lock文件,根据其中的commit id来安装特定版本的包

如果glide.lock文件不存在,则此命令自动调用glide update并生成glide.lock

update 更新项目的依赖
list 列出所有的依赖项
cc 清除Glide缓存
dep

Go语言的官方的试验阶段的依赖管理工具。需要Go 1.9+版本。从1.11版本开始,Go工具链内置了和dep差异很大的依赖管理机制。

安装
Shell
1
go get -u github.com/golang/dep/cmd/dep
创建项目
Shell
1
2
3
4
mkdir -p $GOPATH/src/github.com/gmemcc/dep-study
cd $GOPATH/src/github.com/gmemcc/dep-study
 
dep init # 自动生成Gopkg.toml Gopkg.lock vendor/
管理依赖

dep ensure可用于管理依赖,更新依赖后,vendor目录中的内容出现变化。dep ensure的功能包括:

  1. 添加新的依赖
  2. 更新现有依赖
  3. 对Gopkg.toml中的规则变化作出响应
  4. 项目中第一次导入某个包,或者移除某个包的最后一个导入后,作出响应

命令示例:

Shell
1
2
3
4
5
6
7
# 添加依赖
dep ensure -add github.com/foo/bar github.com/baz/quux
# 更新一个依赖项目到新版本
dep ensure -update github.com/foo/bar
# 更新所有依赖,通常不建议
# 搜索匹配Gopkg.toml中的branch、version、revision约束的代码版本
dep ensure -update
Gopkg.toml

此文件由dep init自动生成,后续主要由人工编辑。此文件可以包含几种规则声明,以控制dep的行为:

规则类型 说明
constraints

定义直接依赖如何加入到依赖图中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[[constraint]]
# 导入路径
  name = "github.com/user/project"
# 导入的版本,可以指定version branch 或 revision
# version操作符:
# = 等于 != 不等于 > 大于 < 小于 >= 大于等于 <= 小于等于
# - 版本范围,示例 1.2 - 1.4.5
# ~ 小版本范围,示例 ~1.2.3 表示大于等于1.2.3但小于1.3.0
# ^ 大版本范围,示例 ^1.2.3 表示大于等于1.2.3但小于2.0.0
# xX* 通配符,示例   1.2.x 表示大于等于1.2.0但小于1.3.0
  version = "1.0.0"
  branch = "master"
# 通常是反模式,例如Git的SHA1
  revision = "abc123"
 
# 可选。此依赖的源码的替换位置(URL或者导入路径),通常用在fork的场景下
  source = "https://github.com/myfork/package.git"
  [metadata]
# 定义一些键值对,dep本身不使用这些键值对
overrides

覆盖所有依赖(直接或传递)的规则,应当小心使用。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[override]]
  name = "k8s.io/api"
  version = "kubernetes-1.11.0"
 
[[override]]
  name = "k8s.io/apimachinery"
  version = "kubernetes-1.11.0"
 
[[override]]
  name = "k8s.io/code-generator"
  version = "kubernetes-1.11.0"
 
[[override]]
  name = "k8s.io/client-go"
  version = "kubernetes-1.11.0" 
required

必须在任何[[constraint]]或[[override]]之前声明

dep通过分析go代码中的import语句来构建一个依赖图。required/ignored规则用于操控此依赖图

required列出必须包含在Gopkg.lock中的包的列表,此列表会和当前项目导入的包合并。required可以强制声明一个未import的包为项目的直接依赖

 

对于linter、generator或者其它开发工具,如果:

  1. 被当前项目使用
  2. 不被直接或传递的导入到当前项目
  3. 你不需要把这些工具加入GOPATH,或者你想锁定版本

则可以使用required规则声明:

1
required = ["k8s.io/code-generator/cmd/client-gen"]
ignored

必须在任何[[constraint]]或[[override]]之前声明

dep通过分析go代码中的import语句来构建一个依赖图。required/ignored规则用于操控此依赖图

required列出dep进行静态代码分析时需要忽略的包,可以使用通配符:

1
ignored = ["github.com/user/project/badpkg*"]
metadata 定义供第三方工具使用的元数据,可以定义在根下,或者constraint、override下
prune

定义全局/某个项目的依赖修剪选项。这些选项决定了写入vendor时哪些文件被忽略

支持以下变量,取值均为布尔:

  1. unused-packages 提示不在包导入图中出现的目录,应该被修剪掉
  2. non-go 提示不被Go使用的文件应该修剪掉
  3. go-tests 将Go测试文件修剪掉

dep init会自动生成:

1
2
3
[prune]
  go-tests = true
  unused-packages = true

 你可以为每个项目(依赖)定义修剪规则:

1
2
3
4
5
6
7
[prune]
  non-go = true
 
  [[prune.project]]
    name = "github.com/project/name"
    go-tests = true
    non-go = false
vgo

由于Go一直没有提供官方的依赖管理工具,导致了dep,glide,govendor,godep等工具群雄争霸的局面。vgo是解决这种现状的早期尝试,vgo即Versioned go。

安装
Shell
1
go get -u golang.org/x/vgo
go.mod

这是vgo的依赖配置文件,需要放在项目的根目录下。

命令

vgo具有同名的CLI,可以使用的子命令包括:

子命令 说明
install 安装依赖
build 编译项目
run 运行项目
get github.com/pkg

获取依赖的最新版本。依赖包数据会缓存到GOPATH路径下的src/mod目录

get github.com/pkg@v1.0 获取依赖的指定版本
mod -vendor 将依赖直接放在vendor目录中
go modules

从vgo发展而来。

Go modules在1.11属于试验特性,环境变量 GO111MODULE用于控制其开关,取值auto/on/off,默认auto。

从1.16开始,默认开启go modules,即使项目中没有go.mod文件。计划在1.17中完全废弃对GOPATH模式的支持。

工作流

使用go modules进行依赖管理的日常工作流如下:

  1. 在Go源码中,使用import语句导入需要的包
  2. 调用go build/test等命令时,会自动将依赖加入到go.mod并下载依赖
  3. 如果需要限定依赖的版本,你可以选用以下方法之一:
    1. 使用go get,例如:
      Shell
      1
      2
      3
      go get foo@v1.2.3
      go get foo@master
      go get foo@e3702bed2 
    2. 手工修改go.mod文件

代理

很多Google的库在国内无法访问,可以设置环境变量:

Shell
1
2
3
4
5
# 默认源
export GOPROXY=https://goproxy.io
 
# 国内源
export GOPROXY=https://goproxy.cn

这样,所有模块都会通过此代理下载。 

访问私有仓库

你需要在仓库软件管理页面创建自己的Access Token,并配置Git:

Shell
1
2
3
4
5
#                                用户 Token
git config --global url."https://alex:***@git.pacloud.io".insteadOf "https://git.pacloud.io"
 
# SSH方式
git config --global url."git@git.tencent.com:".insteadOf "https://git.tencent.com/"

然后,需要设置跳过代理:

Shell
1
export GOPRIVATE=*.pacloud.io,*.gmem.cc
命令

Go modules在 go mod下定义了若干子命令:

子命令 说明
init

初始化一个新模块,示例:

Shell
1
2
cd gotools
go mod init github.com/gmemcc/gotools
download 将模块下载到本地缓存
edit 编辑go.mod
graph 打印模块依赖图
tidy 添加缺失的、移除多余的模块
vendor 将依赖的副本拷贝到vendor目录
verify 验证依赖是否满足期望
why 解释包或模块为何被main模块需要:
Shell
1
go mod why github.com/coreos/etcd

此外,你还可能用到以下Go命令:

Shell
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
# 显示构建时会实际使用的直接、间接依赖的版本
go list -m all
 
# 显示所有直接、间接依赖可用的minor/patch版本更新
go list -u -m all
 
# 列出依赖的可用版本
go list -m -json -versions go.etcd.io/etcd@latest
 
# 更新所有直接、间接依赖到最新的minor版本
go get -u
# 更新指定的依赖
go get -u -v go.etcd.io/etcd
# 更新所有直接、间接依赖到最新的patch版本
go get -u=patch
 
# 在模块根目录下执行,构建或测试模块的所有包
go build ./...
go test ./...
 
# 修剪不再需要的依赖
go mod tidy
 
# 创建并同步到vendor目录
go mod vendor
go.mod

仅仅包含四个指令:

指令 说明
module

声明当前模块的标识: module github.com/my/thing

此标识提供了模块路径(module path),模块中所有包的导入路径,都以此模块路径为前缀。模块路径 + 包相对于go.mod的路径共同决定了包的导入路径

require

声明依赖:

1
2
3
4
require (
    github.com/some/dependency v1.2.3
    github.com/another/dependency/v4 v4.0.0
)
replace

仅仅针对当前(主)模块,在构建主模块时,非主模块的go.mod中replace声明被忽略。该指令可以:

  1. 用来映射导入路径(在你Fork一个项目并做补丁的情况下使用):
    1
    2
    # 模块代码中使用的导入路径                      实际寻找代码时的导入路径
    replace example.com/original/import/path => /your/forked/import/path
  2. 精确的控制依赖的版本:
    1
    replace example.com/some/dependency => example.com/some/dependency v1.2.3
  3. 提示一个多模块协作项目的模块位于磁盘的绝对、相对路径:
    1
    2
    3
    4
    5
    6
    7
    module example.com/me/hello
     
    require (
      example.com/me/goodbye v0.0.0
    )
     
    replace git.yun.gmem.cc/eks/chart-service => ../chart-service

    这种用法可以将依赖位置定位到磁盘,解除对VCS的依赖 

exclude

仅仅针对当前(主)模块,在构建主模块时,非主模块的go.mod中exclude声明被忽略

该指令用于禁止某个直接或传递性的依赖包,例如:

Shell
1
exclude k8s.io/client-go v2.0.0-alpha.0.0.20190313235726-6ee68ca5fd83+incompatible
模块

一个Go项目通常会在Git上托管源码,例如github.com/gmem/gotools,项目的代码库的URL称为代码库(Repo)。

在同一Repo中,可以有多个包(Package),在Go 1.11中这些包被抽象为模块(Module)—— 模块是一系列相关包的集合,使用go.mod来记录模块的元数据。

Google的风格是使用单个代码库(Mono Repo),包括像Istio这样的项目,所有组件都在同一个代码库中。这种情况下,在单个代码库中创建多个模块是自然的需求,Go modules支持这种需求:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 单代码库单模块
monorepo-single-mod
├── go.mod
├── pkga
├── pkgb
└── pkgc
 
# 单代码库多模块
monorepo-multi-mods
├── proj1
│   ├── go.mod
│   ├── pkga
│   ├── pkgb
│   └── pkgc
├── proj2
│   ├── go.mod
│   ├── pkga
│   ├── pkgb
│   └── pkgc
└── proj3
    ├── go.mod
    ├── pkga
    ├── pkgb
    └── pkgc

逐级的1:N关系:Repo ⇨ Module ⇨ Package ⇨ Source。

依赖搜索模式

我们最熟悉的依赖搜索模式:vender、$GOPATH下搜索,叫做GOPATH mode。从1.8开始,可以不去显式的设置GOPATH,SDK默认使用~/go

Go modules引入一种新的Module-aware mode,在此模式下:

  1. 你的项目源码不需要放在GOPATH下
  2. 源码树的顶层目录下有一个go.mod文件,这种文件定义了一个Go模块
  3. 放置了go.mod的目录称为模块根目录,它通常是源码库的根目录,但非必须
  4. 模块根目录,及其子目录中的所有Go包,都属于模块。除了那些自身定义了go.mod的子目录
  5. Go编译器不再在vendor、GOPATH下寻找第三方Go包,而是会直接去下载并编译,然后更新go.mod:
    Go
    1
    2
    3
    4
    5
    6
    7
    8
    module proj1
     
    // 分析出的依赖,放到require区域
    require (
        # 使用最新版的代码,并以Pseudo-versions形式记录
        github.com/gmem/gotools/x v0.0.0-20190515063616-861b08fcd24b
        github.com/gmem/gotools/y v0.0.0-20190515005150-3e3f9af80a02 // indirect   传递性依赖
    ) 
  6. Go编译器会把下载的依赖包,缓存到 $GOPATH/pkg/mod目录下

依赖版本选择

go.mod一旦被创建,其内容将被Go工具链管理,执行get/build/mod都会导致go.mod被修改。

命令 go list -m输出的信息被称为build list,它是构建当前module所要构建的所有相关Package(及其版本)的列表:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
// go list -m -json all
{
    "Path": "proj1",
    "Main": true,
    "Dir": "/root/proj1"
}
{
    "Path": "github.com/gmem/gotools/x",
    "Version": "v0.0.0-20190515063616-861b08fcd24b",
    "Time": "2015-05-15T06:36:16Z",
    "Dir": "/home/alex/Go/workspaces/default/pkg/mod/github.com/gmem/gotools/x@v0.0.0-20190515063616-861b08fcd24b"
}

标记为Main: true的模块,称为主模块(main module), 即执行Go命令所在的目录。Go会在当前目录,以及当前目录的祖先目录寻找go.mod文件。

如果依赖打了发布版的Tag,则不会使用最新版的代码的Pseudo-version,而是使用最新的发布版。

如果需要显式依赖指定版本,可以使用命令来操控go.mod:

Shell
1
2
go mod -require=github.com/gmem/gotools/x@v1.0.0
go mod -require=github.com/gmem/gotools/y@v1.1.0

上述命令修改go.mod:

Shell
1
2
3
4
require (
    github.com/gmem/gotools/x v1.0.0 // indirect
    github.com/gmem/gotools/y v1.1.0 // indirect
)

你还可以指定依赖表达式:

Shell
1
go mod -require='github.com/gmem/gotools/y@>=v1.1.0'

使用go get命令,可以更新go.mod中的依赖为指定分支的最新Commit:

Shell
1
go get -u git.pacloud.io/pks/helm-operator@master 
版本号格式

发布版Tag必须是 vMAJOR.MINOR.PATCH格式。

incompatible表示你的依赖不支持Go modeules。

Pseudo-version的格式是 v0.0.0-yyyymmddhhmmss-abcdefabcdef。

版本号升级

对于一个库helm.sh/helm,Tag 1.x.x 发布后,消费者这样引用:

Shell
1
require helm.sh/helm v1.0.0 

没有问题。但是如果主版本改成 3.x.x,引用:

Shell
1
require helm.sh/helm v3.0.2 

就会提示:require helm.sh/helm: version "v3.0.2" invalid: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v3

解决办法是,对于3.x版本,(库的作者)修改模块路径,添加一个v3后缀:

Shell
1
2
# Git仓库地址 https://github.com/helm/helm 一直不变
module helm.sh/helm/v3

相应的,消费者这样引用: require helm.sh/helm/v3 v3.0.2 

如果你想Fork此库到Git私服git.pacloud.io/pks/helm,然后Replace时必须这样写:

Shell
1
2
3
4
#                                          注意这个v3不能少,否则报错
# replace git.pacloud.io/pks/helm: version "v3.0.2" invalid: module contains a go.mod file,
# so major version must be compatible: should be v0 or v1, not v3
helm.sh/helm/v3 => git.pacloud.io/pks/helm/v3 v3.0.2
同步到vendor

使用下面的命令,可以将某个模块的全部依赖保存一份副本到根目录的vendor目录:

Shell
1
go mod vendor

这样,你就可以使用vendor下的包来构建当前模块了:

Shell
1
go build -getmode=vendor tool.go

生成的vendor还可以用来兼容1.11以前的版本,但是由于1.11以前的版本不支持在GOPATH之外使用vendor机制,因此你需要把代码库移动到$GOPATH/src下。

vendor模式

Go modules支持使用vendoring模式:

Shell
1
2
3
4
5
# 使用环境变量
export GOFLAGS=-mod=vendor
 
# 或者
go build -mod=vendor ...

上述命令提示Go命令使用main模块的vendor目录来满足依赖,而忽略go.mod中的指令。 

最佳实践
代码组织

Go代码包含在工作区中:

  1. 你通常把所有的Go代码存放在单一的工作区中
  2. 一个工作区中可能包含多个版本控制仓库
  3. 每个仓库中可能包含一个或者多个包
  4. 每个包会包含一个或者多个位于单个目录中的Go源码文件
  5. 到达包的目录路径,对应了它的导入路径

一个工作区包含以下目录:

  1. src目录包含源代码。其中常常包含多个版本库。源代码文件必须总是以UTF-8方式存储
  2. pkg目录包含包对象
  3. bin目录可执行文件
GOPATH

该环境变量指定你的工作区的位置,默认指向$HOME/go

导入路径

导入路径唯一的识别包,一个包的导入路径对应了它在工作区中、或者远程仓库中的位置。

标准库占用了很多简短的导入路径,例如fmt、net/http。设计自己的包时,你要注意避免导入路径的冲突。例如,可以使用你的GitHub账号作为包路径前缀。

命名
驼峰式记法

变量命名使用驼峰式大小写。

Getter/Setter

不提供Getter/Setter支持,如果你有一个字段owner,建议 Owner()方法作为Getter, SetOwner()作为Setter。

接口名

一般只有一个方法的接口,以 er作为后缀,例如 Reader、Writer、 Formatter、CloseNotifier。

包名

包名应该是导入路径的最后一段:位于src/encoding/base64的包,其导入路径为encoding/base64,而其包名是base64。

包命名方面没有硬性规定。但是作为惯例,包名应该尽可能简短、仅仅使用小写字母。

包名仅仅用于导入,不需要是全局唯一的,如果出现名字冲突,你可以在导入时赋予别名:

Go
1
2
3
4
5
6
//     别名  包名
import FMT "fmt"
 
func main() {
    FMT.Print(0)
}
工程布局

Go官方没有提供开发工程的结构的标准布局,下面是Go生态系统中常用的一种布局:

/cmd 存放工程会产生的所有二进制文件

/binname/main.go 二进制文件binname的入口点函数,量尽量少的胶水代码

/internal 存放工程私有的、不需要被其它应用程序或者库引用的Go代码

/app  私有应用程序代码

/pkg 可以被私有应用程序共享的库代码

/pkg  可以被外部应用导入、使用的库代码

/vendor  依赖的外部库代码

/api  OpenAPI/Swagger Specs、JSON Schema文件、Protocol定义文件

/web  Web应用的特殊组件,例如静态资产、服务器端模板

/configs  配置文件模板、默认配置

/init 系统初始化(Systemd、System V等)配置、进程管理器(Supervisor)配置

/scripts  执行构建、安装、分析等任务的脚本

/build  打包和CI用目录

/package  云/容器/操作系统级别的打包配置信息

/ci  持续集成配置信息

/deployments  IaaS/PaaS/容器编排的配置文件,例如K8S资源定义,或者Chart

/test 额外的外部测试应用,以及测试数据

/docs 设计文档、用户手册

/tools  工程的支持性工具

/examples  示例代码

/assets  资产文件,例如图片、图标

/githooks  Git的钩子

测试

Go提供了一个轻量级的测试框架,此框架由 test包和 go test命令组成。

要编写测试用例,你需要创建一个以 _test.go结尾的源文件。该文件中包含名为 func TestXXX(t *testing.T)的函数。

注意:运行一个测试文件时,里面的所有函数在一个进程内调用完成。

远程包

包的导入路径可以用来描述如何从某个版本控制系统(Git或者Mercurial)获取包的源代码。go命令会自动基于此路径从远程仓库自动抓取包(及其依赖的其它包):

Shell
1
go get github.com/golang/example/hello

如果上述包不存在于本地工作区,则go命令会自动下载到工作区。如果上述包已经存在,则go get的行为和go install相同。

Getter/Setter

Go没有提供统一的Getter/Setter支持,参考如下方式:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type User struct {
    // 字段小写,表示不导出到包外部
    name string
}
 
// Getter,不需要Get前缀
func (user *User) Name() string {
    return user.name
}
 
// Setter,通常使用Set前缀
func (user *User) SetName(name string) {
    user.name = name
}
接口名称

作为惯例,单方法的接口,命名使用-er后缀,例如Reader、Writer、Formatter。

驼峰式大小写

如果一个标识符包含多个单词,Go中惯例的方式是使用CamelCase或者camelCase风格的驼峰式大小写,而不是使用下划线。 

暴露接口而非实现

如果某个类型仅仅是为了实现一个接口,则应该导出接口,而非类型本身。然后使用构造函数创建类型的实例:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 接口
type Block interface {
    BlockSize() int
    Encrypt(src, dst []byte)
    Decrypt(src, dst []byte)
}
 
type Stream interface {
    XORKeyStream(dst, src []byte)
}
 
// 构造函数
func NewCTR(block Block, iv []byte) Streamgcflags
 
// 实现 ...
内存

Go语言使用垃圾回收机制,这可能导致性能问题。要编写高性能的应用程序,应当尽量避免触发GC。

内存重用和对象池

利用sync.Pool,预先分配好一块内存,然后反复的使用它。这样不但GC压力小,而且CPU缓存命中更高、TLB效率更高,代码的速度可能提升10倍。 

避免使用指针

引用“外部”对象具有与生俱来的开销:

  1. 指向对象需要8字节
  2. 每个独立对象具有隐藏的内存开销,可能在8-16字节之间
  3. 使用引用(指针)效率更低,因为GC写屏障的存在

因此,如果不需要共享OtherStuff,使用下面的风格:

Go
1
2
3
type MyContainer struct {
  inlineStruct OtherStuff
}

而非: 

Go
1
2
3
type MyContainer struct {
  outoflineStruct *OtherStruct
}
不要提供需要内存分配的API

倾向于:

Go
1
2
// 由调用者提供缓冲
func (r *Reader) Read(buf []byte) (int, error)

而不是:

Go
1
func (r *Reader) Read() ([]byte, error)

后者可能导致大量内存分配。 

关于Goroutine 
  1. Goroutine基本上只有栈的开销,一开始栈只有2KB
  2. 尽管Goroutine成本较低,还是要避免在主要的请求处理路径上创建Goroutine。提前创建Goroutine并让其等待输入
ToString

为任何命名类型实现如下签名的方法,即可获得类似Java的toString()的功能:

Go
1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
 
type bin int
func (b bin) String() string {
        return fmt.Sprintf("%b", b)
}
 
func main() {
        fmt.Println(bin(42))  // 101010
}
远程调试
构建选项

为了保留调试信息,你需要在构建时设置特定的标记:

Shell
1
2
go build -i -a -o build/eb-rdbg -gcflags="all=-N -l" -ldflags='-linkmode internal'
                                -gcflags="-N -l"  # 1.10之前的版本,用于保留调试信息
运行应用程序

你需要通过dlv来启动被调试应用程序,dlv具有反复重启被调试程序的能力:

Shell
1
2
3
4
# 需要预先安装dlv
go get -u github.com/derekparker/delve/cmd/dlv
# 启动应用程序
dlv --listen=:2345 --headless=true --api-version=2 exec build/eb-rdbg
连接到应用程序 

你需要连接到被调试应用程序:

Shell
1
dlv connect localhost:2345

如果使用IDE,则更加简单。例如对于Goland,你只需要创建一个Go Remote类型的Run Configuration即可。

调试命令

连接到被调试应用程序后,你会看到 (dlv)命令提示符,以下命令可用:

  命令 说明
h help

显示命令列表,或者显示某个命令的帮助:

Shell
1
2
3
4
# 显示命令列表
help
# 显示config命令的帮助信息
help config 
  config

进行dlv配置:

Shell
1
2
3
4
5
# 列出配置
config -list
 
# 保存位置到磁盘,覆盖当前配置文件
config -save

示例:

Shell
1
2
# 进行源码路径映射
config substitute-path /go/src /home/alex/Go/workspaces/default/src 
b break

设置断点,格式: break [name] <linespec>

其中linespec格式可以是:

  1. *<address>,内存地址
  2. <filename>:<line>,文件路径和行号,文件路径可以是部分路径,甚至仅仅是basename,只要不产生歧义即可
  3. +<offset>,当前行的后面N行
  4. -<offset>,当前行的前面N行
  5. <function>[:<line>],指定函数的第N行,函数的完整形式 <package>.(*<receiver type>).<function name>,但是仅仅function name是必须的,其它的可以省略(只要不产生歧义)
  6. /<regex>/,匹配正则式的位置

示例:

Shell
1
b pkg/cmd/cli/restic/server.go:156 
cond condition 设置条件式断点: condition <breakpoint name or id> <boolean expression>
bp breakpoints 列出断点
  clear 删除一个断点: clear <breakpoint name or id>
  clearall 删除全部断点
  on 到达断点时执行命令: on <breakpoint name or id> <command>,可用命令print, stack, goroutine
c continue 执行到下一个断点,或者程序结束
  locals 打印本地变量: [goroutine <n>] [frame <m>] locals [-v] [<regex>]
  vars 打印包变量: vars [-v] [<regex>]
  print 估算一个表达式的值: [goroutine <n>] [frame <m>] print <expression>
  whatis 打印表达式的类型: whatis <expression>
  set 设置变量的值: [goroutine <n>] [frame <m>] set <variable> = <value>
n next Step Over: next [count],count指定前进多少行
s step 单步跟踪,遇到函数会自动Step Into
so stepout Step Out
bt stack 打印当前调用栈
  up 向上移动帧 ,可以同时指定在其上执行命令: up [<m>] <command>
  down 向下移动帧 ,可以同时指定在其上执行命令: down [<m>] <command>
  frame 设置当前帧,或者在一个指定的帧上执行命令 : frame <m> <command>
l list

列出当前调用栈对应的源码

也可以指定列出任何协程的源码: [goroutine <n>] [frame <m>] list [<linespec>]

  funcs 列出函数,一般都要带正则式,否则太多了:
gr goroutine  显示协程,或者切换当前协程: goroutine <id> <command>
grs goroutines 列出所有协程
  libraries 列出加载的动态库 
  sources 列出所有源文件清单 
  restart  重启被调试进程,dlv进程保持不变
q exit  退出调试客户端 
  deferred 在延迟调用上下文中执行命令 
  args 打印程序参数 
  disassemble 执行反汇编 
  regs 打印寄存器内容
常见问题
零散问题
编译报错cannot load...but does not contain package

使用命令 go mod tidy可以发现根源。

调用包内函数提示undefiend

原因是构建时没有引用目标函数所在文件,可以这样进行构建:

Shell
1
go build *.go
容器内报错 no such file or directory

找不到可执行文件:could not launch process: fork/exec /eb-rdbg: no such file or directory

实际上此文件是存在的,只是二进制格式不支持。CGO_ENABLED=0后发现解决。

go get 私有仓库报错disabled by GOPRIVATE/GONOPROXY

命令: go get -u git.pacloud.io/pks/addons-api-server@develop

报错原因:可能因为执行命令时Gitlab正在处理develop分支的Push

checksum mismatch

基于Go Modules进行构建,或者执行命令 go mod tiny可能产生此错误。可能需要清除mod缓存重新下载:

Shell
1
2
3
go clean -modcache
cd project && rm go.sum
go mod tidy
← 玉露
使用Chrome开发者工具分析内存泄漏 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • gRPC学习笔记
  • Kuberentes客户端编程
  • Go语言系统编程
  • Protocol Buffers初探
  • Go语言中的模板引擎

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2