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:

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("Hello Go")
  5. }

$ go run hello.go

操作符

算术操作符

Operator Description
+
-
*
/
% 取余
& 按位与
`\ ` 按位或
^ 按位异或,这个也可以当做”按位取反“操作符
&^ bit clear (and not)操作,x&y表示把y按位取反,再和x做位与&操作,也即x&(y)
<< 按位左移
>> 按位右移

比较操作符

Operator Description
== 等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于

逻辑操作符

Operator Description
&& 逻辑与
`\ \ ` 逻辑或
! 逻辑非

其它

Operator Description
& 取变量地址或者创建指针
* 取指针指向的变量的值
<- 管道channel的发送和接收操作符

声明

类型在标识符后面

  1. var foo int // 只声明,不做初始化
  2. var foo int = 42 // 声明的同时做初始化
  3. var foo, bar int = 42, 1302 // 一次声明和初始化多个变量
  4. var foo = 42 // 忽略类型,编译器自行推导
  5. foo := 42 // 简写,只能在函数或者方法体内使用,没有var关键字,变量类型也是隐式推导而来
  6. const constant = "This is a constant"
  7. // iota的值从0开始,用于常量的数值递增
  8. const (
  9. _ = iota
  10. a
  11. b
  12. c = 1 << iota
  13. d
  14. )
  15. fmt.Println(a, b) // 1 2 (0被赋值给了_,相当于被跳过了)
  16. fmt.Println(c, d) // 8 16 (2^3, 2^4)

函数

  1. // 一个简单的函数
  2. func functionName() {}
  3. // 带参数的函数,参数的类型在标识符后面
  4. func functionName(param1 string, param2 int) {}
  5. // 多个参数有相同的类型
  6. func functionName(param1, param2 int) {}
  7. // 返回值类型声明
  8. func functionName() int {
  9. return 42
  10. }
  11. // 可以返回多个值
  12. func returnMulti() (int, string) {
  13. return 42, "foobar"
  14. }
  15. var x, str = returnMulti()
  16. // 函数返回值有标识符,可以在函数体内对返回标识符赋值
  17. func returnMulti2() (n int, s string) {
  18. n = 42
  19. s = "foobar"
  20. // 只需要return即可,n和s的值会被返回
  21. return
  22. }
  23. var x, str = returnMulti2()

函数作为值和闭包

  1. func main() {
  2. // 把函数赋值给变量add, add是一个函数类型变量
  3. add := func(a, b int) int {
  4. return a + b
  5. }
  6. // 使用函数变量来调用函数
  7. fmt.Println(add(3, 4))
  8. }
  9. // 闭包是匿名函数,闭包可以访问当前作用域可以访问到的变量
  10. func scope() func() int{
  11. outer_var := 2
  12. foo := func() int { return outer_var}
  13. return foo
  14. }
  15. func another_scope() func() int{
  16. // 编译失败,因为outer_var和foo没有在another_scope里定义
  17. outer_var = 444
  18. return foo
  19. }
  20. // 闭包
  21. func outer() (func() int, int) {
  22. outer_var := 2
  23. inner := func() int {
  24. outer_var += 99 // 如果执行了闭包,闭包外面的outer_var的值会被修改
  25. return outer_var
  26. }
  27. inner()
  28. return inner, outer_var // outer_var的值被改变,这里返回inner函数和101
  29. }

参数可变的函数

  1. func main() {
  2. fmt.Println(adder(1, 2, 3)) // 6
  3. fmt.Println(adder(9, 9)) // 18
  4. nums := []int{10, 20, 30}
  5. fmt.Println(adder(nums...)) // 60
  6. }
  7. // 在最后一个参数的类型前面加...表示函数的最后一个传参可以有0个或者多个
  8. // 函数调用和普通函数一样,只是我们可以传递任意多个参数
  9. func adder(args ...int) int {
  10. total := 0
  11. for _, v := range args { // 遍历传进来的参数, args是一个slice类型变量
  12. total += v
  13. }
  14. return total
  15. }

内置类型

  1. bool
  2. string
  3. int int8 int16 int32 int64
  4. uint uint8 uint16 uint32 uint64 uintptr
  5. byte // uint8的别名
  6. rune // int32的别名,主要用来表示字符类型
  7. float32 float64
  8. complex64 complex128

Go所有的内置类型都被定义在标准库的builtin这个包里。

类型转换

  1. var i int = 42
  2. var f float64 = float64(i)
  3. var u uint = uint(f)
  4. // 下面这种语法也可以,用于局部作用域
  5. i := 42
  6. f := float64(i)
  7. u := uint(f)

  • 在每个Go源文件的最开头(不包括注释)添加包声明
  • 可执行文件在main包里
  • 惯例:包名 == 包导入路径的最后一个名称(导入路径 math/rand => 包名 rand)
  • 大写字母开头的标识符: 表示可导出(exported)标识符, 对其它包可见
  • 小写字母开头的标识符: 表示私有(private)标识符,对其它包不可见

控制结构

If

  1. func main() {
  2. // 基本语法
  3. if x > 10 {
  4. return x
  5. } else if x == 10 {
  6. return 10
  7. } else {
  8. return -x
  9. }
  10. // 在if条件前面可以加一条代码语句
  11. if a := b + c; a < 42 {
  12. return a
  13. } else {
  14. return a - 42
  15. }
  16. // 在if里做类型判断
  17. var val interface{} = "foo"
  18. if str, ok := val.(string); ok {
  19. fmt.Println(str)
  20. }
  21. }

循环

  1. // Go只有for,没有while和until关键字
  2. for i := 1; i < 10; i++ {
  3. }
  4. for ; i < 10; { // 相当while循环的效果
  5. }
  6. for i < 10 { // 如果只有一个条件,可以省略分号,也相当于while循环
  7. }
  8. for { // 可以忽略条件,相当于while (true)
  9. }
  10. // 循环里可以使用break/continue来控制循环执行逻辑
  11. // break/continue还可以和循环外的label一起使用,用于控制外层循环的执行逻辑
  12. // continue here表示外层的for循环继续执行,继续执行时外层for循环里的i会++
  13. // break there表示退出外层循环,也就是退出整个循环了
  14. here:
  15. for i := 0; i < 2; i++ {
  16. for j := i + 1; j < 3; j++ {
  17. if i == 0 {
  18. continue here
  19. }
  20. fmt.Println(j)
  21. if j == 2 {
  22. break
  23. }
  24. }
  25. }
  26. there:
  27. for i := 0; i < 2; i++ {
  28. for j := i + 1; j < 3; j++ {
  29. if j == 1 {
  30. continue
  31. }
  32. fmt.Println(j)
  33. if j == 2 {
  34. break there
  35. }
  36. }
  37. }

Switch

  1. // switch语句
  2. switch operatingSystem {
  3. case "darwin":
  4. fmt.Println("Mac OS Hipster")
  5. // case分支里的代码执行完后会自动退出switch,默认没有fallthrough
  6. case "linux":
  7. fmt.Println("Linux Geek")
  8. default:
  9. // Windows, BSD, ...
  10. fmt.Println("Other")
  11. }
  12. // 和if一样,switch的value之前可以添加一条赋值语句
  13. switch os := runtime.GOOS; os {
  14. case "darwin": ...
  15. }
  16. // switch的case条件还可以是比较语句
  17. number := 42
  18. switch {
  19. case number < 42:
  20. fmt.Println("Smaller")
  21. case number == 42:
  22. fmt.Println("Equal")
  23. case number > 42:
  24. fmt.Println("Greater")
  25. }
  26. // case分支后还可以带多个值,用逗号分隔,任意一个匹配即可
  27. var char byte = '?'
  28. switch char {
  29. case ' ', '?', '&', '=', '#', '+', '%':
  30. fmt.Println("Should escape")
  31. }

数组,切片和range迭代

数组

  1. var a [10]int // 声明一个长度为10的int数组,数组长度也是数组类型的一部分
  2. a[3] = 42 // 设置数组元素的值
  3. i := a[3] // 读数组元素的值
  4. // 声明和初始化
  5. var a = [2]int{1, 2}
  6. a := [2]int{1, 2} //简写
  7. a := [...]int{1, 2} // 编译器自行推导数组长度

切片

  1. var a []int // 声明切片,和数组类型声明类似,不需要指定长度
  2. var a = []int {1, 2, 3, 4} // 声明和初始化切片
  3. a := []int{1, 2, 3, 4} // 简写
  4. chars := []string{0:"a", 2:"c", 1: "b"} // ["a", "b", "c"]
  5. var b = a[lo:hi] // 通过下标索引从已有的数组或切片创建新切片,下标前闭后开,取值从lo到hi-1
  6. var b = a[1:4] // 取切片a的下标索引从1到3的值赋值给新切片b
  7. var b = a[:3] // :前面没有值表示起始索引是0,等同于a[0:3]
  8. var b = a[3:] // :后面没有值表示结束索引是len(a),等同于a[3:len(a)]
  9. a = append(a,17,3) // 往切片里添加新元素
  10. c := append(a,b...) // 把切片a和b的值拼接起来,组成新切片
  11. // 使用make来创建切片
  12. a = make([]byte, 5, 5) // make的第2个参数是切片长度,第3个参数是切片容量
  13. a = make([]byte, 5) // 第3个切片容量参数可选,即可以不传值
  14. // 根据数组来创建切片
  15. x := [3]string{"Лайка", "Белка", "Стрелка"}
  16. s := x[:] // 切片s指向了数组x的内存空间,改变切片s的值,也会影响数组x的值

数组和切片上的操作

len(a)可以用来计算数组或切片的长度,len()是Go的内置函数,不是数组或者切片的方法

  1. // 循环遍历数组或切片
  2. for i, e := range a {
  3. // i是下标索引,从0开始, e是具体的元素
  4. }
  5. // 如果你只需要元素,不需要下标索引,可以按照下面的方式做:
  6. for _, e := range a {
  7. // e是元素
  8. }
  9. // 如果你只需要下标索引,可以按照下面的方式做
  10. for i := range a {
  11. }
  12. // Go 1.4之前, 如果range的前面不按照上面2个示例那样带上i和e,会编译报错
  13. // Go 1.4开始,可以不用带上i和e,直接for range遍历
  14. for range time.Tick(time.Second) {
  15. // 每秒执行一次
  16. }

集合

  1. m := make(map[string]int)
  2. m["key"] = 42
  3. fmt.Println(m["key"])
  4. delete(m, "key")
  5. elem, ok := m["key"] // 判断key是否存在:如果存在,ok就是true,elem是对应value,否则ok是false,elem是map的value的类型的零值
  6. // map字面值,声明的同时做初始化
  7. var m = map[string]Vertex{
  8. "Bell Labs": {40.68433, -74.39967},
  9. "Google": {37.42202, -122.08408},
  10. }
  11. // 遍历map
  12. for key, value := range m {
  13. }

结构体

Go没有class,只有结构体struct,结构体可以有自己的方法。

  1. // 结构体是一种类型,也是一系列字段的集合
  2. // 声明
  3. type Vertex struct {
  4. X, Y float64
  5. }
  6. // 创建结构体变量
  7. var v = Vertex{1, 2}
  8. var v = Vertex{X: 1, Y: 2} // 通过字段名称:值的形式来创建结构体变量
  9. var v = []Vertex{{1,2},{5,2},{5,5}} // 初始化结构体切片
  10. // 访问结构体的字段
  11. v.X = 4
  12. // 给结构体定义方法,在func关键字和方法名称之间加上结构体声明(var_name StructName)即可
  13. // 调用方法时,会把结构体的值拷贝一份
  14. func (v Vertex) Abs() float64 {
  15. return math.Sqrt(v.X*v.X + v.Y*v.Y)
  16. }
  17. // 调用结构体方法
  18. v.Abs()
  19. // 如果想调用方法时改变外部结构体变量的值,方法需要使用指针接受者
  20. // 下面的方法,每次调用add方法时就不会拷贝结构体的值
  21. func (v *Vertex) add(n float64) {
  22. v.X += n
  23. v.Y += n
  24. }

匿名结构体:
比使用map[string]interface{}更轻量、更安全。

  1. point := struct {
  2. X, Y int
  3. }{1, 2}

指针

  1. p := Vertex{1, 2} // p是结构体Vertex的变量或者说实例
  2. q := &p // q是指向Vertex的指针
  3. r := &Vertex{1, 2} // r也是指向Vertex的指针
  4. // Vertex指针的类型是*Vertex
  5. var s *Vertex = new(Vertex) // new函数创建一个指向Vertex实例的指针

接口

  1. // 接口声明
  2. type Awesomizer interface {
  3. Awesomize() string
  4. }
  5. // 结构体不会在声明的时候指定要实现某个接口
  6. type Foo struct {}
  7. // 相反,结构体如果实现了接口里的所有方法,那就隐式表明该结构体满足了该接口
  8. // 可以通过接口变量来调用结构体方法
  9. func (foo Foo) Awesomize() string {
  10. return "Awesome!"
  11. }

接口和结构体嵌套

Go没有子类的概念,不过Go有接口嵌套和结构体嵌套。

  1. // 接口嵌套,ReadWriter的实现一定要同时实现Reader和Writer这2个接口类型里的所有方法
  2. type ReadWriter interface {
  3. Reader
  4. Writer
  5. }
  6. // 结构体嵌套,Server同时有了log.Logger的所有方法
  7. type Server struct {
  8. Host string
  9. Port int
  10. *log.Logger
  11. }
  12. // 初始化嵌套结构体变量,和普通结构体初始化一样
  13. server := &Server{"localhost", 80, log.New(...)}
  14. // 被嵌套的结构体log.Logger的方法也自然成为了结构体Server的方法
  15. server.Log(...) // 相当于调用了server.Logger.Log(...)
  16. // 被嵌套的类型的字段名称是它的类型名称(在本代码示例里,被嵌套的类型*log.Logger的字段名称是它的类型名称Logger)
  17. var logger *log.Logger = server.Logger

错误处理

Go没有异常处理。函数如果可能产生错误只需要在函数返回值里额外增加一个类型为error的返回值。error接口类型的定义如下:

  1. // error接口类型是Go内置类型,用于表示错误
  2. // 值为nil时表示没有错误
  3. type error interface {
  4. Error() string
  5. }

这里有一个示例:

  1. func sqrt(x float64) (float64, error) {
  2. if x < 0 {
  3. return 0, errors.New("negative value")
  4. }
  5. return math.Sqrt(x), nil
  6. }
  7. func main() {
  8. val, err := sqrt(-1)
  9. if err != nil {
  10. // 处理错误
  11. fmt.Println(err) // negative value
  12. return
  13. }
  14. // 没有错误,打印结果
  15. fmt.Println(val)
  16. }

并发

协程Goroutine

Goroutines是轻量级线程(由Go运行时来管理,不是操作系统线程)。 go f(a, b) 语句会开启了一个新的goroutine,这个goroutine执行f(a, b)这个函数调用。

  1. // 这里只是定义一个函数,后面用于goroutine执行
  2. func doStuff(s string) {
  3. }
  4. func main() {
  5. // 在goroutine里使用有名称的函数
  6. go doStuff("foobar")
  7. // 在goroutine里使用匿名函数(闭包)
  8. go func (x int) {
  9. // 函数体定义
  10. }(42)
  11. }

管道Channel

  1. ch := make(chan int) // 创建类型为int的管道
  2. ch <- 42 // 发送数据到管道ch
  3. v := <-ch // 从管道ch接收数据
  4. // 没有缓冲区的管道会阻塞。
  5. // 如果没有往管道发送值,读操作会阻塞,如果没有从管道接收值,写操作会阻塞
  6. // 创建带缓冲区的管道
  7. // 如果缓冲区未满,往有缓冲区的管道发送数据不会阻塞
  8. ch := make(chan int, 100)
  9. close(ch) // 关闭管道(只有往管道发送数据的发送者才应该执行close操作)
  10. // 从管道读数据,并判断管道是否已经被关闭
  11. v, ok := <-ch
  12. // 如果ok是false就表示管道已经被关闭了
  13. // 从管道读数据,直到管道被关闭
  14. for i := range ch {
  15. fmt.Println(i)
  16. }
  17. // select关键字在多个管道操作上阻塞,只要有1个不阻塞了,对应case分支就会被执行
  18. func doStuff(channelOut, channelIn chan int) {
  19. select {
  20. case channelOut <- 42:
  21. fmt.Println("We could write to channelOut!")
  22. case x := <- channelIn:
  23. fmt.Println("We could read from channelIn")
  24. case <-time.After(time.Second * 1):
  25. fmt.Println("timeout")
  26. }
  27. }

Channel原则

  • 给值为nil的管道发送数据会一直阻塞
  1. var c chan string
  2. c <- "Hello, World!"
  3. // fatal error: all goroutines are asleep - deadlock!
  • 从值为nil的管道接收数据会一直阻塞
  1. var c chan string
  2. fmt.Println(<-c)
  3. // fatal error: all goroutines are asleep - deadlock!
  • 往被关闭的管道发送数据会panic
  1. var c = make(chan string, 1)
  2. c <- "Hello, World!"
  3. close(c)
  4. c <- "Hello, Panic!"
  5. // panic: send on closed channel
  • 从被关闭的管道接收数据会立即返回零值
  1. var c = make(chan int, 2)
  2. c <- 1
  3. c <- 2
  4. close(c)
  5. for i := 0; i < 3; i++ {
  6. fmt.Printf("%d ", <-c)
  7. }
  8. // 1 2 0

打印

  1. fmt.Println("Hello, 你好, नमस्ते, Привет, ᎣᏏᏲ") //基本的打印,会自动换行
  2. p := struct { X, Y int }{ 17, 2 }
  3. fmt.Println( "My point:", p, "x coord=", p.X ) // 打印结构体和字段值
  4. s := fmt.Sprintln( "My point:", p, "x coord=", p.X ) // 打印内容到字符串变量里
  5. fmt.Printf("%d hex:%x bin:%b fp:%f sci:%e",17,17,17,17.0,17.0) // C风格的格式化打印
  6. s2 := fmt.Sprintf( "%d %f", 17, 17.0 ) // 字符串格式化
  7. hellomsg := `
  8. "Hello" in Chinese is 你好 ('Ni Hao')
  9. "Hello" in Hindi is नमस्ते ('Namaste')
  10. ` // 跨越多行的字符串,使用``

反射

类型Switch

类型switch类似普通的switch语句,只是case分支的判断条件是类型,而不是具体的值。使用场景主要是用于判断接口变量的值类型。

  1. func do(i interface{}) {
  2. switch v := i.(type) {
  3. case int:
  4. fmt.Printf("Twice %v is %v\n", v, v*2)
  5. case string:
  6. fmt.Printf("%q is %v bytes long\n", v, len(v))
  7. default:
  8. fmt.Printf("I don't know about type %T!\n", v)
  9. }
  10. }
  11. func main() {
  12. do(21)
  13. do("hello")
  14. do(true)
  15. }

代码片段

文件嵌入

Go程序可以使用embed包嵌入静态文件

  1. package main
  2. import (
  3. "embed"
  4. "fmt"
  5. "io"
  6. "log"
  7. "net/http"
  8. )
  9. // content持有服务器static目录下的所有文件
  10. //go:embed static/*
  11. var content embed.FS
  12. func main() {
  13. http.Handle("/", http.FileServer(http.FS(content)))
  14. go func() {
  15. log.Fatal(http.ListenAndServe(":8080", nil))
  16. }()
  17. // 读取服务器static目录下的内容
  18. entries, err := content.ReadDir("static")
  19. if err != nil {
  20. log.Fatal(err)
  21. }
  22. for _, e := range entries {
  23. resp, err := http.Get("http://localhost:8080/static/" + e.Name())
  24. if err != nil {
  25. log.Fatal(err)
  26. }
  27. body, err := io.ReadAll(resp.Body)
  28. if err != nil {
  29. log.Fatal(err)
  30. }
  31. if err := resp.Body.Close(); err != nil {
  32. log.Fatal(err)
  33. }
  34. fmt.Printf("%q: %s", e.Name(), body)
  35. }
  36. }

Go Playground代码示例

HTTP服务器

运行下面的代码,在浏览器访问 http://127.0.0.1:4000 会显示”hello”

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. // 定义处理http请求的Handler
  7. type Hello struct{}
  8. // 结构体Hello实现接口类型http.Handler里的方法ServeHTTP
  9. // 这样结构体Hello的实例就可以作为http的Handler来接收http请求,返回http响应结果
  10. func (h Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  11. fmt.Fprint(w, "Hello!")
  12. }
  13. func main() {
  14. var h Hello
  15. http.ListenAndServe("localhost:4000", h)
  16. }
  17. // 下面是http.ServerHTTP的方法签名
  18. // type Handler interface {
  19. // ServeHTTP(w http.ResponseWriter, r *http.Request)
  20. // }