nav_path: golang_instant_reference
golang-cheat-sheet是目前GitHub上最流行的golang代码速查表。作者Ariel Mashraki也是Facebook著名ORM框架ent(2019年开源)的作者和首席布道师。
这一篇是中文翻译版
致谢
大多数代码示例来源于A Tour of Go,它对Go做了非常棒的介绍。
很认真地说,如果你是Go新手,一定要看完A Tour of Go。
Go特性一览
- 命令式编程语言
 - 静态类型
 - 语法类似C语言(但是和C语言相比,圆括号更少,没有分号),结构类似Oberon-2编程语言
 - 编译成机器代码(没有JVM)
 - 没有类,但是有可以带方法的结构体
 - 接口类型Interfaces
 - 没有继承,但是有类型嵌套
 - 函数是一等公民
 - 函数可以返回多个值
 - 有闭包
 - 有指针,但是没有指针运算
 - 内置并发原语:协程goroutine和管道channel
 
基础语法
Hello World
文件 hello.go:
package mainimport "fmt"func main() {fmt.Println("Hello Go")}
$ go run hello.go
操作符
算术操作符
| Operator | Description | |
|---|---|---|
+ | 
加 | |
- | 
减 | |
* | 
乘 | |
/ | 
除 | |
% | 
取余 | |
& | 
按位与 | |
| `\ | ` | 按位或 | 
^ | 
按位异或,这个也可以当做”按位取反“操作符 | |
&^ | 
bit clear (and not)操作,x&y表示把y按位取反,再和x做位与&操作,也即x&(y) | |
<< | 
按位左移 | |
>> | 
按位右移 | 
比较操作符
| Operator | Description | 
|---|---|
== | 
等于 | 
!= | 
不等于 | 
< | 
小于 | 
<= | 
小于等于 | 
> | 
大于 | 
>= | 
大于等于 | 
逻辑操作符
| Operator | Description | ||
|---|---|---|---|
&& | 
逻辑与 | ||
| `\ | \ | ` | 逻辑或 | 
! | 
逻辑非 | 
其它
| Operator | Description | 
|---|---|
& | 
取变量地址或者创建指针 | 
* | 
取指针指向的变量的值 | 
<- | 
管道channel的发送和接收操作符 | 
声明
类型在标识符后面
var foo int // 只声明,不做初始化var foo int = 42 // 声明的同时做初始化var foo, bar int = 42, 1302 // 一次声明和初始化多个变量var foo = 42 // 忽略类型,编译器自行推导foo := 42 // 简写,只能在函数或者方法体内使用,没有var关键字,变量类型也是隐式推导而来const constant = "This is a constant"// iota的值从0开始,用于常量的数值递增const (_ = iotaabc = 1 << iotad)fmt.Println(a, b) // 1 2 (0被赋值给了_,相当于被跳过了)fmt.Println(c, d) // 8 16 (2^3, 2^4)
函数
// 一个简单的函数func functionName() {}// 带参数的函数,参数的类型在标识符后面func functionName(param1 string, param2 int) {}// 多个参数有相同的类型func functionName(param1, param2 int) {}// 返回值类型声明func functionName() int {return 42}// 可以返回多个值func returnMulti() (int, string) {return 42, "foobar"}var x, str = returnMulti()// 函数返回值有标识符,可以在函数体内对返回标识符赋值func returnMulti2() (n int, s string) {n = 42s = "foobar"// 只需要return即可,n和s的值会被返回return}var x, str = returnMulti2()
函数作为值和闭包
func main() {// 把函数赋值给变量add, add是一个函数类型变量add := func(a, b int) int {return a + b}// 使用函数变量来调用函数fmt.Println(add(3, 4))}// 闭包是匿名函数,闭包可以访问当前作用域可以访问到的变量func scope() func() int{outer_var := 2foo := func() int { return outer_var}return foo}func another_scope() func() int{// 编译失败,因为outer_var和foo没有在another_scope里定义outer_var = 444return foo}// 闭包func outer() (func() int, int) {outer_var := 2inner := func() int {outer_var += 99 // 如果执行了闭包,闭包外面的outer_var的值会被修改return outer_var}inner()return inner, outer_var // outer_var的值被改变,这里返回inner函数和101}
参数可变的函数
func main() {fmt.Println(adder(1, 2, 3)) // 6fmt.Println(adder(9, 9)) // 18nums := []int{10, 20, 30}fmt.Println(adder(nums...)) // 60}// 在最后一个参数的类型前面加...表示函数的最后一个传参可以有0个或者多个// 函数调用和普通函数一样,只是我们可以传递任意多个参数func adder(args ...int) int {total := 0for _, v := range args { // 遍历传进来的参数, args是一个slice类型变量total += v}return total}
内置类型
boolstringint int8 int16 int32 int64uint uint8 uint16 uint32 uint64 uintptrbyte // uint8的别名rune // int32的别名,主要用来表示字符类型float32 float64complex64 complex128
Go所有的内置类型都被定义在标准库的builtin这个包里。
类型转换
var i int = 42var f float64 = float64(i)var u uint = uint(f)// 下面这种语法也可以,用于局部作用域i := 42f := float64(i)u := uint(f)
包
- 在每个Go源文件的最开头(不包括注释)添加包声明
 - 可执行文件在main包里
 - 惯例:包名 == 包导入路径的最后一个名称(导入路径 
math/rand=> 包名rand) - 大写字母开头的标识符: 表示可导出(exported)标识符, 对其它包可见
 - 小写字母开头的标识符: 表示私有(private)标识符,对其它包不可见
 
控制结构
If
func main() {// 基本语法if x > 10 {return x} else if x == 10 {return 10} else {return -x}// 在if条件前面可以加一条代码语句if a := b + c; a < 42 {return a} else {return a - 42}// 在if里做类型判断var val interface{} = "foo"if str, ok := val.(string); ok {fmt.Println(str)}}
循环
// Go只有for,没有while和until关键字for i := 1; i < 10; i++ {}for ; i < 10; { // 相当while循环的效果}for i < 10 { // 如果只有一个条件,可以省略分号,也相当于while循环}for { // 可以忽略条件,相当于while (true)}// 循环里可以使用break/continue来控制循环执行逻辑// break/continue还可以和循环外的label一起使用,用于控制外层循环的执行逻辑// continue here表示外层的for循环继续执行,继续执行时外层for循环里的i会++// break there表示退出外层循环,也就是退出整个循环了here: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}}}there: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}}}
Switch
// switch语句switch operatingSystem {case "darwin":fmt.Println("Mac OS Hipster")// case分支里的代码执行完后会自动退出switch,默认没有fallthroughcase "linux":fmt.Println("Linux Geek")default:// Windows, BSD, ...fmt.Println("Other")}// 和if一样,switch的value之前可以添加一条赋值语句switch os := runtime.GOOS; os {case "darwin": ...}// switch的case条件还可以是比较语句number := 42switch {case number < 42:fmt.Println("Smaller")case number == 42:fmt.Println("Equal")case number > 42:fmt.Println("Greater")}// case分支后还可以带多个值,用逗号分隔,任意一个匹配即可var char byte = '?'switch char {case ' ', '?', '&', '=', '#', '+', '%':fmt.Println("Should escape")}
数组,切片和range迭代
数组
var a [10]int // 声明一个长度为10的int数组,数组长度也是数组类型的一部分a[3] = 42 // 设置数组元素的值i := a[3] // 读数组元素的值// 声明和初始化var a = [2]int{1, 2}a := [2]int{1, 2} //简写a := [...]int{1, 2} // 编译器自行推导数组长度
切片
var a []int // 声明切片,和数组类型声明类似,不需要指定长度var a = []int {1, 2, 3, 4} // 声明和初始化切片a := []int{1, 2, 3, 4} // 简写chars := []string{0:"a", 2:"c", 1: "b"} // ["a", "b", "c"]var b = a[lo:hi] // 通过下标索引从已有的数组或切片创建新切片,下标前闭后开,取值从lo到hi-1var b = a[1:4] // 取切片a的下标索引从1到3的值赋值给新切片bvar b = a[:3] // :前面没有值表示起始索引是0,等同于a[0:3]var b = a[3:] // :后面没有值表示结束索引是len(a),等同于a[3:len(a)]a = append(a,17,3) // 往切片里添加新元素c := append(a,b...) // 把切片a和b的值拼接起来,组成新切片// 使用make来创建切片a = make([]byte, 5, 5) // make的第2个参数是切片长度,第3个参数是切片容量a = make([]byte, 5) // 第3个切片容量参数可选,即可以不传值// 根据数组来创建切片x := [3]string{"Лайка", "Белка", "Стрелка"}s := x[:] // 切片s指向了数组x的内存空间,改变切片s的值,也会影响数组x的值
数组和切片上的操作
len(a)可以用来计算数组或切片的长度,len()是Go的内置函数,不是数组或者切片的方法
// 循环遍历数组或切片for i, e := range a {// i是下标索引,从0开始, e是具体的元素}// 如果你只需要元素,不需要下标索引,可以按照下面的方式做:for _, e := range a {// e是元素}// 如果你只需要下标索引,可以按照下面的方式做for i := range a {}// Go 1.4之前, 如果range的前面不按照上面2个示例那样带上i和e,会编译报错// Go 1.4开始,可以不用带上i和e,直接for range遍历for range time.Tick(time.Second) {// 每秒执行一次}
集合
m := make(map[string]int)m["key"] = 42fmt.Println(m["key"])delete(m, "key")elem, ok := m["key"] // 判断key是否存在:如果存在,ok就是true,elem是对应value,否则ok是false,elem是map的value的类型的零值// map字面值,声明的同时做初始化var m = map[string]Vertex{"Bell Labs": {40.68433, -74.39967},"Google": {37.42202, -122.08408},}// 遍历mapfor key, value := range m {}
结构体
Go没有class,只有结构体struct,结构体可以有自己的方法。
// 结构体是一种类型,也是一系列字段的集合// 声明type Vertex struct {X, Y float64}// 创建结构体变量var v = Vertex{1, 2}var v = Vertex{X: 1, Y: 2} // 通过字段名称:值的形式来创建结构体变量var v = []Vertex{{1,2},{5,2},{5,5}} // 初始化结构体切片// 访问结构体的字段v.X = 4// 给结构体定义方法,在func关键字和方法名称之间加上结构体声明(var_name StructName)即可// 调用方法时,会把结构体的值拷贝一份func (v Vertex) Abs() float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)}// 调用结构体方法v.Abs()// 如果想调用方法时改变外部结构体变量的值,方法需要使用指针接受者// 下面的方法,每次调用add方法时就不会拷贝结构体的值func (v *Vertex) add(n float64) {v.X += nv.Y += n}
匿名结构体:
比使用map[string]interface{}更轻量、更安全。
point := struct {X, Y int}{1, 2}
指针
p := Vertex{1, 2} // p是结构体Vertex的变量或者说实例q := &p // q是指向Vertex的指针r := &Vertex{1, 2} // r也是指向Vertex的指针// Vertex指针的类型是*Vertexvar s *Vertex = new(Vertex) // new函数创建一个指向Vertex实例的指针
接口
// 接口声明type Awesomizer interface {Awesomize() string}// 结构体不会在声明的时候指定要实现某个接口type Foo struct {}// 相反,结构体如果实现了接口里的所有方法,那就隐式表明该结构体满足了该接口// 可以通过接口变量来调用结构体方法func (foo Foo) Awesomize() string {return "Awesome!"}
接口和结构体嵌套
Go没有子类的概念,不过Go有接口嵌套和结构体嵌套。
// 接口嵌套,ReadWriter的实现一定要同时实现Reader和Writer这2个接口类型里的所有方法type ReadWriter interface {ReaderWriter}// 结构体嵌套,Server同时有了log.Logger的所有方法type Server struct {Host stringPort int*log.Logger}// 初始化嵌套结构体变量,和普通结构体初始化一样server := &Server{"localhost", 80, log.New(...)}// 被嵌套的结构体log.Logger的方法也自然成为了结构体Server的方法server.Log(...) // 相当于调用了server.Logger.Log(...)// 被嵌套的类型的字段名称是它的类型名称(在本代码示例里,被嵌套的类型*log.Logger的字段名称是它的类型名称Logger)var logger *log.Logger = server.Logger
错误处理
Go没有异常处理。函数如果可能产生错误只需要在函数返回值里额外增加一个类型为error的返回值。error接口类型的定义如下:
// error接口类型是Go内置类型,用于表示错误// 值为nil时表示没有错误type error interface {Error() string}
这里有一个示例:
func sqrt(x float64) (float64, error) {if x < 0 {return 0, errors.New("negative value")}return math.Sqrt(x), nil}func main() {val, err := sqrt(-1)if err != nil {// 处理错误fmt.Println(err) // negative valuereturn}// 没有错误,打印结果fmt.Println(val)}
并发
协程Goroutine
Goroutines是轻量级线程(由Go运行时来管理,不是操作系统线程)。 go f(a, b) 语句会开启了一个新的goroutine,这个goroutine执行f(a, b)这个函数调用。
// 这里只是定义一个函数,后面用于goroutine执行func doStuff(s string) {}func main() {// 在goroutine里使用有名称的函数go doStuff("foobar")// 在goroutine里使用匿名函数(闭包)go func (x int) {// 函数体定义}(42)}
管道Channel
ch := make(chan int) // 创建类型为int的管道ch <- 42 // 发送数据到管道chv := <-ch // 从管道ch接收数据// 没有缓冲区的管道会阻塞。// 如果没有往管道发送值,读操作会阻塞,如果没有从管道接收值,写操作会阻塞// 创建带缓冲区的管道// 如果缓冲区未满,往有缓冲区的管道发送数据不会阻塞ch := make(chan int, 100)close(ch) // 关闭管道(只有往管道发送数据的发送者才应该执行close操作)// 从管道读数据,并判断管道是否已经被关闭v, ok := <-ch// 如果ok是false就表示管道已经被关闭了// 从管道读数据,直到管道被关闭for i := range ch {fmt.Println(i)}// select关键字在多个管道操作上阻塞,只要有1个不阻塞了,对应case分支就会被执行func doStuff(channelOut, channelIn chan int) {select {case channelOut <- 42:fmt.Println("We could write to channelOut!")case x := <- channelIn:fmt.Println("We could read from channelIn")case <-time.After(time.Second * 1):fmt.Println("timeout")}}
Channel原则
- 给值为nil的管道发送数据会一直阻塞
 
var c chan stringc <- "Hello, World!"// fatal error: all goroutines are asleep - deadlock!
- 从值为nil的管道接收数据会一直阻塞
 
var c chan stringfmt.Println(<-c)// fatal error: all goroutines are asleep - deadlock!
- 往被关闭的管道发送数据会panic
 
var c = make(chan string, 1)c <- "Hello, World!"close(c)c <- "Hello, Panic!"// panic: send on closed channel
- 从被关闭的管道接收数据会立即返回零值
 
var c = make(chan int, 2)c <- 1c <- 2close(c)for i := 0; i < 3; i++ {fmt.Printf("%d ", <-c)}// 1 2 0
打印
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) // C风格的格式化打印s2 := fmt.Sprintf( "%d %f", 17, 17.0 ) // 字符串格式化hellomsg := `"Hello" in Chinese is 你好 ('Ni Hao')"Hello" in Hindi is नमस्ते ('Namaste')` // 跨越多行的字符串,使用``
反射
类型Switch
类型switch类似普通的switch语句,只是case分支的判断条件是类型,而不是具体的值。使用场景主要是用于判断接口变量的值类型。
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)}
代码片段
文件嵌入
Go程序可以使用embed包嵌入静态文件
package mainimport ("embed""fmt""io""log""net/http")// content持有服务器static目录下的所有文件//go:embed static/*var content embed.FSfunc main() {http.Handle("/", http.FileServer(http.FS(content)))go func() {log.Fatal(http.ListenAndServe(":8080", nil))}()// 读取服务器static目录下的内容entries, err := content.ReadDir("static")if err != nil {log.Fatal(err)}for _, e := range entries {resp, err := http.Get("http://localhost:8080/static/" + e.Name())if err != nil {log.Fatal(err)}body, err := io.ReadAll(resp.Body)if err != nil {log.Fatal(err)}if err := resp.Body.Close(); err != nil {log.Fatal(err)}fmt.Printf("%q: %s", e.Name(), body)}}
HTTP服务器
运行下面的代码,在浏览器访问 http://127.0.0.1:4000 会显示”hello”
package mainimport ("fmt""net/http")// 定义处理http请求的Handlertype Hello struct{}// 结构体Hello实现接口类型http.Handler里的方法ServeHTTP// 这样结构体Hello的实例就可以作为http的Handler来接收http请求,返回http响应结果func (h Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {fmt.Fprint(w, "Hello!")}func main() {var h Hellohttp.ListenAndServe("localhost:4000", h)}// 下面是http.ServerHTTP的方法签名// type Handler interface {// ServeHTTP(w http.ResponseWriter, r *http.Request)// }
