Go语言学习笔记
Go是一门开源的、表现力强的、简洁的、静态编译型语言。它在语言级别支持协程(Coroutine),让你轻松的编写并发性代码。Go新颖的类型系统便于构建灵活的、模块化的应用程序。Go能够快速的编译为机器码,同时支持垃圾回收(并发标记清除)、运行时反射等现代语言特性。
多年来系统级编程语言没有出现新成员,然而计算领域发生重大的变化:
- 计算机的速度极大的增快了,而软件开发速度的变化不大
- 当今的软件开发中,依赖管理是一项重要的工作。C风格语言的基于头文件的依赖难以进行清晰的依赖分析
- 传统静态类型语言,例如Java、C++笨重的类型系统让人反感,很多团队转而使用动态类型语言,例如Python、JavaScript
- 流行的系统级语言不支持垃圾回收、并行计算等基础概念
Go致力于解决解决上面几点中提及的问题,它能保证大型程序的快速编译、简化依赖管理、完全支持垃圾回收和并行计算。
Go提供了一个运行时,此运行时作为每个Go应用程序的一部分,提供语言的基础服务,例如垃圾回收、并发、栈管理。这个运行时更像是libc而非JVM,Go运行时不提供虚拟机。
任何Go应用程序都是由包构成:
- 程序的执行入口必须是main包,在构建时go自动为main包创建可执行程序
- 程序可以导入一个或者多个包
- 包的名字通常和导入路径的最后一个目录名一致,也就是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在根据导入路径查找包时,遵循以下优先级:
- 首先在$GOROOT下寻找
- 然后在$GOPATH下寻找
- 仍然找不到,尝试进行远程包下载、导入
在导入包的时候,可以赋予其别名,例如: import util "gmem.cc/util"。如果想导入包但又不使用其export出的成员,可以将其别名为 _,这种用法仅仅为了执行包中的init函数。
每个源文件都可以定义init函数(因此一个包可以有多个init函数),进行必要的状态初始化。init函数的执行时机是:
- init所在源文件所有导入的包被初始化后
- 包中声明的所有变量的初始化式被估算后
- 如果目标包中有多个init函数,则根据init所在源文件的名称的字典序,依次执行
例如下面这个源文件:
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程序中,包初始化顺序如下:
1 2 3 4 5 6 7 8 9 |
go run *.go # ├── 主包被执行 # ├── 初始化所有导入包 # | ├── 初始化自己之前,初始化所有导入的包 # | ├── 然后初始化自己的变量 # | └── 然后调用自己的init # └── 主包初始化 # ├── 初始化全局变量 # └── 按源文件名称依次执行init函数 |
Go的函数可以接收0-N个参数:
1 2 3 |
func add(x int, y int) int { return x + y } |
可以注意到,变量的类型、函数的返回值类型,都是后置声明的。 这种与众不同的语法风格,在进行复杂声明时能够保持可读性。
如果连续多个形参的类型相同,则可以仅仅为最后一个声明类型: func add(x, y int) int {}
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的返回值还可以具有名称:
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")) } |
函数可以像任何值一样,被传递来传递去:
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)引用其外部作用域的变量。这导致即使外部作用域生命周期结束,被引用的变量仍然存活。
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函数支持不定数量的参数,例如:
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, "一") } |
切片可以展开为可变参数列表:
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可以定义一个变量,或者变量的列表。变量可以定义在包级别、函数级别:
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) } |
你可以在变量列表声明之后紧跟着初始化列表:
1 2 3 4 |
// 数量必须一致 var i, j int = 1, 2 // 变量类型可以省略,从初始化表达式中推导 var c, python, java = true, false, "no!" |
使用 :=操作符,可以在函数体内部声明变量并初始化,而不需要var关键字和变量类型说明(自动推导):
1 |
c, python, java := true, false, "no!" |
这种语法不能用在函数体外,函数外的每个语句都必须以关键字开头(例如var、func)。
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,看不到子作用域的变量 |
单个变量不能重复声明:
1 2 |
i := 0 i := 1 // 错误 |
但是多变量声明时,只要其中一个是新变量,就可以重复声明:
1 2 |
// 重复声明i i, j := 1, 0 |
如果变量在定义时没有明确的初始化,则自动初始化为零值:数值类型初始化为0;字符串类型初始化为"";布尔型初始化为false;interface/map/func/slice/chan初始化为nil。
字符串不能赋值为nil,它的零值只能是空串:
1 |
var x string = nil // 出错 |
你可以向零值的slice中添加元素:
1 2 |
var s []int s = append(s,1) |
但是不能向零值的map添加键值对。
使用语法 T(v)可以将v转换为T类型:
1 2 3 |
i := 42 f := float64(i) u := uint(f) |
需要注意的是,Go语言在不同的类型之间进行赋值时,需要显式的类型转换。
当定义变量而不显式指定其类型时,变量的类型由赋值符号右侧的子表达式推导:
1 2 3 4 |
var i = 0 // 推导为int j := i // 推导为int f := 3.142 // float64 g := 0.867 + 0.5i // complex128 |
常量类似于变量,但是使用 const关键字声明,常量不支持 :=语法:
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 |
||
rune |
字面意思是“符文”,实际上是int32的别名,代表一个Unicode代码点(Code Point) 代码点和字符唯一性的对应,例如U+2318(十六进制2318)对应字符⌘ rune的直接量语法和Java中的char类型一致:
|
||
不定长整数 | int、uint、uintptr在32bit的系统上通常为32位;在64bit的系统上通常为64位 | ||
浮点数 | float32、float64 | ||
复数 |
complex64、complex128 示例:
|
Go字符串的本质是一个byte切片(以及一些额外的属性)。字符串是不可变的。如果需要修改字符串的某个字符,可以使用byte切片:
1 2 3 4 |
x := "text" xbytes := []byte(x) xbytes[0] = 'T' string(xbytes) |
但是需要注意,单个字符可能存放在多个byte中。因此更新一段字符串,使用rune的slice可能更好,尽管单个字符也可能占用多个rune(例如包含重音符号的字符)。
当字符串转换为byte切片,或者byte切片转换为字符串时,你都会得到原始数据的拷贝。这和通常的Cast语义不同。
len()调用获取的是字符串包含的byte数量。要获取字符数量,可以:
1 2 |
import "unicode/utf8" utf8.RuneCountInString(data) |
实际上RuneCountInString获取的是rune而非char数量,包含重音符号的字符,可能占据两个rune。
对字符串进行[]操作,得到的是byte而非rune。要获得rune,可以使用for range操作,或者利用unicode/utf8、golang.org/x/exp/utf8string等包:
1 2 3 |
import "golang.org/x/exp/utf8string" str := utf8string.NewString("你好") str.At(0) |
1 2 3 |
// 可以使用转义序列引入任意数据 data := "A\xfeC" utf8.ValidString(data) // false |
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语言支持结构体,也就是字段的集合:
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} } |
要访问结构中的字段,使用点号:
1 2 |
v := Vector3{1, 2, 3} fmt.Print(v.X) |
可以通过结构的指针来访问字段,语法和上面一样:
1 2 3 |
v := Vector3{1, 2, 3} pv := &v fmt.Print(pv.X) |
声明结构的不同方式:
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"这种形式的键值对,例如:
1 2 3 |
type User struct { Name string `json:"userName" xml:"user-name"` } |
如果值部分包含更多信息,可以使用逗号分隔:
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{}的特点是占用内存空间为零:
1 2 |
var s struct{} fmt.Println(unsafe.Sizeof(s)) // 0 |
因此可以用它作为占位符:
1 2 |
chan struct{} // 仅仅用来传递信号,而非读写数据 map[string]struct{} // 实现Set结构 |
类型 [n]T表示具有n个元素的T类型的数组。 数组的长度是其类型的组成部分,这和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 |
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 // 解引用并赋值,已经分配内存的索引才能被赋值 |
关于数组,需要注意它的长度是一定的,这和切片不同。元素类型相同、长度也相同的数组,它们的类型才是一样的。
你可以对数组元素进行取地址操作:
1 2 |
// 避免复制 container := &dep.Spec.Template.Spec.Containers[0] |
数组在传参、赋值时是传值,不过参数通常都会用切片。
在C++中,数组即指针。函数的入参是数组时,函数内外引用的是相同内存区域。
但是在Go中,数组是值,向函数传递数组时,函数得到的是原数组的拷贝。也就是说你无法修改原始数组。如果需要修改原始数据,则需要传递其指针:
1 2 3 4 5 6 |
x := [3]int{1,2,3} func(arr *[3]int) { // 解引用 (*arr)[0] = 0 }() |
或者,你可以使用切片。尽管切片本身是传值的,但是其底层数组在函数内外共享:
1 2 3 4 |
x := []int{1,2,3} func(arr []int) { arr[0] = 7 }(x) |
但需注意,这种方法不能用于增加切片元素,因为其底层数组可能被换掉。
[]T表示类型为T的切片。切片类似于数组,实际上它的底层存储就是数组。切片可以用来实现动态长度的数组。
切片是一个很简单的数据结构,它包括三个成员:指向底层数组的指针;切片长度;切片容量。
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} |
切片元素可以是任何类型,包括切片:
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(不包含)的新切片。需要注意,进行切片操作后,新产生的切片会共享底层数组:
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函数:
1 2 3 |
// 构造初始长度为10,容量为100的切片 s := make([]int, 10, 100) fmt.Print(len(s)) // 10 |
切片的零值是nil,nil切片的长度、容量皆为0。另外空切片的长度容量也都是0:
1 2 3 4 |
// nil切片,底层数组的指针为nil var nilSlice []int // 空切片,底层数组的指针为一个地址 slice:=[]int{} |
要向切片的尾部添加元素,可以调用append函数。这个函数有可能导致创建新的底层数组。
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):
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操作获取的都是元素的拷贝。要想修改集合元素,需要使用索引:
1 2 3 |
for i,_ := range data { data[i] *= 10 } |
如果元素存放的是指针则可以直接解引用并修改。
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。两个键是否相等,规则如下:
- 指针:当指针指向同一变量时它们相等,nil指针是相等的
- 通道:两个通道是由同一次make调用产生的,则它们相等,nil通道是相等的
- 接口:具有相同的运行时类型,且运行时对象是相等的,则接口相等
- 结构:如果两个结构的,相同名称的非空字段都相等,则结构相等
- 数组:如果每个元素都相等
注意,Go中map的操作不是原子的,原因是map的典型应用场景下不牵涉到跨Goroutine的安全访问。
要保证操作的原子性,建议使用Goroutine+Channel,或者使用sync包。
注意:不支持对映射的值进行取地址操作:
1 2 3 4 |
users := map[string]User{ "Alex": User{"Alex"}, } alex := &users["Alex"] // 编译错误 |
注意:在迭代映射的过程中删除键值是安全的:
1 2 3 4 5 |
for key := range m { if key.expired() { delete(m, key) } } |
尽管没有类(Class)的概念,Go却支持为类型定义方法。Go不支持方法重载,这意味着两个方法不得具有相同的名字。
方法是一种函数,它具有特殊的接收者(Receiver)参数:
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)类型定义方法,但是你只能为当前包中声明的类型定义方法:
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语言。
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中进行方法动态绑定(多态)的唯一途径。针对结构或者其它具体类型的方法调用,其绑定都发生在编译时。
接口这种类型定义一组方法签名:
1 2 3 4 5 |
type GeoOps interface { // 接口中的方法不需要 func关键字 IsNorthHemisphere() bool isEastHemisphere() bool } |
可以将实现了接口中定义了的方法的类型赋值给接口:
1 2 3 4 5 |
// 以接口类型声明值(Interface Value) var gops GeoOps // 是否需要取地址,取决于实现方法的是T还是*T gops = &jazxPos fmt.Println(gops.IsNorthHemisphere()) |
Go语言不需要显式的implements声明,类型只需要实现接口中规定的方法即可(鸭子类型识别)。这种设计让接口和它的实现完全解耦。
需要注意:
- 实际类型以值接收者实现接口的时候,不管是实际类型的值,还是实际类型值的指针,都实现了该接口
- 实际类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口
如果接口值指向nil变量,调用方法时,接收者是nil。在很多语言中,调用nil的方法会导致空指针异常,但是在Go语言中,你可以优雅的实现如何处理nil值的逻辑。
注意nil接口值和指向nil变量的接口值:
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() |
还有一个陷阱需要注意:
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 |
需要注意:
- 如果指针实现了接口,则这种指针的零值,可以赋值给接口 —— 这个赋值过程赋予了接口type和value
- 尽管可以将零值赋给接口,但是接口变量 != nil。
第2点很容易造成问题,当将指针类型的变量传递给形参类型为接口的方法时,判断nil值很容易出现问题。这种反直觉行为的原因是,接口本质上是 (type, value)对,当你将接口和 nil指针比较时,自然不相等。
解决办法:
- 如果知道接口的type,可以进行类型断言,再判断nil:
12if i.(bool) == nil {} - 否则,可以通过反射:
12if reflect.ValueOf(i).IsNil() {}
定义了零个方法的接口被称为空接口: interface{}
任何类型的变量都可以赋值给空接口,因为任何类型都实现它的全部方法。空接口用于处理未知类型的值。例如标准库中的fmt.Println方法:
1 2 3 |
func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) } |
如果一个包仅暴露一个接口,可以将此接口直接命名为Interface:
1 2 3 4 5 |
package user type Interface interface { GetName() string } |
不要将接口的指针作为入参,只需要使用接口本身:
1 2 3 |
func sayHelloTo(user *user.Interface) { // NOT OK println((*user).GetName()) } |
要避免拷贝,只需要让类型的指针作为接口方法的接收者即可。
子接口类型的实参,可以传递给父接口类型的形参。所谓父子接口,就是体现在方法集的包含关系上。
仅仅支持 i++,不支持++i。而且你不能在表达式中使用自增自减操作符:
1 2 |
++i // 错误 data[i++] // 错误 |
^作为一元操作符,按位取反,作为二元操作符,异或。
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)] |
使用下面的语法可以进行类型断言,将接口类型强制转换为真实类型,或者将接口转换为另外一个接口:
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) |
如果指定两个变量来接收返回值,则:
- 如果断言成功,t为真实类型变量,ok为true
- 如果断言失败,t为真实类型的nil值,ok为false
如果指定单个变量来接收返回值,则断言失败会导致panic
这种语法支持在switch语句中进行多次断言,直到类型匹配:
1 2 3 4 5 6 |
switch val := gops.(type) { case *LatLng: // 执行到这里则val 是 *Latlng default: } |
Go支持两种变量分配原语:new、make,二者都是内置函数。
new仅仅用于分配内存,将其置零,但是不初始化变量,仅仅返回指向零值的指针:
1 2 |
// 为类型SyncedBuffer分配对应大小的内存并清零,然后返回一个 * SyncedBuffer p := new(SyncedBuffer) |
不同类型的零值具有不同含义:例如:
- sync.Mutex的零值表示解锁状态
- bytes.Buffer的零值表示空白的缓冲区。
用于创建切片(slice)、映射(map)和通道(chan),并且返回已初始化的目标类型(而非指针)。这种设计的原因是,这三个类型必须在它们引用的数据结构被初始化后才能使用
你可以声明任何类型的空白标识符,也可以将任何表达式赋值给空白标识符。空白标识符使用符号 _表示,可用来无害的丢弃某些值。
空白标识符可以用在多赋值的表达式中,忽略不关心的那些值:
1 2 3 |
if _, err := os.Stat(path); os.IsNotExist(err) { // ... } |
导入包,但是仅仅执行其init函数而不使用其导出的成员:
1 |
import _ "net/http/pprof" |
检查是否实现了指定的接口:
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 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 } |
注意,只有接口才能被嵌入到接口中。
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() } |
结构可以同时包含普通字段、内嵌结构:
1 2 3 4 5 6 |
type Job struct { Command string *log.Logger } job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)} |
如果需要直接引用内嵌类型,可以使用其类型名:
1 2 |
// 使用类型名引用,不要导入包前缀 job.Logger.Logf("%q: %s",...) |
注意,结构也可以内嵌接口:
1 2 3 4 |
type Registry struct { ClusterID string model.Controller // 类型名Controller } |
结构嵌入接口的意义是,相当于加了个“缺省适配”,结构不必须实现任何方法,仅仅“覆盖”自己关注的方法即可。
内嵌的接口、结构,可以命名,也可以不命名(匿名内嵌)。不命名时,直接以其类型名引用:
1 2 3 4 |
aggregate.Registry{ ClusterID: clusterID, Controller: kubectl, // 直接通过类型名Controller引用 } |
嵌入可能导致接口/结构的成员名冲突(同名),Go基于以下规则解决此冲突:
- 越靠嵌套层次外部的成员优先级越高。例如上面的Job内嵌了Logger,如果Logger有一个字段Command,则此字段被Job.Command隐藏
- 同一层次下出现重名,通常是一个错误。例如上面的Job内嵌了Logger,它不应该同时有一个名为Logger的方法或者字段。即便如此,冲突的名称只要没有在类型定义外部被引用,就不会导致错误
根据实际情况:
- 如果外层对象以值的形式传来传去,而你需要的内层对象的方法是定义在指针上的,则嵌入指针
- 如果外层对象以指针的形式传来传去,则内层对象可以嵌入值,没有问题,你仍然可以访问内层对象的指针方法
- 如果内层对象的方法均是值接收者,则嵌入值
如果对象很小,可以考虑嵌入值,这样可以减少内存分配次数,并实现局部访问(内存中比较靠近)。
下面的语法是定义别名,两个类型是完全一样的,可以任意替换:
1 |
type nodeName = string |
下面的语法是定义一个新类型,两个类型不一样,必须强制转换:
1 2 3 4 |
type nodeName string var n nodeName = string("xenon") str := string(n) |
对现有非接口类型进行类型定义,新的类型不会继承原有类型的方法:
1 2 3 4 |
type myMutex sync.Mutex var mtx myMutex mtx.Lock() // 错误 |
要想继承,可以使用匿名嵌套:
1 2 3 4 5 6 |
type myMutex struct { sync.Mutex } var mtx myMutex mtx.Lock() // OK |
但是,对于接口类型进行类型定义,方法则会被继承。
Go仅仅支持for这一种循环结构:
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语句引入的变量在整个迭代过程中是重用的,也就是说,也就是说,每次迭代中进行取地址,总是会指向同一个地址:
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完全一致。
解决此问题的方法有两个:
- 如果希望得到值副本的指针:
12copied := solverConfig&copied - 如果希望得到原始值的指针:
1&solverConfig[idx]
下面的代码有问题,所有Goroutine打印的都是data最后一个元素的值:
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() // 同样的问题,但是更加隐蔽 } |
解决办法是在循环体内定义局部变量:
1 2 3 4 5 6 |
for _,v := range data { v := v // 每个闭包引用的不是同一变量,这种同名覆盖变量虽然奇怪,在Go里面是惯用法 go func() { fmt.Println(vc) }() } |
或者直接传参给Goroutine:
1 2 3 4 5 |
for _,v := range data { go func(v V) { fmt.Println(vc) }(v) } |
再次强调:整个循环过程中,for语句引入的迭代变量是的内存地址是不变的,也就是说,整个迭代过程中只有一个变量,每次都在修改同一变量的值,在循环体内,传递此变量的地址是危险的,特别注意闭包这种隐晦的传递地址的形式
类似于循环控制语句,分支控制语句也可以包含一个初始化语句:
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 { } |
需要注意,Go中switch语句的case,默认行为是break,而其它很多语言的默认行为是进入下一个case继续判断、执行(注意C语言中,没有break的情况下,一旦某个case匹配,后续所有其它case/default都会执行):
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: ... } |
这个关键字可以让后面的语句延迟执行:
1 2 3 4 5 |
func main() { defer fmt.Println(" World") fmt.Println("Hello") } // Hello World |
直到包围语句的函数(而非代码块)返回之前才执行,因此在循环体中通过defer来进行清理是不可以的。解决办法是把循环体封装为函数。
连续使用defer时,会形成defer栈,先入栈的语句后执行:
1 2 3 4 5 6 |
func main() { for i := 0; i < 10; i += 1 { defer fmt.Print(i) // 不能用这种方式实现每元素迭代后清理 } // 9876543210 } |
需要注意,defer的表达式求值发生在声明时,而非实际执行时:
1 2 3 |
var i int = 1 defer fmt.Println("result =>",func() int { return i * 2 }()) // 打印2而非4 i++ |
- 包裹defer的函数返回时
- 包裹defer的函数执行到末尾时
- 所在的goroutine发生panic时
如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作:
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语句的时候:
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行。
换一种写法:
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()的函数调用不会提前发生。
当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行:
1 2 3 4 5 6 |
func deferExit() { defer func() { fmt.Println("defer") }() os.Exit(0) } |
上面的defer不会执行。
可以用于goto跳转、for switch、for select跳转:
1 2 3 4 5 6 7 |
loop: for { switch { case true: break loop } } |
可以实现无条件转移:
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编码格式:
|
||
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 |
为基本数据类型提供到/从字符串展现的转换
|
||
strings | 提供操控基于UTF-8的字符串的函数 | ||
sync | 提供基本的同步原语,例如互斥量 | ||
sync/atomic | 提供低级别的原子内存原语,用于设计同步算法 | ||
syscall | 支持低级别的系统调用 | ||
testing | 用于支持Go包的自动化测试 | ||
time | 操控和访问时间 | ||
unicode | 提供对Unicode的支持 | ||
unsafe | 用于绕开Go的类型安全机制 |
这两个在Go语言中是常量,其声明如下:
1 2 3 4 |
const ( true = 0 == 0 false = 0 != 0 ) |
用在多常量声明中,其值为当前常量值的索引:
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 } |
预定义的标识符,表示指针、通道、函数、接口、映射或者切片的零值。
函数签名: func append(slice []Type, elems ...Type) []Type
将元素附加到切片的尾部,如果切片容量足够,它被重切已满足新元素。如果容量不足,底层数组被重新分配
支持把字符串添加到[]byte切片中:
1 |
slice = append([]byte("hello "), "world") |
函数签名: func cap(v Type) int
获取数组、数组指针(指向的数组)、切片、缓冲通道的容量。
用于关闭通道
函数签名: func complex(r, i FloatType) ComplexType
用于创建复数
函数 func imag(c ComplexType) FloatType返回复数的虚数部分
函数 func real(c ComplexType) FloatType返回复数的实数部分
函数签名: func copy(dst, src []Type) int
从源切片拷贝元素到目标切片,也可以用来拷贝字符串到[]byte。返回实际拷贝的元素个数。
函数签名: func delete(m map[Type]Type1, key Type)
用于从映射中删除条目。
函数签名: func len(v Type) int
返回数组、数组指针、切片、字符串(字节数)、缓冲通道的元素个数。如果v为nil则返回0
函数签名: func make(t Type, size ...IntegerType) Type
为切片、映射、通道分配内存空间并且初始化。
函数签名: func new(Type) *Type
为指定的类型分配内存
此内置函数用于产生一个运行时异常,通常会导致程序停止运行。此函数接收一个任意类型的参数,参数的字符串形式会被打印到控制台上。
当panic被调用时,不管是否显式调用,当前函数的执行会立即停止,并Unwind调用栈,调用defer函数。如果Unwinding到达Goroutine的栈顶,则程序终止。
使用内置函数recover可以从Unwinding中重新获得程序控制权,恢复正常执行流。recover函数终止Unwind并捕获panic的参数作为返回值,它仅仅能在defer函数体内调用。
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() } |
内置的打印函数:
1 2 |
func print(args ...Type) func println(args ...Type) |
一个整数类型,足够大以存放任何位模式的指针。
这是一个内置的接口:
1 2 3 |
type error interface { Error() string } |
fmt包在打印时,会检测变量是否实现了该接口。
很多函数都会返回error值,调用者应该检查该值是否为nil,从而判断调用是否成功:
1 2 3 4 5 6 7 |
i, err := strconv.Atoi("one") // 非零值表示调用失败 if err != nil { fmt.Printf("%v\n", err) return } fmt.Println(i) |
解析命令行参数,支持短参数(-h)和长参数(--help),支持默认值、命令帮助:
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() // 打印帮助 } } |
该包提供了基本的日志记录功能。示例用法:
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多了当前时间的前缀。要配置输出格式,可以:
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 ) |
该包为Go语言提供了反射功能。
例如判断对象是不是一个指针:
1 2 3 |
if reflect.ValueOf(req).Kind() != reflect.Ptr { } |
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
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()方法:
1 2 3 |
// 获得原始对象 interface{} 然后将其Cast为真实类型 alex, _ = alexValue.Interface().(User) fmt.Printf("%T=%v\n", alex, alex) // main.User={Alex 31} |
要判断一个值是否为零值,使用下面的代码:
1 2 3 4 |
func IsZero(v reflect.Value) bool { // 零值是无效值,但是这样判断不足够, 还需要将 当前值 和 类型的零值 进行比较 return !v.IsValid() || reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) } |
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 ) |
1 2 3 4 |
// 遍历字段 for i := 0; i < alexType.NumField(); i++ { fmt.Println(alexType.Field(i).Name) } |
写入结构的字段:
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} |
写入基本类型:
1 2 3 |
age := 31 reflect.ValueOf(&age).Elem().SetInt(32) fmt.Println(age) // 32 |
要调用方法,首先需要获取方法的Value对象:
1 2 |
alexPtrValue := reflect.ValueOf(&alex) sayHelloMthd := alexPtrValue.MethodByName("SayHello") |
MethodByName根据名称来检索一个Value(它必须是接口、结构等支持方法的对象)的方法,方法也是Value对象,但是它可被调用:
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和实际接口/结构之间可以转换。
1 2 3 |
alex := User{"Alex", 31} tag := reflect.TypeOf(alex).Field(0).Tag fmt.Println(tag) // json:"userName" |
很多现有的包都会利用字段标签,例如:
1 2 3 |
json, _ := json.Marshal(alex) fmt.Printf("%v", string(json)) // {"userName":"Alex","userAge":31} |
使用该包可以绕过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 |
注意返回值仅仅和类型有关,和值没有任何关系。
此函数返回一个类型的对齐系数(对齐倍数):
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。你也可以基于反射来获取对齐倍数:
1 |
reflect.TypeOf(x).Align() |
此函数用于获取结构中字段相对于结构起始内存地址的偏移量:
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 |
你也可以基于反射来获取偏移量:
1 |
reflect.TypeOf(u1).Field(i).Offset |
unsafe.Pointer是一种特殊的指针,它可以指向任何的类型,类似于C语言中的void*
不同具体类型的指针是无法相互转换的:
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:
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作为媒介,我们可以获得精确控制内存的能力:
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) |
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:
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)) |
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表示一个时间长度:
1 2 |
// 解析,可以使用s、m、h等单位 d, _ := time.ParseDuration("1s") |
它的本质就是纳秒数:
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和整数进行算术运算:
1 2 |
var d1 time.Duration = (60 * 1000) * time.Millisecond var d2 time.Duration = 2 * time.Minute |
也可以计算两个时间点之间的差距:
1 |
var duration time.Duration = endingTime.Sub(startingTime) |
提供伪随机数的支持。
注意随机数不随机的问题:
1 2 |
glog.Info(rand.Intn(10)) glog.Info(rand.Intn(10)) |
以上代码无论运行多少次,都输出1、7。原因是使用的“源”是固定的。因此,要产生每次调用都不一样的随机数,需要每次使用不同的源:
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) |
执行一段Bash脚本:
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() |
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() |
也可以直接将标准输出/标准错误一起作为返回值:
1 2 3 4 5 |
cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr") // 执行并收集标准输出+标准错误 stdoutStderr, err := cmd.CombinedOutput() // 执行并收集标准输出 stdout, err := cmd.Output() |
要非阻塞的启动命令,但是不等待其返回,可以:
1 2 |
cmd := exec.Command("sleep", "5") err := cmd.Start() |
你可以稍后等待其结束:
1 |
cmd.Wait() |
支持访问发送到当前进程的信号,示例代码:
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) |
为基本数据类型提供到/从字符串展现的转换,主要函数:
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) |
代码示例:
1 2 |
strconv.ParseInt("FF", 16, 0) // 255 strconv.ParseInt("0xFF", 0, 0) // 255 |
在[]byte和数字之间进行转换:
1 2 3 |
var a []byte = []byte{0, 1, 2, 3} fmt.Println(binary.BigEndian.Uint32(a)) fmt.Println(binary.LittleEndian.Uint32(a)) |
Google内部C++日志框架的Go语言复制品:
- Info/Warning/Error/Fatal函数,以及Infof等格式化版本
- V风格的日志控制(使用命令行选项-v和-vmodule=file=2)
代码示例:
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 |
支持JSON的串行化、反串行化。下面的代码示例将JSON字符串反串行化为map:
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) } |
支持YAML格式的串行化、反串行化。下面的代码示例将map串行化为YAML:
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) } |
包含各种各样的字符串处理函数。
专门用于切片成立,专注于类型安全、性能、不可变性。
包含一些内置的类型定义,不需要 go generate即可使用:
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" } |
如果希望为任何自定义的类型生成上面这样的接口需要添加注释:
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的灵活性:
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 } |
一个工具箱函数库,主要用于集合操作:
- 元素存在性判断、索引判断
- 结构转为map,map转为slice
- 任何可迭代对象的元素查找、过滤、迭代、去重
函数 | 说明 |
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 | 获得一个固定长度的随机字符串 |
能够快速的压缩/解压缩.zip, .tar, .tar.gz, .tar.bz2, .tar.xz, .tar.lz4, .tar.sz等格式,还能够解压缩.rar格式。示例:
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") |
上面的代码自动根据扩展名来判断使用何种归档、压缩算法。你也可以明确指定需要使用的算法:
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中文件条目的例子:
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 }) |
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 } } |
处理Tar格式的归档,写示例:
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) |
读示例:
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) |
处理Gzip压缩格式,下面是写tar.gz(tgz)的例子:
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) |
下面是读的例子:
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) |
支持将一般性的Map解码为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) |
深拷贝(DeepCopy)对象,不管是map、slice、pointer都能递归的解引用并正确拷贝。示例:
1 |
cs, _ := copystructure.Copy(allChartsProto) |
用于合并结构、映射。非导出字段不会被合并,导出字段则会被递归的合并:
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) |
需要注意的是:将结构合并到映射时,不会递归合并。
你还可以提供转换器,指定特定类型字段在合并之前如何转换:
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语言中定义的类型,此导入前面紧跟着的注释叫做Preamble。Preamble中可以包含任意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来定义:
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}会被替换为源码目录的绝对路径:
1 |
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo |
这是Go提供的用于支持C/C++的工具。在进行本地编译时,该工具默认启用;在进行交叉编译时,该工具默认禁用。
设置环境变量CGO_ENABLED可以控制是否使用cgo,设置为1启用,设置为0禁用。
在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 |
此外需要注意:
- 如果需要直接访问C的struct、union、enum类型,为类型名字前缀struct_、union_、enum_
- 要获得任何C类型的尺寸,调用C.sizeof_T
- 由于Go不支持C的联合体,因此联合体在Go中映射为等长度的字节数组
- Go的结构不能包含C类型的内嵌字段
- cgo把C类型转换为非导出的Go类型,因此任何Go包都不能在其导出API中暴露C类型。一个包中的C类型,和另一个包中的同名C类型是不一样的
- 在多赋值上下文中,可以调用任何C函数,第一个变量赋值为C函数返回值,第二个变量赋值为C函数调用的errno
- 目前不支持调用C函数指针,但是你可以定义Go变量来引用函数指针。C函数可以调用这种来自Go的函数指针
- C的函数,如果参数是定长数组,实参传递指向某个数组的首元素指针。在Go中调用这类函数时,你需要明确的传递数组首元素的指针: C.f(&C.x[0])
- 调用C.malloc时,不会直接调用C库中的malloc,二是调用Go对malloc的包装函数。此包装函数保证C.malloc调用不会返回nil。如果内存不足,程序直接崩溃
可以仅仅在Go代码中声明C头文件,然后再链接到对应的C库:
1 2 3 4 5 6 7 8 |
// 在当前目录下寻找libxxx进行链接 // #cgo LDFLAGS: -L ./ -lxxx // #include "xxx.h" import "C" func main() { C.xxx() } |
使用注释来标注某个函数可以导出供C使用:
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中,可以看到如下形式的声明:
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指针。但是这种函数可以:
- 将C指针作为入参,这些指针可以指向非指针、C指针,但是不能指向Go指针
- 将Go指针作为入参,但是这些指针所指向的内存,不得包含Go指针
Go代码不得在C内存中存储Go指针。C代码则可以在C内存中存储Go指针。但是在C函数返回时,必须立即停止存储Go指针。
上述规则可以在运行时动态检查。配置环境变量GODEBUG,设置其值为:
- cgocheck=1 执行廉价的动态检查
- cgocheck=0 禁用动态检查
- cgocheck=2 执行完整的检查,需要消耗一定的资源
要打破上述规则,可以使用unsafe包。另外,没有任何机制来阻止C代码做违反上述规则的事情。如果打破上述规则,程序很可能意外的崩溃。
这是一个用于管理go源代码的工具。
注意,某些子命令的行为会受到 GO111MODULE环境变量的影响。
子命令 | 说明 | ||||||
build |
编译包及其依赖,但是不安装编译结果 当编译单个main包时,结果二进制文件的basename默认和第一个源文件(go build a.go b.go输出a.exe)或者目录(go build unix/sam输出sam.exe)相同 当编译多个包或者单个非main包时,编译结果被丢弃,也就是仅仅验证编译是否可以通过 当编译包时, _test.go结尾的文件被忽略 格式:
选项: -o 仅编译单个包时可用,显式指定输出的二进制文件或对象文件的名字 构建模式: -buildmode=archive 构建非main包为.a文件,main包被忽略 示例:
|
||||||
tool compile |
将单个Go包编译为对象文件 格式: go tool compile [flags] file... 选项: -I dir1 -I dir2 导入包的搜索路径,在搜索$GOROOT/pkg/$GOOS_$GOARCH之后搜索 |
||||||
tool link |
从main包中读取Go归档/object,以及它们的依赖,并链接为二进制文件 格式:
选项: -D address 设置数据段地址
上述选项只有当源码中定义了REVISION变量且它的值没有初始化、或者初始化为常量字符串表达式时有意义:
-a 反汇编输出 链接模式: 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 |
显示包或者符号的文档 格式:
|
||||||
env |
打印Go相关环境变量信息,相关的环境变量包括: GOOS,目标操作系统,支持darwin、freebsd、linux、windows、android等 |
||||||
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 仅仅下载,不安装 |
||||||
install | 编译并安装包及其依赖 | ||||||
list | 列出包 | ||||||
run | 编译并运行程序 | ||||||
test | 测试指定的包 | ||||||
tool | 运行指定的Go tool |
go vendor 是go 1.5 官方引入依赖包管理机制。其基本思路是,将引用的外部包的源代码放在当前工程的vendor目录下面,go 1.6以后编译go代码会优先从vendor目录先寻找依赖包。这样,当前工程放到任何机器的$GOPATH/src下都可以通过编译。
如果不使用vendor机制,每次构建都需要go get依赖包,下载的依赖包版本可能和工程需要的版本不一致,导致编译问题。
仅可执行程序(main包)应该vendor依赖,共享的库则不应该。当你编写项目时,应该将所有传递性的依赖都扁平化到当前项目的vendor目录下。
依赖搜索的优先级:vendor目录 ⇨ $GOROOT ⇨ $GOPATH
go vendor无法精确的引用外部包进行版本控制,不能指定引用某个特定版本的外部包。只是在开发时,将其拷贝过来,但是一旦外部包升级,vendor下的代码不会跟着升级,而且vendor下面并没有元文件记录引用包的版本信息,这个引用外部包升级产生很大的问题,无法评估升级带来的风险。
glide是Go语言的包管理工具,它能够管理vendor目录。
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路径,如果不希望测试依赖的包,可以:
|
||
install |
安装项目的依赖,读取glide.lock文件,根据其中的commit id来安装特定版本的包 如果glide.lock文件不存在,则此命令自动调用glide update并生成glide.lock |
||
update | 更新项目的依赖 | ||
list | 列出所有的依赖项 | ||
cc | 清除Glide缓存 |
Go语言的官方的试验阶段的依赖管理工具。需要Go 1.9+版本。从1.11版本开始,Go工具链内置了和dep差异很大的依赖管理机制。
1 |
go get -u github.com/golang/dep/cmd/dep |
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的功能包括:
- 添加新的依赖
- 更新现有依赖
- 对Gopkg.toml中的规则变化作出响应
- 项目中第一次导入某个包,或者移除某个包的最后一个导入后,作出响应
命令示例:
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 |
此文件由dep init自动生成,后续主要由人工编辑。此文件可以包含几种规则声明,以控制dep的行为:
规则类型 | 说明 | ||||
constraints |
定义直接依赖如何加入到依赖图中:
|
||||
overrides |
覆盖所有依赖(直接或传递)的规则,应当小心使用。示例:
|
||||
required |
必须在任何[[constraint]]或[[override]]之前声明 dep通过分析go代码中的import语句来构建一个依赖图。required/ignored规则用于操控此依赖图 required列出必须包含在Gopkg.lock中的包的列表,此列表会和当前项目导入的包合并。required可以强制声明一个未import的包为项目的直接依赖
对于linter、generator或者其它开发工具,如果:
则可以使用required规则声明:
|
||||
ignored |
必须在任何[[constraint]]或[[override]]之前声明 dep通过分析go代码中的import语句来构建一个依赖图。required/ignored规则用于操控此依赖图 required列出dep进行静态代码分析时需要忽略的包,可以使用通配符:
|
||||
metadata | 定义供第三方工具使用的元数据,可以定义在根下,或者constraint、override下 | ||||
prune |
定义全局/某个项目的依赖修剪选项。这些选项决定了写入vendor时哪些文件被忽略 支持以下变量,取值均为布尔:
dep init会自动生成:
你可以为每个项目(依赖)定义修剪规则:
|
由于Go一直没有提供官方的依赖管理工具,导致了dep,glide,govendor,godep等工具群雄争霸的局面。vgo是解决这种现状的早期尝试,vgo即Versioned go。
1 |
go get -u golang.org/x/vgo |
这是vgo的依赖配置文件,需要放在项目的根目录下。
vgo具有同名的CLI,可以使用的子命令包括:
子命令 | 说明 |
install | 安装依赖 |
build | 编译项目 |
run | 运行项目 |
get github.com/pkg |
获取依赖的最新版本。依赖包数据会缓存到GOPATH路径下的src/mod目录 |
get github.com/pkg@v1.0 | 获取依赖的指定版本 |
mod -vendor | 将依赖直接放在vendor目录中 |
从vgo发展而来。
Go modules在1.11属于试验特性,环境变量 GO111MODULE用于控制其开关,取值auto/on/off,默认auto。
从1.16开始,默认开启go modules,即使项目中没有go.mod文件。计划在1.17中完全废弃对GOPATH模式的支持。
使用go modules进行依赖管理的日常工作流如下:
- 在Go源码中,使用import语句导入需要的包
- 调用go build/test等命令时,会自动将依赖加入到go.mod并下载依赖
- 如果需要限定依赖的版本,你可以选用以下方法之一:
- 使用go get,例如:
123go get foo@v1.2.3go get foo@mastergo get foo@e3702bed2 -
手工修改go.mod文件
- 使用go get,例如:
很多Google的库在国内无法访问,可以设置环境变量:
1 2 3 4 5 |
# 默认源 export GOPROXY=https://goproxy.io # 国内源 export GOPROXY=https://goproxy.cn |
这样,所有模块都会通过此代理下载。
你需要在仓库软件管理页面创建自己的Access Token,并配置Git:
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/" |
然后,需要设置跳过代理:
1 |
export GOPRIVATE=*.pacloud.io,*.gmem.cc |
Go modules在 go mod下定义了若干子命令:
子命令 | 说明 | ||
init |
初始化一个新模块,示例:
|
||
download | 将模块下载到本地缓存 | ||
edit | 编辑go.mod | ||
graph | 打印模块依赖图 | ||
tidy | 添加缺失的、移除多余的模块 | ||
vendor | 将依赖的副本拷贝到vendor目录 | ||
verify | 验证依赖是否满足期望 | ||
why | 解释包或模块为何被main模块需要:
|
此外,你还可能用到以下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 |
# 显示构建时会实际使用的直接、间接依赖的版本 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 |
仅仅包含四个指令:
指令 | 说明 | ||||||
module |
声明当前模块的标识: module github.com/my/thing 此标识提供了模块路径(module path),模块中所有包的导入路径,都以此模块路径为前缀。模块路径 + 包相对于go.mod的路径共同决定了包的导入路径 |
||||||
require |
声明依赖:
|
||||||
replace |
仅仅针对当前(主)模块,在构建主模块时,非主模块的go.mod中replace声明被忽略。该指令可以:
|
||||||
exclude |
仅仅针对当前(主)模块,在构建主模块时,非主模块的go.mod中exclude声明被忽略 该指令用于禁止某个直接或传递性的依赖包,例如:
|
一个Go项目通常会在Git上托管源码,例如github.com/gmem/gotools,项目的代码库的URL称为代码库(Repo)。
在同一Repo中,可以有多个包(Package),在Go 1.11中这些包被抽象为模块(Module)—— 模块是一系列相关包的集合,使用go.mod来记录模块的元数据。
Google的风格是使用单个代码库(Mono Repo),包括像Istio这样的项目,所有组件都在同一个代码库中。这种情况下,在单个代码库中创建多个模块是自然的需求,Go modules支持这种需求:
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,在此模式下:
- 你的项目源码不需要放在GOPATH下
- 源码树的顶层目录下有一个go.mod文件,这种文件定义了一个Go模块
- 放置了go.mod的目录称为模块根目录,它通常是源码库的根目录,但非必须
- 模块根目录,及其子目录中的所有Go包,都属于模块。除了那些自身定义了go.mod的子目录
- Go编译器不再在vendor、GOPATH下寻找第三方Go包,而是会直接去下载并编译,然后更新go.mod:
12345678module proj1// 分析出的依赖,放到require区域require (# 使用最新版的代码,并以Pseudo-versions形式记录github.com/gmem/gotools/x v0.0.0-20190515063616-861b08fcd24bgithub.com/gmem/gotools/y v0.0.0-20190515005150-3e3f9af80a02 // indirect 传递性依赖) -
Go编译器会把下载的依赖包,缓存到 $GOPATH/pkg/mod目录下
go.mod一旦被创建,其内容将被Go工具链管理,执行get/build/mod都会导致go.mod被修改。
命令 go list -m输出的信息被称为build list,它是构建当前module所要构建的所有相关Package(及其版本)的列表:
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:
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:
1 2 3 4 |
require ( github.com/gmem/gotools/x v1.0.0 // indirect github.com/gmem/gotools/y v1.1.0 // indirect ) |
你还可以指定依赖表达式:
1 |
go mod -require='github.com/gmem/gotools/y@>=v1.1.0' |
使用go get命令,可以更新go.mod中的依赖为指定分支的最新Commit:
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 发布后,消费者这样引用:
1 |
require helm.sh/helm v1.0.0 |
没有问题。但是如果主版本改成 3.x.x,引用:
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后缀:
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时必须这样写:
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目录:
1 |
go mod vendor |
这样,你就可以使用vendor下的包来构建当前模块了:
1 |
go build -getmode=vendor tool.go |
生成的vendor还可以用来兼容1.11以前的版本,但是由于1.11以前的版本不支持在GOPATH之外使用vendor机制,因此你需要把代码库移动到$GOPATH/src下。
Go modules支持使用vendoring模式:
1 2 3 4 5 |
# 使用环境变量 export GOFLAGS=-mod=vendor # 或者 go build -mod=vendor ... |
上述命令提示Go命令使用main模块的vendor目录来满足依赖,而忽略go.mod中的指令。
Go代码包含在工作区中:
- 你通常把所有的Go代码存放在单一的工作区中
- 一个工作区中可能包含多个版本控制仓库
- 每个仓库中可能包含一个或者多个包
- 每个包会包含一个或者多个位于单个目录中的Go源码文件
- 到达包的目录路径,对应了它的导入路径
一个工作区包含以下目录:
- src目录包含源代码。其中常常包含多个版本库。源代码文件必须总是以UTF-8方式存储
- pkg目录包含包对象
- bin目录可执行文件
该环境变量指定你的工作区的位置,默认指向$HOME/go
导入路径唯一的识别包,一个包的导入路径对应了它在工作区中、或者远程仓库中的位置。
标准库占用了很多简短的导入路径,例如fmt、net/http。设计自己的包时,你要注意避免导入路径的冲突。例如,可以使用你的GitHub账号作为包路径前缀。
变量命名使用驼峰式大小写。
不提供Getter/Setter支持,如果你有一个字段owner,建议 Owner()方法作为Getter, SetOwner()作为Setter。
一般只有一个方法的接口,以 er作为后缀,例如 Reader、Writer、 Formatter、CloseNotifier。
包名应该是导入路径的最后一段:位于src/encoding/base64的包,其导入路径为encoding/base64,而其包名是base64。
包命名方面没有硬性规定。但是作为惯例,包名应该尽可能简短、仅仅使用小写字母。
包名仅仅用于导入,不需要是全局唯一的,如果出现名字冲突,你可以在导入时赋予别名:
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命令会自动基于此路径从远程仓库自动抓取包(及其依赖的其它包):
1 |
go get github.com/golang/example/hello |
如果上述包不存在于本地工作区,则go命令会自动下载到工作区。如果上述包已经存在,则go get的行为和go install相同。
Go没有提供统一的Getter/Setter支持,参考如下方式:
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风格的驼峰式大小写,而不是使用下划线。
如果某个类型仅仅是为了实现一个接口,则应该导出接口,而非类型本身。然后使用构造函数创建类型的实例:
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倍。
引用“外部”对象具有与生俱来的开销:
- 指向对象需要8字节
- 每个独立对象具有隐藏的内存开销,可能在8-16字节之间
- 使用引用(指针)效率更低,因为GC写屏障的存在
因此,如果不需要共享OtherStuff,使用下面的风格:
1 2 3 |
type MyContainer struct { inlineStruct OtherStuff } |
而非:
1 2 3 |
type MyContainer struct { outoflineStruct *OtherStruct } |
倾向于:
1 2 |
// 由调用者提供缓冲 func (r *Reader) Read(buf []byte) (int, error) |
而不是:
1 |
func (r *Reader) Read() ([]byte, error) |
后者可能导致大量内存分配。
- Goroutine基本上只有栈的开销,一开始栈只有2KB
- 尽管Goroutine成本较低,还是要避免在主要的请求处理路径上创建Goroutine。提前创建Goroutine并让其等待输入
为任何命名类型实现如下签名的方法,即可获得类似Java的toString()的功能:
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 } |
为了保留调试信息,你需要在构建时设置特定的标记:
1 2 |
go build -i -a -o build/eb-rdbg -gcflags="all=-N -l" -ldflags='-linkmode internal' -gcflags="-N -l" # 1.10之前的版本,用于保留调试信息 |
你需要通过dlv来启动被调试应用程序,dlv具有反复重启被调试程序的能力:
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 |
你需要连接到被调试应用程序:
1 |
dlv connect localhost:2345 |
如果使用IDE,则更加简单。例如对于Goland,你只需要创建一个Go Remote类型的Run Configuration即可。
连接到被调试应用程序后,你会看到 (dlv)命令提示符,以下命令可用:
命令 | 说明 | |||||
h | help |
显示命令列表,或者显示某个命令的帮助:
|
||||
config |
进行dlv配置:
示例:
|
|||||
b | break |
设置断点,格式: break [name] <linespec> 其中linespec格式可以是:
示例:
|
||||
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>] | |||||
估算一个表达式的值: [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 | 打印寄存器内容 |
使用命令 go mod tidy可以发现根源。
原因是构建时没有引用目标函数所在文件,可以这样进行构建:
1 |
go build *.go |
找不到可执行文件:could not launch process: fork/exec /eb-rdbg: no such file or directory
实际上此文件是存在的,只是二进制格式不支持。CGO_ENABLED=0后发现解决。
命令: go get -u git.pacloud.io/pks/addons-api-server@develop
报错原因:可能因为执行命令时Gitlab正在处理develop分支的Push
基于Go Modules进行构建,或者执行命令 go mod tiny可能产生此错误。可能需要清除mod缓存重新下载:
1 2 3 |
go clean -modcache cd project && rm go.sum go mod tidy |
Leave a Reply