Go Cheat Sheet
参考:An overview of Go syntax and features.
文中大量的代码都是取自 A Tour of Go (非常好的 Go 入门教程)。
如果是刚入门,非常建议先从这份教程上手。
简而言之,就是:
安装 1 2 3 4 5 6 7 8 9 10 11 12 wget -c https://dl.google.com/go/go1.17.4.linux-amd64.tar.gz -O /usr/local/src/go1.17.4.linux-amd64.tar.gz tar -zxvf /usr/local/src/go1.17.4.linux-amd64.tar.gz -C /usr/local/ /usr/local/go/bin/go version vim ~/.zshrc export GOPATH=/opt/go export GOROOT=/usr/local/go export GOARCH=386 export GOBIN=$GOROOT/bin/ export GOTOOLS=$GOROOT/pkg/tool/ export PATH=$PATH:$GOBIN:$GOTOOLS
其它常用的环境变量:
测试运行:test/hello.go
1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Println("Hello, World!" ) }
测试 Go module:test-module/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "github.com/valyala/fasthttp" "go.uber.org/zap" ) var logger *zap.Loggerfunc init () { logger, _ = zap.NewProduction() } func fastHTTPHandler (ctx *fasthttp.RequestCtx) { logger.Info("hello, go module" , zap.ByteString("uri" , ctx.RequestURI())) } func main () { fasthttp.ListenAndServe(":8081" , fastHTTPHandler) }
1 2 3 4 go mod init github.com/yipwinghong/test-module go mod tidy go build main.go ./main
远程开发插件 打开 Visual Studio Code,在 Extentions
搜索并安装 Remote - SSH
。
配置服务器连接信息 ~/.ssh/config
:
1 2 3 4 5 6 Host wsl HostName 127.0.0.1 Port 25 User root IdentityFile ~/.ssh/id_rsa
配置好本地到服务器的 SSH public key,并尝试连接 Connect to Host in Current Window
。
在 Extention
可安装用于远程项目的插件。
此时在 Explorer
可以打开 target server 上的项目目录 Open Folder
。或直接 clone GitHub 上的项目到本地(Clone Repository
, 需要授权)。
代码格式 建议:
代码都必须用 gofmt 进行格式化。
运算符和操作数之间要留空格。
建议一行代码不超过 120 个字符,超过部分,请采用合适的换行方式换行。例外场景:import 行、工具自动生成的代码、带 tag 的 struct 字段。
文件长度不能超过 800 行。
函数长度不能超过 80 行。
import 规范代码都必须用 goimports 进行格式化(建议将代码 Go 代码编辑器设置为:保存时运行 goimports)。不要使用相对路径引入包,例如 import …/util/net ;包名称与导入路径的最后一个目录名不匹配时,或者多个相同包名冲突时,则必须使用导入别名。
导入的包建议进行分组,匿名包的引用使用一个新的分组,并对匿名包引用进行说明。
操作符 不支持前置的自增(++)自减(–)。
算术
操作符
描述
+
加
-
减
*
乘
/
除
%
求余
&
按位与
|
按位或
^
按位异或
&^
按位清零(且非)
<<
左移
>>
右移
比较
操作符
描述
==
等于
!=
不等于
<
小于
<=
小于等于
>
大于
>=
大于等于
逻辑
操作符
描述
&&
逻辑且
||
逻辑或
!
逻辑非
其他
操作符
描述
&
取址 / 创建指针
*
取值
<-
发送 / 接收 (见下文“Channels”)
声明 变量 类型在标识符后面,且赋值时可自动推断类型。
1 2 3 4 5 6 var foo int var foo int = 42 var foo, bar int = 42 , 1302 var foo = 42 foo := 42 const constant = "This is a constant"
交换两个变量的值(在同一个语句中可对多个变量同时赋值)。
1 2 3 a := 1 b := 2 a, b = b, a
变量声明尽量放在变量第一次使用的前面,遵循就近原则。
变量名称首字母为大写时表示该变量为可导出的全局变量,应尽量少用。
通过参数传递,使每个函数都是“无状态”的,可以减少耦合,也方便分工和单元测试。
常量 如果魔法数字出现超过两次,则禁止使用,改用常量代替。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const Pi = 3.14 const ( Big = 1 << 100 Small = Big >> 99 ) func needInt (x int ) int { return x*10 + 1 } func needFloat (x float64 ) float64 { return x * 0.1 } func TestConst () { fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big)) }
快速设置连续的值:通常用于系统内部变量,不建议在业务代码中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const ( Monday = 1 + iota Tuesday Wednesday ) const ( Readable = 1 << iota Writable Executable ) func TestConstantTry (t *testing.T) { t.Log(Monday, Tuesday) } func TestConstantTry1 (t *testing.T) { a := 1 t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable) }
对于未导出的顶层常量和变量,使用 _
作为前缀。
1 2 3 4 5 6 7 8 9 10 11 const ( defaultHost = "127.0.0.1" defaultPort = 8080 ) const ( _defaultHost = "127.0.0.1" _defaultPort = 8080 )
初始化 内建函数 new
和 make
可用于分配内存。
new
不会执行初始化,只是将内存置零。
new(T)
为类型为 T
的项分配已置零的内存空间并返回地址,即类型为 *T
的值。
由于内存已置零,在设计数据结构时就不需要初始化即可工作。
1 2 3 4 5 6 7 type SyncedBuffer struct { lock sync.Mutex buffer bytes.Buffer } p := new (SyncedBuffer) var v SyncedBuffer
当初始化工作比较复杂,可考虑创建初始化构造函数、对外提供结构的创建服务。
make
make(T, args)
只用于创建切片、映射和信道,并返回类型为 T
(而非 *T
)的已初始化(而非置零)的值(而非指针)。比如:
即创建长度为 100 的数组空间,并创建长度为 10、容量为 100 的、指向该数组前 10 个元素的切片。
与 new
的区别:
1 2 3 4 5 6 7 8 9 var p *[]int = new ([]int ) var v []int = make ([]int , 100 ) var p *[]int = new ([]int )*p = make ([]int , 100 , 100 ) v := make ([]int , 100 )
函数 参数总是值传递,对于引用类型(切片、字典、信道),都会复制其值再传入函数,不会复制它们引用的底层数据(浅拷贝)。
如要修改传入参数的值,应以复制指针传入函数(指向同一块内存)。
函数本身可作为变量的值,也可作为参数和返回值(可返回多个值)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func f () {}func f (param1 string , param2 int ) {}func f (param1, param2 int ) {}func f () int { return 42 } func f () (int , string ) { return 42 , "foobar" } func f () (n int , s string ) { n = 42 s = "foobar" return } var x, str = f()
当函数中需要使用到多个变量时,可在函数开始处使用 var
声明。
在函数外部声明必须使用 var
,不要采用 :=
,容易踩到变量的作用域的问题。
1 2 3 4 var ( Width int Height int )
使用建议:
传入变量和返回变量以小写字母开头。
函数参数个数不超过 5 个、返回值不超过 3 个,否则使用结构体。
函数应按粗略的调用顺序排序。同一文件中的函数应按接收者分组。
尽量采用值传递,而非指针传递(对于较大的结构体可使用指针)。
传入参数是 map、slice、chan、interface ,不要传递指针。
高阶函数 函数作为参数:
1 2 3 4 5 6 7 8 9 type operate func (x, y int ) int func calculate (x int , y int , op operate) (int , error) { if op == nil { return 0 , errors.New("invalid operation" ) } return op(x, y), nil }
函数作为返回值:
1 2 3 4 5 6 7 8 9 10 type calculateFunc func (x int , y int ) (int , error) func genCalculator (op operate) calculateFunc { return func (x int , y int ) (int , error) { if op == nil { return 0 , errors.New("invalid operation" ) } return op(x, y), nil } }
闭包 作为参数的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { add := func (a, b int ) int { return a + b } fmt.Println(add(3 , 4 )) } func scope () func () int { outer_var := 2 return func () int { return outer_var } } func another_scope () func () int { outer_var = 444 return foo }
闭包:其内部逻辑不完整、其中一部分由自由变量构成。在闭包函数定义时状态未知,即动态生成部分程序逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func intSeq () func () int { i := 0 return func () int { i++ return i } } func genCalculator (op operate) calculateFunc { return func (x int , y int ) (int , error) { if op == nil { return 0 , errors.New("invalid operation" ) } return op(x, y), nil } }
可变参数 即在最后一个参数的类型名称之前加上 ...
,可指示接收零或多个参数。该函数的调用方式与其他函数一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { fmt.Println(adder(1 , 2 , 3 )) fmt.Println(adder(9 , 9 )) nums := []int {10 , 20 , 30 } fmt.Println(adder(nums...)) } func adder (args ...int ) int { total := 0 for _, v := range args { total += v } return total }
递归 1 2 3 4 5 6 func fact (n int ) int { if n == 0 { return 1 } return n * fact(n-1 ) }
处理集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 func Index (vs []string , t string ) int { for i, v := range vs { if v == t { return i } } return -1 } func Include (vs []string , t string ) bool { return Index(vs, t) >= 0 } func Any (vs []string , f func (string ) bool ) bool { for _, v := range vs { if f(v) { return true } } return false } func All (vs []string , f func (string ) bool ) bool { for _, v := range vs { if !f(v) { return false } } return true } func Filter (vs []string , f func (string ) bool ) []string { vsf := make ([]string , 0 ) for _, v := range vs { if f(v) { vsf = append (vsf, v) } } return vsf } func Map (vs []string , f func (string ) string ) []string { vsm := make ([]string , len (vs)) for i, v := range vs { vsm[i] = f(v) } return vsm }
控制结构 if 1 2 3 4 5 6 7 8 9 10 11 12 if x > 10 { return x } else if x == 10 { return 10 } else { return -x if a := b + c; a < 42 { return a } else { return a - 42
1 2 3 4 5 6 var val interface {} = "foo" if str, ok := val.(string ); ok { fmt.Println(str) }
if
接受初始化语句,约定如下方式建立局部变量。
1 2 3 4 if err := loadConfig(); err != nil { return err }
for 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 for i := 1 ; i < 10 ; i++ {} for ; i < 10 ; { } for i < 10 { } for { } for i := 0 ; i < 2 ; i++ { for j := i + 1 ; j < 3 ; j++ { if i == 0 { continue here } fmt.Println(j) if j == 2 { break } } } for i := 0 ; i < 2 ; i++ { for j := i + 1 ; j < 3 ; j++ { if j == 1 { continue } fmt.Println(j) if j == 2 { break there } } }
range range
表达式可用于切片/数组、字符串、字典、信道。
除了访问信道时可能会阻塞,其它情况下只会在 for
语句开始执行时被求值一次,无论后面有多少次迭代。
range
表达式求值结果会被复制,即被迭代的对象是表达式结果值的副本,实际迭代时不会使用原值(具体影响视乎结果值是值类型或引用类型)。
1 2 3 4 5 6 7 8 9 10 11 numbers2 := [...]int {1 , 2 , 3 , 4 , 5 , 6 } maxIndex2 := len (numbers2) - 1 for i, e := range numbers2 { if i == maxIndex2 { numbers2[0 ] += e } else { numbers2[i+1 ] += e } } fmt.Println(numbers2)
每次迭代都会执行赋值,当切片元素为 string
等类型,赋值会涉及内存拷贝、开销较大,可使用下标访问来优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 for _, v := range s{ if v == "xxx" { } } for i := range s { if s[i] == "xxx" { } }
switch 每个分支不需要加上 break
,但必须有 default
分支。
1 2 3 4 5 6 7 8 9 10 11 switch operatingSystem { case "darwin" : fmt.Println("Mac OS Hipster" ) case "linux" : fmt.Println("Linux Geek" ) default : fmt.Println("Other" ) }
基本用法:
1 2 3 4 5 6 7 8 9 10 11 switch os := runtime.GOOS; os { case "darwin" : fmt.Println("OS X." ) case "linux" : fmt.Println("Linux." ) default : fmt.Printf("%s.\n" , os) }
1 2 3 4 5 6 7 8 9 10 number := 42 switch { case number < 42 : fmt.Println("Smaller" ) case number == 42 : fmt.Println("Equal" ) case number > 42 : fmt.Println("Greater" ) }
1 2 3 4 5 6 var char byte = '?' switch char { case ' ' , '?' , '&' , '=' , '#' , '+' , '%' : fmt.Println("Should escape" ) }
1 2 3 4 5 6 7 8 9 10 11 whatAmI := func (i interface {}) { switch t := i.(type ) { case bool : fmt.Println("I'm a bool" ) case int : fmt.Println("I'm an int" ) default : fmt.Printf("Don't know type %T\n" , t) } }
defer 延迟执行(函数返回时逆序取出执行),且确保不受 panic
影响。
被推迟函数的实参在出现 defer
语句时就会求值,而不是 defer
调用时才求值:
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 func trace (s string ) string { fmt.Println("entering:" , s) return s } func un (s string ) { fmt.Println("leaving:" , s) } func a () { defer un(trace("a" )) fmt.Println("in a" ) } func b () { defer un(trace("b" )) fmt.Println("in b" ) a() } func main () { b() }
常用于解锁,释放资源等(可避免在多个分支都需要释放时忘记操作)。
当创建资源时,应紧跟 defer
释放资源。
defer
在 Go1.14 版本中性能大幅提升,即使在性能敏感型的业务中损耗也可忽略。
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 func createFile (p string ) *os .File { fmt.Println("creating" ) f, err := os.Create(p) if err != nil { panic (err) } return f } func writeFile (f *os.File) { fmt.Println("writing" ) fmt.Fprintln(f, "data" ) } func closeFile (f *os.File) { fmt.Println("closing" ) err := f.Close() if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n" , err) os.Exit(1 ) } } func TestDefer (t *testing.T) { f := createFile("/tmp/defer.txt" ) defer closeFile(f) writeFile(f) }
不要在 for 循环里面使用 defer
,defer
只有在函数退出时才会执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 for file := range files { fd, err := os.Open(file) if err != nil { return err } defer fd.Close() } for file := range files { func () { fd, err := os.Open(file) if err != nil { return err } defer fd.Close() }() }
先判断是否错误,再 defer
释放资源,比如:
1 2 3 4 5 6 rep, err := http.Get(url) if err != nil { return err } defer resp.Body.Close()
goto 业务代码禁止使用 goto
,框架或其他底层源码尽量不用。
select 用于操作多个信道,具体用法参考 信道
。
只要存在 default
就不会阻塞。
永久阻塞:
快速检错:利用 default
优先级较低的特性,只有在其它 case
不执行时才会被只需。
1 2 3 4 5 select { case err = <-errCh: default : }
数据类型 Go 是静态强类型语言,不支持隐式类型转换(包括别名与原类型之间)。
所有 Go 预先声明的标识符都在 builtin 包中定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte rune float32 float64 complex64 complex128 func TestZeorValue (t *testing.T) { var i int var f float64 var b bool var s string fmt.Printf("%v %v %v %q\n" , i, f, b, s) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type MyInt int64 func TestImplicit (t *testing.T) { var a int32 = 1 var b int64 b = int64 (a) var c MyInt c = MyInt(b) t.Log(a, b, c) } func TestPoint (t *testing.T) { a := 1 aPtr := &a t.Log(a, aPtr) t.Logf("%T %T" , a, aPtr) } func TestString (t *testing.T) { var s string t.Log("*" + s + "*" ) t.Log(len (s)) }
字符串 string
是基本数据类型(不是引⽤或指针)。
string
是只读的 byte
切片,len
函数可以求它所包含的 byte 数。
string
的 byte
数组可以存放任何数据。
常用的字符串包见:strings , strcov 。
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 func TestString (t *testing.T) { var s string t.Log(s) s = "hello" t.Log(len (s)) s = "\xE4\xBA\xBB\xFF" t.Log(s) t.Log(len (s)) s = "中" t.Log(len (s)) c := []rune (s) t.Log(len (c)) t.Logf("中 unicode %x" , c[0 ]) t.Logf("中 UTF8 %x" , s) } func TestStringToRune (t *testing.T) { s := "中华人民共和国" for _, c := range s { t.Logf("%[1]c %[1]x" , c) } } func TestStringFn (t *testing.T) { s := "A,B,C" parts := strings.Split(s, "," ) for _, part := range parts { t.Log(part) } t.Log(strings.Join(parts, "-" )) } func TestConv (t *testing.T) { s := strconv.Itoa(10 ) t.Log("str" + s) if i, err := strconv.Atoi("10" ); err == nil { t.Log(10 + i) } }
判空:字符串可以为 ""
,但不会为 nil
。
1 2 3 4 5 6 7 8 9 if s == "" { } if len (s) == 0 { }
[]byte/string
相等比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 var s1 []byte var s2 []byte ... bytes.Equal(s1, s2) == 0 bytes.Equal(s1, s2) != 0 var s1 []byte var s2 []byte ... bytes.Compare(s1, s2) == 0 bytes.Compare(s1, s2) != 0
string
常用于需要比较、不需要 nil
串的场景。
[]byte
则适用于需要修改字符串(支持粒度为字节),需要使用 nil
表示的场景,而且 []
支持切片操作。
1 2 3 4 5 6 s1 := "hello" b := []byte (s1) s2 := string (b)
1 2 3 4 5 6 7 8 9 10 11 12 13 func String2Bytes (s string ) []byte { sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh := reflect.SliceHeader{ Data: sh.Data, Len: sh.Len, Cap: sh.Len, } return *(*[]byte )(unsafe.Pointer(&bh)) } func Bytes2String (b []byte ) string { return *(*string )(unsafe.Pointer(&b)) }
要复制数据,使用 copy
性能比 append
高:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var ( src = make ([]byte , 512 ) dst = make ([]byte , 512 ) ) for n := 0 ; n < b.N; n++ { rand.Read(src) copy (dst, src) } for n := 0 ; n < b.N; n++ { rand.Read(src) dst = append (dst, src...) }
格式化(优先使用 strconv
而不是 fmt
):
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 p := point{1 , 2 } fmt.Printf("%v\n" , p) fmt.Printf("%+v\n" , p) fmt.Printf("%#v\n" , p) fmt.Printf("%T\n" , p) fmt.Printf("%t\n" , true ) fmt.Printf("%d\n" , 123 ) fmt.Printf("%b\n" , 14 ) fmt.Printf("%c\n" , 33 ) fmt.Printf("%x\n" , 456 ) fmt.Printf("%f\n" , 78.9 ) fmt.Printf("%e\n" , 123400000.0 ) fmt.Printf("%E\n" , 123400000.0 ) fmt.Printf("%s\n" , "\"string\"" ) fmt.Printf("%q\n" , "\"string\"" ) fmt.Printf("%x\n" , "hex this" ) fmt.Printf("%p\n" , &p) fmt.Printf("|%6d|%6d|\n" , 12 , 345 ) fmt.Printf("|%6.2f|%6.2f|\n" , 1.2 , 3.45 ) fmt.Printf("|%-6.2f|%-6.2f|\n" , 1.2 , 3.45 ) fmt.Printf("|%6s|%6s|\n" , "foo" , "b" ) fmt.Printf("|%-6s|%-6s|\n" , "foo" , "b" ) s := fmt.Sprintf("a %s" , "string" ) fmt.Println(s) fmt.Fprintf(os.Stderr, "an %s\n" , "error" )
string
表示的是不可变的字符串变量,对 string
的修改是比较重的操作,基本上都需要重新申请内存。
在需要修改时多使用 []byte
(JSON 序列化除外,[]byte
序列化会被 base64,引起跨语言解析失败)。
类型转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var i int = 42 var f float64 = float64 (i)var u uint = uint (f)i := 42 f := float64 (i) u := uint (f) f, _ := strconv.ParseFloat("1.234" , 64 ) i, _ := strconv.ParseInt("123" , 0 , 64 ) d, _ := strconv.ParseInt("0x1c8" , 0 , 64 ) u, _ := strconv.ParseUint("789" , 0 , 64 ) k, _ := strconv.Atoi("135" ) _, e := strconv.Atoi("wat" )
对于未知的、不被认可的、无法展示的字符,会被替换为 ‘�’(Unicode 码点为 ‘U+FFFD’)。
类型声明 声明语句创建了新的类型名称,和现有类型底层结构相同。
新类型提供了一个方法用来分隔不同概念的类型,即使底层类型相同也不兼容。
1 2 type Celsius float64 type Fahrenheit float64
对于类型别名 type Celsius = float64
,等号两边为相同的类型。
两者不可以被相互比较或混在一个表达式运算。刻意区分类型可避免无意中使用不同单位的温度混合计算导致的错误。
而需要类似 Celsius(t)
或 Fahrenheit(t)
形式的显式转型操作才能将 float64 转为对应的类型。
类型断言 类型断言表达式 v, ok := x.(T)
,x
表示要被判断类型的值,当下的类型必须是任意的接口类型(通常是空接口 interface{}
)。
1 2 3 4 5 6 7 8 9 var val interface {} = "foo" str := val.(string ) if str, ok := val.(string ); ok { fmt.Println(str) }
由于其单个返回值针对不正确的类型将产生 panic
,建议始终使用 “comma ok”的惯用法。
指针 指针保存值的内存地址,不支持运算。
1 2 3 4 5 6 7 p := Vertex{1 , 2 } q := &p r := &Vertex{1 , 2 } var s *Vertex = new (Vertex)
带指针参数的函数必须接受一个指针:
1 2 3 4 5 6 7 8 func ScaleFunc (v *Vertex, f float64 ) { v.X = v.X * f v.Y = v.Y * f } var v VertexScaleFunc(v, 5 ) ScaleFunc(&v, 5 )
以指针为接收者的方法被调用时,接收者既能为值又能为指针:
1 2 3 4 5 6 7 8 9 10 11 func (v *Vertex) Scale (f float64 ) { v.X = v.X * f v.Y = v.Y * f } var v Vertexv.Scale(5 ) p := &v p.Scale(10 )
使用指针接收者:
指针用于寻址,但以下的值不可寻址(即不可变、不安全的临时结果):
常量的值。
基本类型值的字面量。
算术操作的结果值。
对各种字面量的索引表达式和切片表达式的结果值(例外:对切片字面量的索引结果值可寻址)。
对字符串变量的索引表达式和切片表达式的结果值。
对字典变量的索引表达式的结果值。
函数字面量和方法字面量,以及对它们的调用表达式的结果值。
结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值。
类型转换表达式的结果值。
类型断言表达式的结果值。
接收表达式的结果值。
结构 结构是一种类型,也是字段的集合。可附带方法,不支持继承。
对于需要序列化的字段,请以大写字母开头,否则 json.Marshal
时会被忽略。
1 2 3 4 5 6 7 type Vertex struct { X, Y int } var v = Vertex{1 , 2 }var v = Vertex{X: 1 , Y: 2 }var vs = []Vertex{{1 ,2 },{5 ,2 },{5 ,5 }}
声明和初始化格式建议采用多行。
1 2 3 4 5 6 7 8 9 type User struct { Username string Email string } user := User{ Username: "colin" , Email: "colin404@foxmail.com" , }
在初始化结构引用时,请使用 &T{}
代替 new(T)
,以使其与结构体初始化一致。
1 2 3 4 5 6 sptr := new (T) sptr.Name = "bar" sptr := &T{Name: "bar" }
方法 支持值方法与指针方法。
值方法的接收者是该方法所属的类型值的一个副本(修改不会体现在原值上,除非使用切片或字典等引用类型的别名类型);指针方法的接收者是该方法所属的基本类型值的指针值的一个副本(在方法内可修改该副本指向的值)。
一个自定义数据类型的方法集合中仅包含它的所有值方法,而该类型的指针类型的方法集合包含前者的所有方法,包括所有值方法和所有指针方法。
如果基本类型和它的指针类型的方法集合不同,则其具体实现的接口类型的数量就也会有差异(可能两者都是 0,或一个指针类型实现了某接口类型,但其基本类型却不一定能作为该接口的实现类型)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (v Vertex) Abs () float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func (v *Vertex) Add (n float64 ) { v.X += n v.Y += n } v := Vertex{1 , 2 } v.Abs() p := &v p.X = 1e9 fmt.Println(v)
除了结构以外,几乎任何(自定义的)类型都能添加方法:
1 2 3 4 5 6 7 8 9 10 11 type Counter int func (c *Counter) ServeHTTP (w http.ResponseWriter, req *http.Request) { c.n++ fmt.Fprintf(w, "counter = %d\n" , c.n) }
建议方法接收器以类名第一个英文首字母的小写命名(不能采用 me、this、self 这类易混淆名称),且在函数超过 20 行时不要用单字符。
对于需要外部访问的字段,可提供对外暴露的方法(Setter、Getter 方法,不建议以 GetXxx
、SetXxx
命名):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type User struct { name string age int } func (u User) GetName string { return u.name } func (u User) Name string { return u.name }
匿名结构 比使用 map[string]interface{}
开销更低、更安全.
1 2 3 point := struct { X, Y int }{1 , 2 }
扩展(不支持访问子类的字段、方法,无法重载等):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Pet struct {}func (p *Pet) Speak () { fmt.Print("..." ) } func (p *Pet) SpeakTo (host string ) { p.Speak() fmt.Println(" " , host) } type Dog struct { Pet } func (d *Dog) Speak () { fmt.Print("Wang!" ) } func TestDog (t *testing.T) { dog := new (Dog) dog.SpeakTo("Chao" ) }
嵌入 Go 中没有子类化,但有接口和结构嵌入。特点是更灵活,组合相比继承的优势是非侵入性,不会破坏类型的封装、加重类型之间的耦合。
其中嵌入结构的同名方法会被隐藏:类似继承父类的子类会重写父类方法,但仍可以通过链式调用找到目标方法。
如果处于同一个层级的多个嵌入字段拥有同名的字段或方法,在被嵌入类型的值处,选择此名称时就会引发编译错误,因为编译器无法确定被选择的成员。
嵌入式类型应位于结构体内的字段列表的顶部,并且建议有一个空行将嵌入式字段与常规字段分隔开。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type ReadWriter interface { Reader Writer } type Server struct { *log.Logger Host string Port int } server := &Server{"localhost" , 80 , log.New(...)} server.Log(...) var logger *log.Logger = server.Logger
接口 接口类型无法被值化,在赋予它实际的值(实现)之前,其值一定是 nil
,即接口类型的零值。
接口是非侵入性的,实现不依赖于接口定义,因此接口的定义可以包含在接口使用者包内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Programmer interface { WriteHelloWorld() string } type GoProgrammer struct { name string } func (g *GoProgrammer) WriteHelloWorld () string { return "fmt.Println(\"Hello World\")" } func TestClient (t *testing.T) { var p Programmer p = new (GoProgrammer) t.Log(p.WriteHelloWorld()) }
可在结构中直接嵌套显式写入该接口,表示该接口实现了该接口。但只是起提示作用,非必须。
在 Go 中数据结构只需要实现了接口的一个方法,就隐式地实现了该接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 type Service interface { GetFoo() Foo } type DemoService struct { Service } func (s *DemoService) GetFoo () Foo { return Foo{ Name: "i am foo" , } }
接口的嵌入不允许方法间的隐藏,组合的方法有同名的方法则会发生编译错误。
建议使用更小的接口,并通过小接口之间的组合扩展程序、增加灵活性。比如 io.ReadWriteCloser
就是由 io.Reader
、io.Writer
、io.Closer
组成(都只有一个方法)。
多态 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 type Code string type Programmer interface { WriteHelloWorld() Code } type GoProgrammer struct {} func (p *GoProgrammer) WriteHelloWorld () Code { return "fmt.Println(\"Hello World!\")" } type JavaProgrammer struct {} func (p *JavaProgrammer) WriteHelloWorld () Code { return "System.out.Println(\"Hello World!\")" } func writeFirstProgram (p Programmer) { fmt.Printf("%T %v\n" , p, p.WriteHelloWorld()) } func TestPolymorphism (t *testing.T) { goProg := &GoProgrammer{} javaProg := new (JavaProgrammer) writeFirstProgram(goProg) writeFirstProgram(javaProg) }
空接口 空接口可表示任何类型,通过断言可将空接口转换为指定类型:v, ok := p.(int)
(可类比 Java 的 Object
、C/C++ 的 void *
指针)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func DoSomething (p interface {}) { switch v := p.(type ) { case int : fmt.Println("Integer" , v) case string : fmt.Println("String" , v) default : fmt.Println("Unknow Type" ) } } func TestEmptyInterfaceAssertion (t *testing.T) { DoSomething(10 ) DoSomething("10" ) }
由于编译过程无法检查 interface{}
的转换,只能在运行时检查,小心引起 panic
。
完整性检查 编译器没有严格检查其是否实现了 interface 包含的所有方法。
要实现强验证,可声明 _
变量把 nil
的空指针转换成接口类型,如果没有实现其所有的方法,在编译阶段会报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type Shape interface { Sides() int Area() int } type Square struct { len int } func (s *Square) Sides () int { return 4 } var _ Shape = (*Square)(nil )
容器 对于切片和字典,都建议指定容器容量,以便为容器预先分配内存:
1 2 v := make (map [int ]string , 4 ) v := make ([]string , 0 , 4 )
判空操作(适用于 slice、map、channel):
1 2 3 4 5 6 7 8 9 if len (slice) = 0 { } if slice != nil && len (slice) == 0 { }
数组 数组长度是其类型的一部分,因此不能改变大小。
1 2 3 4 5 6 7 8 var a [10 ]int a[3 ] = 42 i := a[3 ] var a = [2 ]int {1 , 2 }a := [2 ]int {1 , 2 } a := [...]int {1 , 2 }
只有相同维数且含有相同元素个数的数组才可以进行比较:所有元素相等才相等。
切片 切片为底层数组的视图(零值为 nil
),提供动态大小、灵活的访问方式。
函数传参是值传递,数组参数传入的是整个数组的内容,而切片传入的则是对底层数组的引用。
1 2 3 4 5 6 7 a := []int {1 ,2 ,3 ,4 } chars := []string {0 :"a" , 2 :"c" , 1 : "b" } var b = a[lo:hi] var b = a[1 :4 ] var b = a[:3 ] var b = a[3 :]
1 2 3 4 5 6 7 a = make ([]byte , 5 , 5 ) a = make ([]byte , 5 ) x := [3 ]string {"Лайка" , "Белка" , "Стрелка" } s := x[:]
append
函数可为切片添加元素,要小心自动分配内存,其返回的可能是新分配的地址。
1 2 a = append (a, 17 , 3 ) c := append (a, b...)
从数组中创建的切片的容量等于数组的 容量 - 切片的起始下标
。len(a)
返回数组/切片的长度,cap(a)
返回数组/切片的容量。
1 2 3 4 b := make ([]int , 0 , 5 ) b = b[:cap (b)] b = b[1 :]
切片容量可视作透过窗口最多可以看到底层数组中元素的个数,其窗口可以向右扩展直至底层数组的末尾,但无法向左扩展。
复制元素:
1 2 3 4 5 6 7 8 9 10 11 var b1, b2 []byte for i, v := range b1 { b2[i] = v } for i := range b1 { b2[i] = b1[i] } copy (b2, b1)
range
遍历:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 for i, e := range a {} for _, e := range a {} for i := range a {} for range time.Tick(time.Second) { }
二维切片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 board := [][]string { []string {"_" , "_" , "_" }, []string {"_" , "_" , "_" }, []string {"_" , "_" , "_" }, } board[0 ][0 ] = "X" board[2 ][2 ] = "O" board[1 ][2 ] = "X" board[1 ][0 ] = "O" board[0 ][2 ] = "X" for i := 0 ; i < len (board); i++ { fmt.Printf("%s\n" , strings.Join(board[i], " " )) }
使用建议:
创建切片时预估容量,避免追加时扩容。
切片拷贝时需要判断时机拷贝元素个数。
谨慎使用多个切片操作同一个数组,以防读写冲突。
字典 非线程安全,在并发中需要加锁,编译时加上竞态检查(-race
)。
key 之间通过 ==
和 !=
进行比较,不能以函数、切片、字典类型(包括接口类型 interface{}
)为 key,
key 类型长度越小,比较效率越高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 m := make (map [string ]int , 10 ) var m = map [interface {}]int { "1" : 1 , []int {2 }: 2 , 3 : 3 , } var m = map [string ]Vertex{ "Bell Labs" : Vertex{ 40.68433 , -74.39967 }, "Google" : { 37.42202 , -122.08408 }, }
1 2 3 4 if elem, ok := m["key" ]; ok { fmt.Println(m["key" ]) }
如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。
1 2 m["key" ] = 42 delete (m, "key" )
遍历:
1 2 for key, value := range m {}
函数字典 map[int]func(){}
实现工厂模式:
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 func TestMapWithFunValue (t *testing.T) { m := map [int ]func (op int ) int {} m[1 ] = func (op int ) int { return op } m[2 ] = func (op int ) int { return op * op } m[3 ] = func (op int ) int { return op * op * op } t.Log(m[1 ](2 ), m[2 ](2 ), m[3 ](2 )) } func TestMapForSet (t *testing.T) { mySet := map [int ]bool {} mySet[1 ] = true n := 3 if mySet[n] { t.Logf("%d is existing" , n) } else { t.Logf("%d is not existing" , n) } mySet[3 ] = true t.Log(len (mySet)) delete (mySet, 1 ) n = 1 if mySet[n] { t.Logf("%d is existing" , n) } else { t.Logf("%d is not existing" , n) } }
Go 的内置集合中没有 Set,但可以利用 map[type]bool
实现。
错误 error
是可恢复的错误,panic
为不可恢复错误(退出前会执行 defer 的内容)。
可能产生错误的函数通常会声明一个额外的 error
类型返回值(参考 error ),一般是最后一个参数。
1 2 3 4 5 6 7 8 9 func load () (error, int ) { } func load () (int , error) { }
其中 error
类型实现了 error
接口,提供 Error
方法,并可以通过 errors.New("xxx")
创建错误实例。
1 2 3 4 type error interface { Error() string }
1 2 3 var LessThanTwoError = errors.New("n should be not less than 2" )var LargerThenHundredError = errors.New("n should be not larger than 100" )
建议应尽早进行错误处理,并尽早返回,减少嵌套。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func GetFibonacci (n int ) ([]int , error) { if n < 2 { return nil , LessThanTwoError } if n > 100 { return nil , LargerThenHundredError } fibList := []int {1 , 1 } for i := 2 ; i < n; i++ { fibList = append (fibList, fibList[i-2 ]+fibList[i-1 ]) } return fibList, nil }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func GetFibonacci1 (str string ) { var ( i int err error list []int ) if i, err = strconv.Atoi(str); err != nil { fmt.Println("Error" , err) return } if list, err = GetFibonacci(i); err != nil { fmt.Println("Error" , err) return } fmt.Println(list) }
1 2 3 4 5 6 7 8 9 10 func TestGetFibonacci (t *testing.T) { if v, err := GetFibonacci(1 ); err != nil { if err == LessThanTwoError { fmt.Println("It is less." ) } t.Error(err) } else { t.Log(v) } }
error
作为函数的值返回,必须对其处理,或将返回值赋值给明确忽略。对于 defer xx.Close()
可以不用显式处理。
1 2 3 4 5 6 7 8 9 func load () error { } load() _ = load()
错误要单独判断,不与其他逻辑组合判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 v, err := foo() if err != nil || v == nil { return err } v, err := foo() if err != nil { return err } if v == nil { return errors.New("invalid value v" ) }
判断某种类型的错误:应该使用 Is
方法,可判定错误链上的所有错误是否含有特定错误。
1 2 3 4 5 6 7 8 9 if err == os.ErrNotExist { } if errors.Is(err, os.ErrorExist) { }
获取错误链上特定种类的错误:
1 2 3 4 var pathError *os.PathErrorif errors.As(err, &pathError) { }
错误描述
告诉用户他们可以做什么,而非不能做什么。
声明需求时用 must 而不是 should。比如 must be greater than 0、must match regex ‘[a-z]+’
。
当声明格式错误时用 must not。比如 must not contain
。
当声明动作时用 may not。比如 may not be specified when otherField is empty、only name may be specified
。
引用文字字符串值时在单引号中指示文字。比如 ust not contain ‘…’
。
当引用另一个字段名称时在反引号中指定该名称。比如 must be greater than request
。
指定不等时使用单词而不是符号。比如 must be less than 256、must be greater than or equal to 0
(不要用 larger than、bigger than、more than、higher than)。
指定数字范围时尽可能使用包含范围。建议 Go 1.13 以上,error
生成方式为 fmt.Errorf("module xxx: %w", err)
。
错误描述用小写字母开头,结尾不要加标点符号。
封装异常 实现 error
接口的 Error
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func f1 (arg int ) (int , error) { if arg == 42 { return -1 , errors.New("can't work with 42" ) } return arg + 3 , nil } type argError struct { arg int prob string } func (e *argError) Error () string { return fmt.Sprintf("%d - %s" , e.arg, e.prob) } func f2 (arg int ) (int , error) { if arg == 42 { return -1 , &argError{arg, "can't work with it" } } return arg + 3 , nil }
可使用第三方库 errors 。
panic panic
是不可恢复的错误,会导致程序终止。比如切片检索越界或类型断言失败等。
当 panic
被调用后,程序将终止当前函数执行,并开始回溯 goroutine 调用栈、运行任何被 defer
的函数。直到执行至栈顶、程序终止。
由于 defer
是延迟到该语句所在函数执行结束时才执行(而无论结束原因是什么),在 defer
中调用 recovery
将停止回溯过程,并返回传入 panic
的实参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func TestPanicVxExit (t *testing.T) { defer func () { if err := recover (); err != nil { fmt.Println("recovered from " , err) } }() fmt.Println("Start" ) panic (errors.New("Something wrong!" )) }
recover
要慎用:
在 Go 中每个 Goroutine 独立存在,父 Goroutine 一旦开启子 Goroutine,两者就是平等存在,互相不能干扰。
所有 Goroutine 异常需要自己管理,不存在父 Goroutine 捕获子 Goroutine 异常的操作。每个 Goroutine 创建时可使用 defer 和 recover 关键字为其捕获 panic 异常并进行处理,否则任意一个 Goroutine 发生 panic 都会导致整个进程崩溃!
使用建议:
在业务逻辑处理中禁止使用 panic
。
在 main
包中,只有当程序完全不可运行时使用 panic
,例如无法打开文件、无法连接数据库导致程序无法正常运行。
在 main
包中,使用 log.Fatal
来记录错误,由 log 来结束程序,或者将 panic
抛出的异常记录到日志文件中,方便排查问题。
可导出的接口一定不能有 panic
。
包内建议采用 error
而不是 panic
来传递错误。
进程 调用 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 26 27 28 29 30 31 dateCmd := exec.Command("date" ) dateOut, err := dateCmd.Output() if err != nil { panic (err) } fmt.Println("> date" ) fmt.Println(string (dateOut)) grepCmd := exec.Command("grep" , "hello" ) grepIn, _ := grepCmd.StdinPipe() grepOut, _ := grepCmd.StdoutPipe() grepCmd.Start() grepIn.Write([]byte ("hello grep\ngoodbye grep" )) grepIn.Close() grepBytes, _ := ioutil.ReadAll(grepOut) grepCmd.Wait() fmt.Println("> grep hello" ) fmt.Println(string (grepBytes)) lsCmd := exec.Command("bash" , "-c" , "ls -a -l -h" ) lsOut, err := lsCmd.Output() if err != nil { panic (err) } fmt.Println("> ls -a -l -h" ) fmt.Println(string (lsOut))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 binary, lookErr := exec.LookPath("ls" ) if lookErr != nil { panic (lookErr) } args := []string {"ls" , "-a" , "-l" , "-h" } env := os.Environ() execErr := syscall.Exec(binary, args, env) if execErr != nil { panic (execErr) }
信号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 sigs := make (chan os.Signal, 1 ) done := make (chan bool , 1 ) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func () { sig := <-sigs fmt.Println() fmt.Println(sig) done <- true }() fmt.Println("awaiting signal" ) <-done fmt.Println("exiting" )
协程 协程是由 Go 管理的轻量级线程(stack 初始化为 2K,远远小于 Java Thread Stack 的 1M)。
go f()()
启动一个运行 f
函数的 goroutine。
1 2 3 4 5 6 7 8 9 10 11 12 func doStuff (s string ) {} func main () { go doStuff("foobar" ) go func (x int ) { }(42 ) }
状态协程 即数据状态由单个协程拥有,保证数据不会因并发访问而破坏。
有时要解决同步问题,基于状态协程的方法比基于互斥锁的方法复杂。
在某些情况下可能很有用,例如当涉及其他信道时,或者管理多个此类互斥体时容易出错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 type readOp struct { key int resp chan int } type writeOp struct { key int val int resp chan bool } func TestStatefulGoroutines (t *t.Testing) { var readOps uint64 var writeOps uint64 reads := make (chan readOp) writes := make (chan writeOp) go func () { var state = make (map [int ]int ) for { select { case read := <-reads: read.resp <- state[read.key] case write := <-writes: state[write.key] = write.val write.resp <- true } } }() for r := 0 ; r < 100 ; r++ { go func () { for { read := readOp{key: rand.Intn(5 ), resp: make (chan int )} reads <- read <-read.resp atomic.AddUint64(&readOps, 1 ) time.Sleep(time.Millisecond) } }() } for w := 0 ; w < 10 ; w++ { go func () { for { write := writeOp{key: rand.Intn(5 ), val: rand.Intn(100 ), resp: make (chan bool )} writes <- write <-write.resp atomic.AddUint64(&writeOps, 1 ) time.Sleep(time.Millisecond) } }() } time.Sleep(time.Second) readOpsFinal := atomic.LoadUint64(&readOps) fmt.Println("readOps:" , readOpsFinal) writeOpsFinal := atomic.LoadUint64(&writeOps) fmt.Println("writeOps:" , writeOpsFinal) }
信道 Channel 用于多个 goroutines 之间的通信,每个发往 Channel 的元素都会被复制一份、再放入 Channel 中;而从 Channel 中读取元素,也是从中复制出一份,再把该元素从 Channel 中删除(不会重复读取)。
如使用 Buffer Channel,则 Channel 中会缓存发送者数条消息(不超过容量),接收者随时都可以从中获取。
阻塞条件:信道缓冲区为空,或读取时无数据/写入时满数据,或信道为 nil。
1 2 3 4 5 6 7 8 ch := make (chan int ) ch <- 42 v := <-ch ch := make (chan int , 100 )
关闭写入:
1 2 3 4 5 6 7 8 9 10 11 12 close (ch) v, ok := <-ch for i := range ch { fmt.Println(i) }
不要通过共享内存来通信,而应通过通信来共享内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func Serve (queue chan *Request) { for req := range queue { sem <- 1 go func () { process(req) <-sem }() } } func Serve (queue chan *Request) { for req := range queue { sem <- 1 go func (req *Request) { process(req) <-sem }(req) } }
互斥锁 使用缓冲区长度为 1 的 channel,写入数据相当于加锁,读取数据相当于解锁。
1 2 3 4 5 6 7 8 var counter int = 0 var ch = make (chan int , 1 )func Worker () { ch <- 1 counter++ <-ch }
单向信道 只能接收或发送,由其类型字面量决定。通常用于约束函数的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var ch = make (chan <- int , 1 )type Notifier interface { SendInt(ch chan <- int ) } func SendInt (ch chan <- int ) { ch <- rand.Intn(1000 ) } func RecvInt (ch <-chan int ) { <-ch } intChan1 := make (chan int , 3 ) SendInt(intChan1)
在函数声明的返回值列表中使用单向信道:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func getIntChan () <-chan int { num := 5 ch := make (chan int , num) for i := 0 ; i < num; i++ { ch <- i } close (ch) return ch } intChan2 := getIntChan() for elem := range intChan2 { fmt.Printf("The element in intChan2: %v\n" , elem) }
异步任务 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 func service () string { time.Sleep(time.Millisecond * 50 ) return "Done" } func otherTask () { fmt.Println("working on something else" ) time.Sleep(time.Millisecond * 100 ) fmt.Println("Task is done." ) } func AsyncService () chan string { retCh := make (chan string , 1 ) go func () { ret := service() fmt.Println("returned result." ) retCh <- ret fmt.Println("service exited." ) }() return retCh } func TestService (t *testing.T) { fmt.Println(service()) otherTask() } func TestAsyncService (t *testing.T) { retCh := AsyncService() otherTask() fmt.Println(<-retCh) time.Sleep(time.Second * 1 ) }
多信道选择 只要任何一个信道消息就绪,即执行对应的 case。如果所有信道都被阻塞,则会走 default
分支。
1 2 3 4 5 6 7 8 select { case ret := <-retCh1: t.Logf("result %s" , ret) case ret :=<-retCh2: t.Logf("result %s" , ret) default : t.Error("No one returned" ) }
超时控制:
1 2 3 4 5 6 7 8 select { case ret := <-retCh: t.Logf("result %s" , ret) case <-time.After(time.Second * 1 ): t.Error("time out" ) }
非阻塞:利用 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 messages := make (chan string ) signals := make (chan bool ) select { case msg := <-messages: fmt.Println("received message" , msg) default : fmt.Println("no message received" ) } msg := "hi" select { case messages <- msg: fmt.Println("sent message" , msg) default : fmt.Println("no message sent" ) } select { case msg := <-messages: fmt.Println("received message" , msg) case sig := <-signals: fmt.Println("received signal" , sig) default : fmt.Println("no activity" ) }
基本规则:
对于每个 case
,都至少会包含一个发送表达式或一个接收表达式,同时也可能会包含其他的表达式。
候选分支中的 case
都会在该语句执行开始时先被求值,顺序是依从代码编写的顺序从上到下。
对于每个 case
,如其中的发送表达式或接收表达式在被求值时,相应的操作正阻塞,则对该 case
的求值失败。
仅当所有 case
表达式都被求值完毕后,才会开始选择满足条件的候选分支。如果所有条件都不满足,则选择 default
分支。default
分支不存在,则 select
进入阻塞、直到至少一个分支满足选择条件。
如果发现同时有多个候选分支满足条件,会用一种伪随机算法从中选择一个执行。即使 select
语句是在被唤醒时发现的这种情况也会这样做。
select
语句每次执行(比如在循环中),包括 case
表达式求值和分支选择都是独立的。
定时任务 任务延迟执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 timer1 := time.NewTimer(2 * time.Second) <-timer1.C fmt.Println("Timer 1 fired" ) timer2 := time.NewTimer(time.Second) go func () { <-timer2.C fmt.Println("Timer 2 fired" ) }() stop2 := timer2.Stop() if stop2 { fmt.Println("Timer 2 stopped" ) } time.Sleep(2 * time.Second)
周期任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ticker := time.NewTicker(500 * time.Millisecond) done := make (chan bool ) go func () { for { select { case <-done: return case t := <-ticker.C: fmt.Println("Tick at" , t) } } }() time.Sleep(1600 * time.Millisecond) ticker.Stop() done <- true fmt.Println("Ticker stopped" )
任务取消 通过关闭 channel 取消:
1 2 3 func cancel_2 (cancelChan chan struct {}) { close (cancelChan) }
发送取消消息:
1 2 3 func cancel_1 (cancelChan chan struct {}) { cancelChan <- struct {}{} }
获取取消通知:
1 2 3 4 5 6 7 8 func isCancelled (cancelChan chan struct {}) bool { select { case <-cancelChan: return true default : return false } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func TestCancel (t *testing.T) { cancelChan := make (chan struct {}, 0 ) for i := 0 ; i < 5 ; i++ { go func (i int , cancelCh chan struct {}) { for { if isCancelled(cancelCh) { break } time.Sleep(time.Millisecond * 5 ) } fmt.Println(i, "Cancelled" ) }(i, cancelChan) } cancel_2(cancelChan) time.Sleep(time.Second * 1 ) }
Channel 规则 发送数据到 nil
channel 会一直阻塞:
1 2 3 var c chan string c <- "Hello, World!"
从 nil
channel 读取数据会一直阻塞:
1 2 3 var c chan string fmt.Println(<-c)
发送数据到已关闭的 channel 发生 panic
:
1 2 3 4 5 var c = make (chan string , 1 )c <- "Hello, World!" close (c)c <- "Hello, Panic!"
在关闭的 channel 上接收会立即返回零值:
1 2 3 4 5 6 7 8 var c = make (chan int , 2 )c <- 1 c <- 2 close (c)for i := 0 ; i < 3 ; i++ { fmt.Printf("%d " , <-c) }
在 channel 关闭时,所有的接收者都会立即从阻塞等待中返回,且 v, ok <-ch
的 ok 值为 false
。
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 func dataProducer (ch chan int , wg *sync.WaitGroup) { go func () { for i := 0 ; i < 10 ; i++ { ch <- i } close (ch) wg.Done() }() } func dataReceiver (ch chan int , wg *sync.WaitGroup) { go func () { for { if data, ok := <-ch; ok { fmt.Println(data) } else { break } } wg.Done() }() } func TestCloseChannel (t *testing.T) { var wg sync.WaitGroup ch := make (chan int ) wg.Add(1 ) dataProducer(ch, &wg) wg.Add(1 ) dataReceiver(ch, &wg) wg.Wait() }
这个⼴播机制常被利⽤,进⾏向多个订阅者同时发送信号,如退出信号。
关于上下文、互斥锁、等待组、原子操作等并发工具,可参考 Go 并发原语 。
并发执行模式 顺序执行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var count uint32 trigger := func (i uint32 , fn func () ) { for { if n := atomic.LoadUint32(&count); n == i { fn() atomic.AddUint32(&count, 1 ) break } time.Sleep(time.Nanosecond) } } for i := uint32 (0 ); i < 10 ; i++ { go func (i uint32 ) { fn := func () { fmt.Println(i) } trigger(i, fn) }(i) } trigger(10 , func () {})
单例模式(懒汉式) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type Singleton struct { data string } var singleInstance *Singletonvar once sync.Oncefunc GetSingletonObj () *Singleton { once.Do(func () { fmt.Println("Create Obj" ) singleInstance = new (Singleton) }) return singleInstance } func TestGetSingletonObj (t *testing.T) { var wg sync.WaitGroup for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func () { obj := GetSingletonObj() fmt.Printf("%X\n" , unsafe.Pointer(obj)) wg.Done() }() } wg.Wait() }
任意任务完成 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 func runTask (id int ) string { time.Sleep(10 * time.Millisecond) return fmt.Sprintf("The result is from %d" , id) } func FirstResponse () string { numOfRunner := 10 ch := make (chan string , numOfRunner) for i := 0 ; i < numOfRunner; i++ { go func (i int ) { ret := runTask(i) ch <- ret }(i) } return <-ch } func TestFirstResponse (t *testing.T) { t.Log("Before:" , runtime.NumGoroutine()) t.Log(FirstResponse()) time.Sleep(time.Second * 1 ) t.Log("After:" , runtime.NumGoroutine()) }
所有任务完成 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 func runTask (id int ) string { time.Sleep(10 * time.Millisecond) return fmt.Sprintf("The result is from %d" , id) } func FirstResponse () string { numOfRunner := 10 ch := make (chan string , numOfRunner) for i := 0 ; i < numOfRunner; i++ { go func (i int ) { ret := runTask(i) ch <- ret }(i) } return <-ch } func AllResponse () string { numOfRunner := 10 ch := make (chan string , numOfRunner) for i := 0 ; i < numOfRunner; i++ { go func (i int ) { ret := runTask(i) ch <- ret }(i) } finalRet := "" for j := 0 ; j < numOfRunner; j++ { finalRet += <-ch + "\n" } return finalRet } func TestFirstResponse (t *testing.T) { t.Log("Before:" , runtime.NumGoroutine()) t.Log(AllResponse()) time.Sleep(time.Second * 1 ) t.Log("After:" , runtime.NumGoroutine()) }
对象池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 type ReusableObj struct {} type ObjPool struct { bufChan chan *ReusableObj } func NewObjPool (numOfObj int ) *ObjPool { objPool := ObjPool{} objPool.bufChan = make (chan *ReusableObj, numOfObj) for i := 0 ; i < numOfObj; i++ { objPool.bufChan <- &ReusableObj{} } return &objPool } func (p *ObjPool) GetObj (timeout time.Duration) (*ReusableObj, error) { select { case ret := <-p.bufChan: return ret, nil case <-time.After(timeout): return nil , errors.New("time out" ) } } func (p *ObjPool) ReleaseObj (obj *ReusableObj) error { select { case p.bufChan <- obj: return nil default : return errors.New("overflow" ) } } func TestObjPool (t *testing.T) { pool := NewObjPool(10 ) for i := 0 ; i < 11 ; i++ { if v, err := pool.GetObj(time.Second * 1 ); err != nil { t.Error(err) } else { fmt.Printf("%T\n" , v) if err := pool.ReleaseObj(v); err != nil { t.Error(err) } } } fmt.Println("Done" ) }
对象缓存(sync.Pool) 适用于通过复用降低复杂对象创建和 GC 代价。
协程安全,但会有锁的开销,因此需要权衡维护对象的开销与锁的开销。
取用:
尝试从私有对象获取,不存在则尝试从当前 Processor 的共享池获取。
如果当前 Processor 共享池也为空,则尝试从其他 Processor 的共享池获取。
如果所有⼦池都是空的,则由⽤户指定的 New 函数产⽣⼀个新的对象返回。
放回:如果私有对象不存在则保存为私有对象,否则放⼊当前 Processor ⼦池的共享池中。
Go GC 会清除 sync.pool
缓存的对象,因此对象的缓存有效期为下⼀次 GC 之前。由于生命周期受 GC 影响,不适用于连接池等,需要自己管理生命周期的资源池化。
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 func TestSyncPool (t *testing.T) { pool := &sync.Pool{ New: func () interface {} { fmt.Println("Create a new object." ) return 100 }, } v := pool.Get().(int ) fmt.Println(v) pool.Put(3 ) runtime.GC() v1, _ := pool.Get().(int ) fmt.Println(v1) } func TestSyncPoolInMultiGroutine (t *testing.T) { pool := &sync.Pool{ New: func () interface {} { fmt.Println("Create a new object." ) return 10 }, } pool.Put(100 ) pool.Put(100 ) pool.Put(100 ) var wg sync.WaitGroup for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func (id int ) { fmt.Println(pool.Get()) wg.Done() }(i) } wg.Wait() }
限流器 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 func TestRateLimit (t *testing.T) { requests := make (chan int , 5 ) for i := 1 ; i <= 5 ; i++ { requests <- i } close (requests) limiter := time.Tick(200 * time.Millisecond) for req := range requests { <-limiter fmt.Println("request" , req, time.Now()) } burstyRequests := make (chan int , 5 ) for i := 1 ; i <= 5 ; i++ { burstyRequests <- i } close (burstyRequests) burstyLimiter := make (chan time.Time, 3 ) for i := 0 ; i < 3 ; i++ { burstyLimiter <- time.Now() } go func () { for t := range time.Tick(200 * time.Millisecond) { burstyLimiter <- t } }() for req := range burstyRequests { <-burstyLimiter fmt.Println("request" , req, time.Now()) } }
反射
reflect.TypeOf
返回类型 reflct.Type
reflect.ValueOf
返回值 reflect.Value
(可以从中获取类型)
类型分支 类型 switch
的 case
指定变量的类型,这些值与给定接口值持有的值的类型比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func do (i interface {}) { switch v := i.(type ) { case int : fmt.Printf("Twice %v is %v\n" , v, v*2 ) case string : fmt.Printf("%q is %v bytes long\n" , v, len (v)) default : fmt.Printf("I don't know about type %T!\n" , v) } } func main () { do(21 ) do("hello" ) do(true ) }
深度比较 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func TestDeepEqual (t *testing.T) { a := map [int ]string {1 : "one" , 2 : "two" , 3 : "three" } b := map [int ]string {1 : "one" , 2 : "two" , 3 : "three" } t.Log("a==b?" , reflect.DeepEqual(a, b)) s1 := []int {1 , 2 , 3 } s2 := []int {1 , 2 , 3 } s3 := []int {2 , 3 , 1 } t.Log("s1 == s2?" , reflect.DeepEqual(s1, s2)) t.Log("s1 == s3?" , reflect.DeepEqual(s1, s3)) c1 := Customer{"1" , "Mike" , 40 } c2 := Customer{"1" , "Mike" , 40 } fmt.Println(c1 == c2) fmt.Println(reflect.DeepEqual(c1, c2)) }
kind 判断类型 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 func TestBasicType (t *testing.T) { var f float64 = 12 t := reflect.TypeOf(&f) switch t.Kind() { case reflect.Float32, reflect.Float64: fmt.Println("Float" ) case reflect.Int, reflect.Int32, reflect.Int64: fmt.Println("Integer" ) default : fmt.Println("Unknown" , t) } }
根据名称调用 1 2 3 4 5 6 7 8 9 10 11 12 13 func TestInvokeByName (t *testing.T) { e := &Employee{"1" , "Mike" , 30 } t.Logf("Name: value(%[1]v), Type(%[1]T) " , reflect.ValueOf(*e).FieldByName("Name" )) if nameField, ok := reflect.TypeOf(*e).FieldByName("Name" ); !ok { t.Error("Failed to get 'Name' field." ) } else { t.Log("Tag:format" , nameField.Tag.Get("format" )) } reflect.ValueOf(e).MethodByName("UpdateAge" ).Call([]reflect.Value{reflect.ValueOf(1 )}) t.Log("Updated Age:" , e) }
Struct Tag 1 2 3 4 5 6 7 8 9 10 type BasicInfo struct { Name string `json:"name"` Age int `json:"age"` } if nameField, ok := reflect.TypeOf(*e).FieldByName("Name" ); !ok { t.Error("Failed to get 'Name' field." ) } else { t.Log("Tag:format" , nameField.Tag.Get("format" )) }
万能设值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Employee struct { EmployeeID string Name string `format:"normal"` Age int } func (e *Employee) UpdateAge (newVal int ) { e.Age = newVal } type Customer struct { CookieID string Name string Age int }
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 func fillBySettings (st interface {}, settings map [string ]interface {}) error { if reflect.TypeOf(st).Kind() != reflect.Ptr { return errors.New("the first param should be a pointer to the struct type." ) } if reflect.TypeOf(st).Elem().Kind() != reflect.Struct { return errors.New("the first param should be a pointer to the struct type." ) } if settings == nil { return errors.New("settings is nil." ) } var ( field reflect.StructField ok bool ) for k, v := range settings { if field, ok = (reflect.ValueOf(st)).Elem().Type().FieldByName(k); !ok { continue } if field.Type == reflect.TypeOf(v) { vstr := reflect.ValueOf(st) vstr = vstr.Elem() vstr.FieldByName(k).Set(reflect.ValueOf(v)) } } return nil }
1 2 3 4 5 6 7 8 9 10 11 12 13 func TestFillNameAndAge (t *testing.T) { settings := map [string ]interface {}{"Name" : "Mike" , "Age" : 30 } e := Employee{} if err := fillBySettings(&e, settings); err != nil { t.Fatal(err) } t.Log(e) c := new (Customer) if err := fillBySettings(c, settings); err != nil { t.Fatal(err) } t.Log(*c) }
Unsafe 类型转换 Go 本身不支持强制转换,利用 Unsafe
可实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Customer struct { Name string Age int } func TestUnsafe (t *testing.T) { i := 10 f := *(*float64 )(unsafe.Pointer(&i)) t.Log(unsafe.Pointer(&i)) t.Log(f) } type MyInt int func TestConvert (t *testing.T) { a := []int {1 , 2 , 3 , 4 } b := *(*[]MyInt)(unsafe.Pointer(&a)) t.Log(b) }
原子操作 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 func TestAtomic (t *testing.T) { var shareBufPtr unsafe.Pointer writeDataFn := func () { data := []int {} for i := 0 ; i < 100 ; i++ { data = append (data, i) } atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data)) } readDataFn := func () { data := atomic.LoadPointer(&shareBufPtr) fmt.Println(data, *(*[]int )(data)) } var wg sync.WaitGroup writeDataFn() for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func () { for i := 0 ; i < 10 ; i++ { writeDataFn() time.Sleep(time.Microsecond * 100 ) } wg.Done() }() wg.Add(1 ) go func () { for i := 0 ; i < 10 ; i++ { readDataFn() time.Sleep(time.Microsecond * 100 ) } wg.Done() }() } wg.Wait() }
包 package 是基本复用模块单元,在每个源文件顶部声明。
可执行文件放在 main
包中。
代码的 package 可以和所在的目录不一致,但同一目录中代码的 package 要保持一致。
包名 == 导入路径的最后一层(import math/rand
=> package rand
),导入时从 src 后开始。
大写标识符:导出(从其他包中可见);小写标识符:私有(在其他包中不可见)。
go get [-u] github.com/stretchr/testify/assert
可(强制)从远程获取/更新依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 import ( "testing" cm "github.com/easierway/concurrent_map" ) func TestConcurrentMap (t *testing.T) { m := cm.CreateConcurrentMap(99 ) m.Set(cm.StrKey("key" ), 10 ) t.Log(m.Get(cm.StrKey("key" ))) }
如果要把自己的代码提交到 GitHub,应注意适应 go get
:直接以代码路径开始,不要有 src
。
参考:https://github.com/easierway/concurrent_map
init 函数
1 2 3 4 5 6 7 8 9 10 11 package seriesfunc init () { fmt.Println("init1" ) } func init () { fmt.Println("init2" ) }
init
函数不允许被手动显式调用。
重置包级变量值 参考 flag
包:其定义了一个导出的包级变量 CommandLine
,如果用户没有通过 flag.NewFlagSet
创建新的代表命令行标志集合的实例,CommandLine
作为 flag 包各种导出函数背后,默认的代表命令行标志集合的实例。在 flag 包初始化时,init 函数初始化次序在包级变量之后,包级变量 CommandLine
会在 init 函数之前被初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var CommandLine = NewFlagSet(os.Args[0 ], ExitOnError)func NewFlagSet (name string , errorHandling ErrorHandling) *FlagSet { f := &FlagSet{ name: name, errorHandling: errorHandling, } f.Usage = f.defaultUsage return f } func (f *FlagSet) defaultUsage () { if f.name == "" { fmt.Fprintf(f.Output(), "Usage:\n" ) } else { fmt.Fprintf(f.Output(), "Usage of %s:\n" , f.name) } f.PrintDefaults() }
flag 包在 init 函数中重置了 CommandLine
的 Usage
字段:
1 2 3 4 5 6 7 8 9 10 11 12 func init () { CommandLine.Usage = commandLineUsage } func commandLineUsage () { Usage() } var Usage = func () { fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n" , os.Args[0 ]) PrintDefaults() }
CommandLine
的 Usage
字段设置为了一个 flag 包内的未导出函数 commandLineUsage
,后者直接使用了 flag 包的另外一个导出包变量 Usage
。通过 init 函数将 CommandLine
与包变量 Usage
关联在一起。
当用户将自定义的 usage
赋值给了 flag.Usage
后,改变了默认代表命令行标志集合的 CommandLine
变量的 Usage
。
当 flag 包完成初始化后,CommandLine
变量便处于一个合理可用的状态了。
实现对包级变量的复杂初始化 有些包级变量需要比较复杂的初始化过程,使用类型零值或通过简单初始化表达式不能满足业务逻辑要求,init
函数非常适合完成该项工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var ( http2VerboseLogs bool http2logFrameWrites bool http2logFrameReads bool http2inTests bool ) func init () { e := os.Getenv("GODEBUG" ) if strings.Contains(e, "http2debug=1" ) { http2VerboseLogs = true } if strings.Contains(e, "http2debug=2" ) { http2VerboseLogs = true http2logFrameWrites = true http2logFrameReads = true } }
其中定义了一系列布尔类型的特性开关变量,它们默认处于关闭状态(即值为 false
),可以通过 GODEBUG
环境变量开启相关特性开关。
但将这些变量初始化为类型零值不能满足要求了,所以 http
包在 init 函数中根据环境变量 GODEBUG
的值对包级开关变量进行了复杂的初始化,保证了在 http
包完成初始化后,这些遍历处于合理状态。
实现注册模式 使用 lib/pq
包访问 PostgreSQL 数据库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import ( "database/sql" _ "github.com/lib/pq" ) func main () { db, err := sql.Open("postgres" , "user=pqgotest dbname=pqgotest sslmode=verify-full" ) if err != nil { log.Fatal(err) } age := 21 rows, err := db.Query("SELECT name FROM users WHERE age = $1" , age) ... }
示例代码以空导入方式导入 lib/pq
包,main 函数中没有使用 pq 包的任何变量、函数或方法,即可实现对 PostgreSQL 数据库的访问。
见 init
函数:
1 2 3 func init () { sql.Register("postgres" , &Driver{}) }
lib/pq
包作为 main 包的依赖包,init
函数会在 pq 包初始化时执行:pq
包将自己实现的 sql
驱动注册到了 sql
包中;通过 sql.Open
函数,返回的数据库实例句柄对数据库进行的操作,实际上是调用 pq
包中相应的驱动实现。
通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动。
包管理 Go 未解决依赖管理问题:
同⼀环境下,不同项⽬无法使⽤同⼀包的不同版本。
⽆法管理对包的特定版本的依赖。
Vendor 路径:Go 1.5 release 后,vendor ⽬录被添加到除了 GOPATH 和 GOROOT 之外的依赖⽬录查找的解决⽅案(在 Go 1.6 前需要⼿动设置环境变量)。查找依赖包路径的解决⽅案:
第三方依赖管理工具:
1 2 3 4 5 mkdir module_package && cd !$ glide init vim glide.yaml # ... glide install
Go Module Go 1.16+ 默认使用 Go Module 构建模式。
Go Module 是 Go 包的集合,并附带版本属性:module 与其下的包会组成独立的版本单元。
每个代码仓库对应一个 Go Module,顶层以一个 go.mod 文件唯一标识。
使用 Go Module:
模式切换 在 Go 1.11 时,GOPATH
与 Go Modules 两种构建模式各自独立工作,通过设置环境变量 GO111MODULE
在两种构建模式间切换。直到 Go 1.16,Go Module 成为默认模式。
版本机制
主版本号不同的两个版本相互不兼容。在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号也不影响兼容性。
已知同一个包的新旧版本是兼容的,它们的包导入路径相同,否则不相同。
引入互不兼容的同一个包的不同版本:
1 2 3 4 import ( "github.com/sirupsen/logrus" logv2 "github.com/sirupsen/logrus/v2" )
最小选择原则:在该项目依赖项的所有版本中,选出符合项目整体要求的最小版本。
引入依赖 一个 module 项目(go mod tidy
):
1 2 3 4 5 module github.com/yipwinghong/module-test go 1.16 require github.com/sirupsen/logrus v1.8 .1
添加 uuid
包时,在代码中导入:
1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "github.com/google/uuid" "github.com/sirupsen/logrus" ) func main () { logrus.Println("hello, go module mode" ) logrus.Println(uuid.NewString()) }
拉取 uuid
包:将新增的依赖包下载到了本地 module 缓存,并在 go.mod 文件的 require
段中新增一行内容。
1 go get github.com/google/uuid
1 2 3 4 require ( github.com/google/uuid v1.3 .0 github.com/sirupsen/logrus v1.8 .1 )
也可以在执行构建前自动分析源码中的依赖变化,识别新增依赖项并下载(在管理大量依赖时显然更高效):
升 / 降级依赖的版本 查询包的版本:
1 2 3 go list -m -versions github.com/sirupsen/logrus # github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1
比如要降级到 v1.7.0:
1 2 3 go mod edit -require=github.com/sirupsen/logrus@v1.7.0 go mod tidy # go: downloading github.com/sirupsen/logrus v1.7.0
添加主版本号大于 1 的包 主版本号为 0 表示处于开发阶段,与主版本号 v1 做同等对待,采用不带主版本号的包导入路径:
1 2 3 4 import github.com/user/repo/v0import github.com/user/repo/v1
对于主版本号大于 1 的依赖,需要在声明的导入路径上加上版本号信息。
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( _ "github.com/go-redis/redis/v7" "github.com/google/uuid" "github.com/sirupsen/logrus" ) func main () { logrus.Println("hello, go module mode" ) logrus.Println(uuid.NewString()) }
升级到不兼容版本 按照语义导入版本的原则,不同主版本的包的导入路径是不同的。因此直接修改版本号、使用 go get
下载即可:
1 2 3 4 5 import ( _ "github.com/go-redis/redis/v8" "github.com/google/uuid" "github.com/sirupsen/logrus" )
1 go get github.com/go-redis/redis/v8
移除依赖 除了删除使用到该依赖的代码(包括 import)以外、还需要执行 go mod tidy
分析源码依赖,将不再使用的依赖从 go.mod 和 go.sum 中移除。
结合 Vendor 使用 在 Go Module 构建模式下,无需手动维护 vendor 目录下的依赖包,Go 提供可以快速建立和更新 vendor 的命令:
1 2 3 4 5 6 7 8 9 10 go mod vendor tree -LF 2 vendor # vendor # ├── github.com/ # │ ├── google/ # │ ├── magefile/ # │ └── sirupsen/ # ├── golang.org/ # │ └── x/ # └── modules.txt
在 vendor 目录下创建一份该项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。
如果要基于 vendor 而不是基于本地缓存的 Go Module 构建,需要在 go build 后面加上 -mod=vendor 参数。
在 Go 1.14+,如果 Go 项目的顶层目录下存在 vendor 目录,go build 默认优先基于 vendor 构建,除非执行 go build -mod=mod
。
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const ( Monday = 1 + iota Tuesday Wednesday ) const ( Readable = 1 << iota Writable Executable ) func TestConstantTry (t *testing.T) { t.Log(Monday, Tuesday) } func TestConstantTry1 (t *testing.T) { a := 1 t.Log(a&Readable == Readable, a&Writable == Writable, a&Executable == Executable) }
测试:go test test.go
单元测试 Go 内置单元测试框架:
Fail, Error: 该测试最终失败,但后续代码继续执行,且其他测试继续执行。
FailNow, Fatal: 该测试失败且马上中止,其他测试继续执行。
测试覆盖率:go test -v -cover
要使用断言可引入第三方包 testfy 。
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 import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func square (op int ) int { return op * op } func TestSquare (t *testing.T) { inputs := [...]int {1 , 2 , 3 } expected := [...]int {1 , 4 , 9 } for i := 0 ; i < len (inputs); i++ { ret := square(inputs[i]) if ret != expected[i] { t.Errorf("input is %d, the expected is %d, the actual %d" , inputs[i], expected[i], ret) } } } func TestErrorInCode (t *testing.T) { fmt.Println("Start" ) t.Error("Error" ) fmt.Println("End" ) } func TestFailInCode (t *testing.T) { fmt.Println("Start" ) t.Fatal("Error" ) fmt.Println("End" ) } func TestSquareWithAssert (t *testing.T) { inputs := [...]int {1 , 2 , 3 } expected := [...]int {1 , 4 , 9 } for i := 0 ; i < len (inputs); i++ { ret := square(inputs[i]) assert.Equal(t, expected[i], ret) } }
使用建议:
单元测试文件名命名规范为 example_test.go。
每个重要的可导出函数都要编写测试用例。
因为单元测试文件内的函数都是不对外的,所以可导出的结构体、函数等可以不带注释。
如果存在 func (b *Bar) Foo
,单测函数可以为 func TestBar_Foo
。
基准测试 基准测试:go test -bench=. -benchmem
(-bench=<相关benchmark测试>
)
Windows 下使⽤ go test
命令⾏时应写为 go test -bench="."
。
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 func TestConcatStringByAdd (t *testing.T) { assert := assert.New(t) elems := []string {"1" , "2" , "3" , "4" , "5" } ret := "" for _, elem := range elems { ret += elem } assert.Equal("12345" , ret) } func TestConcatStringByBytesBuffer (t *testing.T) { assert := assert.New(t) var buf bytes.Buffer elems := []string {"1" , "2" , "3" , "4" , "5" } for _, elem := range elems { buf.WriteString(elem) } assert.Equal("12345" , buf.String()) } func BenchmarkConcatStringByAdd (b *testing.B) { elems := []string {"1" , "2" , "3" , "4" , "5" } b.ResetTimer() for i := 0 ; i < b.N; i++ { ret := "" for _, elem := range elems { ret += elem } } b.StopTimer() } func BenchmarkConcatStringByBytesBuffer (b *testing.B) { elems := []string {"1" , "2" , "3" , "4" , "5" } b.ResetTimer() for i := 0 ; i < b.N; i++ { var buf bytes.Buffer for _, elem := range elems { buf.WriteString(elem) } } b.StopTimer() }
性能分析 第三方工具 可下载并安装以下工具:
通过⽂件⽅式输出 Profile:
通过 HTTP ⽅式输出 Profile:
简单,适合于持续性运⾏的应⽤:在应⽤程序中导⼊ import _ "net/http/pprof"
,并启动 http server 即可(http://<host>:<port>/debug/pprof/)。
go tool pprof http://<host>:<port>/debug/pprof/profile?seconds=10
go-torch -seconds 10 http://<host>:<port>/debug/pprof/profifile
Go 支持的多种 Profile:https://golang.org/src/runtime/pprof/pprof.go
BDD (Behavior Driven Development) https://github.com/smartystreets/goconvey
安装:go get -u github.com/smartystreets/goconvey/convey
启动:$GOPATH/bin/goconvey
其它模块 打印文本 1 2 3 4 5 6 7 8 9 10 11 12 13 fmt.Println("Hello, 你好, नमस्ते, Привет, ᎣᏏᏲ" ) p := struct { X, Y int }{ 17 , 2 } fmt.Println( "My point:" , p, "x coord=" , p.X ) s := fmt.Sprintln( "My point:" , p, "x coord=" , p.X ) fmt.Printf("%d hex:%x bin:%b fp:%f sci:%e" ,17 ,17 ,17 ,17.0 ,17.0 ) s2 := fmt.Sprintf( "%d %f" , 17 , 17.0 ) hellomsg := ` "Hello" in Chinese is 你好 ('Ni Hao') "Hello" in Hindi is नमस्ते ('Namaste') `
时间 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 p := fmt.Println now := time.Now() p(now) then := time.Date(2009 , 11 , 17 , 20 , 34 , 58 , 651387237 , time.UTC) p(then) p(then.Year()) p(then.Month()) p(then.Day()) p(then.Hour()) p(then.Minute()) p(then.Second()) p(then.Nanosecond()) p(then.Location()) p(then.Weekday()) p(then.Before(now)) p(then.After(now)) p(then.Equal(now)) diff := now.Sub(then) p(diff) p(diff.Hours()) p(diff.Minutes()) p(diff.Seconds()) p(diff.Nanoseconds()) p(then.Add(diff)) p(then.Add(-diff))
Epoch 1 2 3 4 5 6 7 8 9 10 11 12 now := time.Now() secs := now.Unix() nanos := now.UnixNano() fmt.Println(now) millis := nanos / 1000000 fmt.Println(secs) fmt.Println(millis) fmt.Println(nanos) fmt.Println(time.Unix(secs, 0 )) fmt.Println(time.Unix(0 , nanos))
格式化时间 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 p := fmt.Println t := time.Now() p(t.Format(time.RFC3339)) t1, e := time.Parse(time.RFC3339, "2012-11-01T22:08:41+00:00" ) p(t1) p(t.Format("3:04PM" )) p(t.Format("Mon Jan _2 15:04:05 2006" )) p(t.Format("2006-01-02T15:04:05.999999-07:00" )) form := "3 04 PM" t2, e := time.Parse(form, "8 41 PM" ) p(t2) fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n" , t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) ansic := "Mon Jan _2 15:04:05 2006" _, e = time.Parse(ansic, "8:41PM" ) p(e)
随机 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 fmt.Print(rand.Intn(100 ), "," ) fmt.Print(rand.Intn(100 )) fmt.Println() fmt.Println(rand.Float64()) fmt.Print((rand.Float64()*5 )+5 , "," ) fmt.Print((rand.Float64() * 5 ) + 5 ) fmt.Println() s1 := rand.NewSource(time.Now().UnixNano()) r1 := rand.New(s1) fmt.Print(r1.Intn(100 ), "," ) fmt.Print(r1.Intn(100 )) fmt.Println() s2 := rand.NewSource(42 ) r2 := rand.New(s2) fmt.Print(r2.Intn(100 ), "," ) fmt.Print(r2.Intn(100 )) fmt.Println() s3 := rand.NewSource(42 ) r3 := rand.New(s3) fmt.Print(r3.Intn(100 ), "," ) fmt.Print(r3.Intn(100 ))
文件嵌入 Go 程序可以使用 embed
包嵌入静态文件,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "embed" "log" "net/http" ) var content embed.FSfunc main () { http.Handle("/" , http.FileServer(http.FS(content))) log.Fatal(http.ListenAndServe(":8080" , nil )) }
完整示例
HTTP Server 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func hello (w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hello\n" ) } func headers (w http.ResponseWriter, req *http.Request) { for name, headers := range req.Header { for _, h := range headers { fmt.Fprintf(w, "%v: %v\n" , name, h) } } } func main () { http.HandleFunc("/hello" , hello) http.HandleFunc("/headers" , headers) http.ListenAndServe(":8090" , nil ) }
路由规则
URL 分为两种,末尾是 /:表示⼀个⼦树,后⾯可以跟其他⼦路径; 末尾不是 /,表示⼀个叶⼦,固定的路径。
以 / 结尾的 URL 可以匹配它的任何⼦路径,⽐如 /images 会匹配 /images/cute-cat.jpg。
它采⽤最⻓匹配原则,如果有多个匹配,⼀定采⽤匹配路径最⻓的那个进⾏处理。
如果没有找到任何匹配项,会返回 404 错误。
Default Router 1 2 3 4 5 6 7 8 9 10 func (sh serverHandler) ServeHTTP (rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) }
更好的 Router HttpRouter: https://github.com/julienschmidt/httprouter
1 2 3 4 5 6 7 8 9 func Hello (w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, "hello, %s!\n" , ps.ByName("name" )) } func main () { router := httprouter.New() router.GET("/" , Index) router.GET("/hello/:name" , Hello) log.Fatal(http.ListenAndServe(":8080" , router)) }
Restful 服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package mainimport ( "encoding/json" "fmt" "log" "net/http" "github.com/julienschmidt/httprouter" ) type Employee struct { ID string `json:"id"` Name string `json:"name"` Age int `json:"age"` } var employeeDB map [string ]*Employeefunc init () { employeeDB = map [string ]*Employee{} employeeDB["Mike" ] = &Employee{"e-1" , "Mike" , 35 } employeeDB["Rose" ] = &Employee{"e-2" , "Rose" , 45 } } func Index (w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n" ) } func GetEmployeeByName (w http.ResponseWriter, r *http.Request, ps httprouter.Params) { qName := ps.ByName("name" ) var ( ok bool info *Employee infoJson []byte err error ) if info, ok = employeeDB[qName]; !ok { w.Write([]byte ("{\"error\":\"Not Found\"}" )) return } if infoJson, err = json.Marshal(info); err != nil { w.Write([]byte (fmt.Sprintf("{\"error\":,\"%s\"}" , err))) return } w.Write(infoJson) } func main () { router := httprouter.New() router.GET("/" , Index) router.GET("/employee/:name" , GetEmployeeByName) log.Fatal(http.ListenAndServe(":8080" , router)) }
HTTP Client 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 resp, err := http.Get("http://gobyexample.com" ) if err != nil { panic (err) } defer resp.Body.Close()fmt.Println("Response status:" , resp.Status) scanner := bufio.NewScanner(resp.Body) for i := 0 ; scanner.Scan() && i < 5 ; i++ { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { panic (err) }
JSON Struct Tag 解析 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 type BasicInfo struct { Name string `json:"name"` Age int `json:"age"` } type JobInfo struct { Skills []string `json:"skills"` } type Employee struct { BasicInfo BasicInfo `json:"basic_info"` JobInfo JobInfo `json:"job_info"` } var jsonStr = `{ "basic_info":{ "name":"Mike", "age":30 }, "job_info":{ "skills":["Java","Go","C"] } }` func TestEmbeddedJson (t *testing.T) { e := new (Employee) err := json.Unmarshal([]byte (jsonStr), e) if err != nil { t.Error(err) } fmt.Println(*e) if v, err := json.Marshal(e); err == nil { fmt.Println(string (v)) } else { t.Error(err) } }
第三方库 EasyJSON: go get -u github.com/mailru/easyjson/...
XML 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 type Plant struct { XMLName xml.Name `xml:"plant"` Id int `xml:"id,attr"` Name string `xml:"name"` Origin []string `xml:"origin"` } func (p Plant) String () string { return fmt.Sprintf("Plant id=%v, name=%v, origin=%v" , p.Id, p.Name, p.Origin) } func TestXML (t *t.Testing) { coffee := &Plant{Id: 27 , Name: "Coffee" } coffee.Origin = []string {"Ethiopia" , "Brazil" } out, _ := xml.MarshalIndent(coffee, " " , " " ) fmt.Println(string (out)) fmt.Println(xml.Header + string (out)) var p Plant if err := xml.Unmarshal(out, &p); err != nil { panic (err) } fmt.Println(p) tomato := &Plant{Id: 81 , Name: "Tomato" } tomato.Origin = []string {"Mexico" , "California" } type Nesting struct { XMLName xml.Name `xml:"nesting"` Plants []*Plant `xml:"parent>child>plant"` } nesting := &Nesting{} nesting.Plants = []*Plant{coffee, tomato} out, _ = xml.MarshalIndent(nesting, " " , " " ) fmt.Println(string (out)) }
编码 SHA1 1 2 3 4 5 6 7 8 9 10 11 12 13 import ( "crypto/sha1" "fmt" ) func TestSHA1 (t *t.Testing) { s := "sha1 this string" h := sha1.New() h.Write([]byte (s)) bs := h.Sum(nil ) fmt.Println(s) fmt.Printf("%x\n" , bs) }
Base64 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import ( b64 "encoding/base64" "fmt" ) func TestBase64 (t *t.Testing) { data := "abc123!?$*&()'-=@~" sEnc := b64.StdEncoding.EncodeToString([]byte (data)) fmt.Println(sEnc) sDec, _ := b64.StdEncoding.DecodeString(sEnc) fmt.Println(string (sDec)) fmt.Println() uEnc := b64.URLEncoding.EncodeToString([]byte (data)) fmt.Println(uEnc) uDec, _ := b64.URLEncoding.DecodeString(uEnc) fmt.Println(string (uDec)) }
读写文件 列出文件与目录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func main () { dir := os.Args[1 ] listAll(dir,0 ) } func listAll (path string , curHier int ) { fileInfos, err := ioutil.ReadDir(path) if err != nil {fmt.Println(err); return } for _, info := range fileInfos{ if info.IsDir(){ for tmpHier := curHier; tmpHier > 0 ; tmpHier--{ fmt.Printf("|\t" ) } fmt.Println(info.Name(),"\\" ) listAll(path + "/" + info.Name(),curHier + 1 ) }else { for tmpHier := curHier; tmpHier > 0 ; tmpHier--{ fmt.Printf("|\t" ) } fmt.Println(info.Name()) } } }
整个读取:针对普通的小文件,并且对内容没有太多操作。
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 func readAll (filePath string ) { ioutil.ReadFile(filePath) } func readAllBuff (filePath string ) { FileHandle, err := os.Open(filePath) if err != nil { log.Println(err) return } defer FileHandle.Close() fileInfo, err := FileHandle.Stat() if err != nil { log.Println(err) return } buffer := make ([]byte , fileInfo.Size()) n, err := FileHandle.Read(buffer) if err != nil { log.Println(err) } fmt.Println(string (buffer[:n])) }
分片读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func readBlock (filePath string ) { FileHandle, err := os.Open(filePath) if err != nil { log.Println(err) return } defer FileHandle.Close() buffer := make ([]byte , 1024 ) for { n, err := FileHandle.Read(buffer) if err != nil && err != io.EOF { log.Println(err) } if n == 0 { break } } }
逐行读取:
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 func readEachLineReader (filePath string ) { FileHandle, err := os.Open(filePath) if err != nil { log.Println(err) return } defer FileHandle.Close() lineReader := bufio.NewReader(FileHandle) for { line, _, err := lineReader.ReadLine() if err == io.EOF { break } fmt.Println(string (line)) } } func readEachLineScanner (filePath string ) { FileHandle, err := os.Open(filePath) if err != nil { log.Println(err) return } defer FileHandle.Close() lineScanner := bufio.NewScanner(FileHandle) for lineScanner.Scan() { fmt.Println(lineScanner.Text()) } }
写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 func TestFileHandler () { file, _ := os.OpenFile("test2.txt" , os.O_RDWR | os.O_APPEND | os.O_CREATE, 0664 ) defer file.Close() data := []byte ("hello go\n" ) count, _ := file.Write(data) printWroteBytes(count) count, _ = file.WriteString("hello world\n" ) printWroteBytes(count) file.Sync() } func TestIoutil () { data := []byte ("hello goo\n" ) ioutil.WriteFile("test.txt" , data, 0664 ) } func TestBufio () { file, _ := os.OpenFile("test.txt" , os.O_RDWR | os.O_APPEND | os.O_CREATE, 0664 ) defer file.Close() writer := bufio.NewWriter(file) count, _ := writer.Write([]byte ("hello go\n" )) fmt.Printf("wrote %d bytes\n" , count) count, _ = writer.WriteString("hello world\n" ) fmt.Printf("wrote %d bytes\n" , count) writer.Flush() }
行过滤器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() {ucl := strings.ToUpper(scanner.Text()) fmt.Println(ucl) } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "error:" , err) os.Exit(1 ) }
命令行参数 1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { argsWithProg := os.Args argsWithoutProg := os.Args[1 :] arg := os.Args[3 ] fmt.Println(argsWithProg) fmt.Println(argsWithoutProg) fmt.Println(arg) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { wordPtr := flag.String("word" , "foo" , "a string" ) numbPtr := flag.Int("numb" , 42 , "an int" ) boolPtr := flag.Bool("fork" , false , "a bool" ) var svar string flag.StringVar(&svar, "svar" , "bar" , "a string var" ) flag.Parse() fmt.Println("word:" , *wordPtr) fmt.Println("numb:" , *numbPtr) fmt.Println("fork:" , *boolPtr) fmt.Println("svar:" , svar) fmt.Println("tail:" , flag.Args()) }
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 func main () { fooCmd := flag.NewFlagSet("foo" , flag.ExitOnError) fooEnable := fooCmd.Bool("enable" , false , "enable" ) fooName := fooCmd.String("name" , "" , "name" ) barCmd := flag.NewFlagSet("bar" , flag.ExitOnError) barLevel := barCmd.Int("level" , 0 , "level" ) if len (os.Args) < 2 { fmt.Println("expected 'foo' or 'bar' subcommands" ) os.Exit(1 ) } switch os.Args[1 ] { case "foo" : fooCmd.Parse(os.Args[2 :]) fmt.Println("subcommand 'foo'" ) fmt.Println(" enable:" , *fooEnable) fmt.Println(" name:" , *fooName) fmt.Println(" tail:" , fooCmd.Args()) case "bar" : barCmd.Parse(os.Args[2 :]) fmt.Println("subcommand 'bar'" ) fmt.Println(" level:" , *barLevel) fmt.Println(" tail:" , barCmd.Args()) default : fmt.Println("expected 'foo' or 'bar' subcommands" ) os.Exit(1 ) } }
环境变量 1 2 3 4 5 6 7 8 9 10 11 12 func main () { os.Setenv("FOO" , "1" ) fmt.Println("FOO:" , os.Getenv("FOO" )) fmt.Println("BAR:" , os.Getenv("BAR" )) fmt.Println() for _, e := range os.Environ() { pair := strings.SplitN(e, "=" , 2 ) fmt.Println(pair[0 ]) } }
正则表达式 使用正则表达式必须预编译,避免每次使用时构建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 match, _ := regexp.MatchString("p([a-z]+)ch" , "peach" ) fmt.Println(match) r, _ := regexp.Compile("p([a-z]+)ch" ) fmt.Println(r.MatchString("peach" )) fmt.Println(r.FindString("peach punch" )) fmt.Println(r.FindStringIndex("peach punch" )) fmt.Println(r.FindStringSubmatch("peach punch" )) fmt.Println(r.FindStringSubmatchIndex("peach punch" )) fmt.Println(r.FindAllString("peach punch pinch" , -1 )) fmt.Println(r.FindAllStringSubmatchIndex("peach punch pinch" , -1 )) fmt.Println(r.FindAllString("peach punch pinch" , 2 )) fmt.Println(r.Match([]byte ("peach" ))) r = regexp.MustCompile("p([a-z]+)ch" ) fmt.Println(r) fmt.Println(r.ReplaceAllString("a peach" , "<fruit>" )) in := []byte ("a peach" ) out := r.ReplaceAllFunc(in, bytes.ToUpper) fmt.Println(string (out))
技巧总结
package 的名字和目录名一样,main 除外。
string
表示的是不可变的字符串变量,对 string
的修改是比较重的操作,基本上都需要重新申请内存,如果没有特殊需要,需要修改时多使用 []byte
。
尽量使用 strings
库操作 string
可以提高性能。
append
要小心自动分配内存,append
返回的可能是新分配的地址。
如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。
map 在并发中需要加锁。
编译过程无法检查 interface{}
的转换,只有运行时检查,小心引起 panic
。
使用 defer
,保证退出函数时释放资源。
尽量少用全局变量,通过参数传递,使每个函数都是“无状态”的,可减少耦合,也方便分工和单元测试。
参数如果比较多,将相关参数定义成结构体传递。