Golang-cheat-sheet 的中文版本


共计 7919 个字符,预计需要花费 20 分钟才能阅读完成。

English

golang-cheat-sheet是目前GitHub上最流行的golang代码速查表。

作者Ariel Mashraki也是Facebook著名ORM框架ent(2019年开源)的作者和首席布道师。

本项目是本人翻译的该速查表的中文版本,会根据原版实时更新。

版本

  • 当前更新版本:2022-08-27 英文原版链接:fix "mismatched types int and float64"#76

  • 如果您发现更新、问题或改进,欢迎随时提issue或PR。

  • please feel free to open an issue or PR if you find any updates, issues or improvement.

致谢

大多数代码示例来源于A Tour of Go,它对Go做了非常棒的介绍。
很认真地说,如果你是Go新手,一定要看完A Tour of Go

Go特性一览

  • 命令式编程语言
  • 静态类型
  • 语法类似C语言(但是和C语言相比,圆括号更少,没有分号),结构类似Oberon-2编程语言
  • 编译成机器代码(没有JVM)
  • 没有类,但是有可以带方法的结构体
  • 接口类型Interfaces
  • 没有继承,但是有类型嵌套
  • 函数是一等公民
  • 函数可以返回多个值
  • 有闭包
  • 有指针,但是没有指针运算
  • 内置并发原语:协程goroutine和管道channel

基础语法

Hello World

文件 hello.go:

package main

import "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 (
    _ = iota
    a
    b
    c = 1 << iota
    d
)
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 = 42
    s = "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 := 2
    foo := func() int { return outer_var}
    return foo
}

func another_scope() func() int{
    // 编译失败,因为outer_var和foo没有在another_scope里定义
    outer_var = 444
    return foo
}

// 闭包
func outer() (func() int, int) {
    outer_var := 2
    inner := 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))     // 6
    fmt.Println(adder(9, 9))    // 18

    nums := []int{10, 20, 30}
    fmt.Println(adder(nums...)) // 60
}

// 在最后一个参数的类型前面加...表示函数的最后一个传参可以有0个或者多个
// 函数调用和普通函数一样,只是我们可以传递任意多个参数
func adder(args ...int) int {
    total := 0
    for _, v := range args { // 遍历传进来的参数, args是一个slice类型变量
        total += v
    }
    return total
}

内置类型

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8的别名

rune // int32的别名,主要用来表示字符类型

float32 float64

complex64 complex128

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

类型转换

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

// 下面这种语法也可以,用于局部作用域
i := 42
f := 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,默认没有fallthrough
case "linux":
    fmt.Println("Linux Geek")
default:
    // Windows, BSD, ...
    fmt.Println("Other")
}

// 和if一样,switch的value之前可以添加一条赋值语句
switch os := runtime.GOOS; os {
case "darwin": ...
}

// switch的case条件还可以是比较语句
number := 42
switch {
    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-1
var b = a[1:4]      // 取切片a的下标索引从1到3的值赋值给新切片b
var 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"] = 42
fmt.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},
}

// 遍历map
for 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 += n
    v.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指针的类型是*Vertex

var 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 {
    Reader
    Writer
}

// 结构体嵌套,Server同时有了log.Logger的所有方法
type Server struct {
    Host string
    Port 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 value
        return
    }
    // 没有错误,打印结果
    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             // 发送数据到管道ch
v := <-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 string
    c 
  • 从值为nil的管道接收数据会一直阻塞

    var c chan string
    fmt.Println(
  • 往被关闭的管道发送数据会panic

    var c = make(chan string, 1)
    c 
  • 从被关闭的管道接收数据会立即返回零值

    var c = make(chan int, 2)
    c 

打印

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 main

import (
    "embed"
    "fmt"
    "io"
    "log"
    "net/http"
)

// content持有服务器static目录下的所有文件
//go:embed static/*
var content embed.FS

func 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)
    }
}

Go Playground代码示例

HTTP服务器

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

package main

import (
    "fmt"
    "net/http"
)

// 定义处理http请求的Handler
type 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 Hello
    http.ListenAndServe("localhost:4000", h)
}

// 下面是http.ServerHTTP的方法签名
// type Handler interface {
//     ServeHTTP(w http.ResponseWriter, r *http.Request)
// }

转载自:a8m/golang-cheat-sheet 的中文版本

Tips:清朝云网络工作室

阅读剩余
THE END