TOC

Golang 基础

工作这么多年之后,学习一门新的语言,对于语法方面的东西应该是只用看看就行了(重点在其生态的学习)。
所以,这只是对 Go 语法做一个简单的梳理。

25 个关键字

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var
  1. 声明 (4): var 变量 / const 常量 / type 类型 / func 函数
  2. 并发相关 (3): go / chan / select
  3. 类型 (3): interface 接口 / map 映射 / struct 结构体
  4. 循环 (4): for continue break
    range 用于读取 slice、map、channel 数据
  5. 分支 (6): if else
    switch case default fallthrough
  6. 包 (2): package / import
  7. 其他 (3): defer goto return

预定义标识符 Predefined identifiers

https://golang.org/ref/spec#Predeclared_identifiers
https://go.dev/ref/spec#Predeclared_identifiers

或者叫保留字 Reserved words

Types:
    bool byte complex64 complex128 error float32 float64
    int int8 int16 int32 int64 rune string
    uint uint8 uint16 uint32 uint64 uintptr

Constants:
    true false iota

Zero value:
    nil

Functions:
    append cap close complex copy delete imag len
    make new panic print println real recover
  • 数据类型 (19) 参见下面基础类型部分
    • 数值类型 17
    • string 1
    • bool 1
  • 特殊的量 4
    • true / false
    • iota
    • nil
  • 内建函数 (15)
    • 长度和容量 2
      • cap 返回切片的容量
      • len 返回切片的长度
    • 切片 2
      • append 给切片追加元素
      • copy 拷贝数据
    • Map 1
      • delete 删除 map 中的元素
    • 异常 2
      • panic
      • recover
    • 打印 2
      • print 打印
      • println 打印并换行
    • 复数 3
      • complex 复数
      • imag 返回复数的虚部
      • real 返回复数的实部
    • 其他 3
      • make 创建 slice、map、channel
      • new 分配内存,返回指定类型的零值
      • close 关闭 channel

语法 Syntax

if else

if condition {
    // ...
} else if {
    // ...
} else {
    // ...
}

switch

// 注意:case 代码块执行完成之后会自动 break
switch varable {
case value1:
    // ...
case value2:
    // ...
default:
    // ...
}

switch {
case exp1:
    // ...
case exp2:
    // ...
default:
    // ...
}

// 一种特殊用法:判断 interface 的数据类型
// invalid syntax tree: use of .(type) outside type switch
switch t := x.(type) {
case int:
    print(t + 1)
case float64:
    print(t + 1)
}
// 如果不需要 t,直接这样也行
// switch x.(type) {

for

for i := 0; i < 10; i++ {
    // ...
}

i = 0
for i < 10 {
    // ...
    i++
}

死循环

for {
    // ...
}

利用标签跳出嵌套循环

outerLoop:
for i := 1; i <= 3; i++ {
    for j := 1; j <= 3; j++ {
        fmt.Printf("i: %d, j: %d\n", i, j)
        if i == 2 && j == 2 {
            break outerLoop // 使用标签跳出外部循环
        }
    }
}

提示:golang 标签可以用 goto,continue,break。

for range

for index, value := range iterable {
    // ...
}
for _, value := range iterable {
    // ...
}

// 数组和切片直接通过索引取值可以减少一次赋值操作,或许对性能有提升
// map 类型需要做一次 hash,可能反倒不划算了
for index := range iterable {
    // ...
}

提示:for range 取到的 value 是数据拷贝!!!所以对 value 的修改不会作用到原变量。

package main

import "fmt"

func main() {
    s := "abc"
    for k, v := range s {
        fmt.Printf("k:%v, v:%v\n", k, v)
    }
    var kp *int
    var vp *rune
    for k, v := range s {
        if k == 0 {
            kp = &k
            vp = &v
        }
    }
    fmt.Printf("-- final --\nk:%v, v:%v\n", *kp, *vp)
}

// k:0, v:97
// k:1, v:98
// k:2, v:99
// -- final --
// k:2, v:99

指针指向了拷贝出来的变量,所以最后输出就不如预期。

函数

func Func1() {}
func Func1(p1, p2 string, p3 int) {}
func Func1(p1, p2 string, p3 int) int {}
func Func1(p1, p2 string, p3 int) (int, int) {}
func Func1(p1, p2 string, p3 int) (result int) {}

提示:函数传参是值传递,如果需要修改数据内容,应该采用指针传递的方式。

匿名函数和闭包

匿名函数就是没有名字的函数,可以复制给一个变量,作为参数传递,作为函数返回值。
匿名函数的一个非常重要的应用场景就是闭包 Closure,即允许操作它所在的内部作用域的数据。

提示:匿名函数和闭包的定义完全不同,只是大部分时候都使用匿名函数来实现闭包。

// 访问内部变量
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

// 回调函数
func forEachItem(items []int, callback func(int)) {
    for _, item := range items {
        callback(item)
    }
}

// 函数工厂
func multiplier(factor int) func(int) int {
    return func(x int) int {
        return factor * x
    }
}

数据类型 Types

  1. 不支持隐式类型转换,比如
    var a int64 = 1
    var b int = a   // error
    
    哪怕 int 和 int64 底层相同也不行,别名都不行
    参考:https://go.dev/ref/spec#Conversions
  2. 每种数据类型关联到一组方法(Method set)
  3. 字面量没有类型,Golang 中叫做 Untyped Constant, 无类型常量
    比如:var a int = 1.0 合法
    不过要是有小数,比如 var a int = 1.1 不合法,会报:constant xxx truncated to integer关于这一点的官方说明我没有找到。
    PS: 无类型常量有一个默认类型,在 := 的时候会用到
    相关文章:https://go.dev/blog/constants

基本类型

注意:常量只支持基本类型。

  1. Numberic 数值型
    • int8, int16, int32, int64
    • uint8, uint16, uint32, uint64
    • float32, float64
    • complex64, complex128
    • 两个别名类型:byte => uint8 (Byte), rune => int32 (Unicode)
    • 三种平台相关的类型:int, uint, uintptr (指针)
  2. String 字符串 string
  3. Boolean 布尔型 bool
    只有两个值:true / false
  4. error

默认值:

  • 数值型默认值 0
  • 布尔型默认值 false
  • 字符串默认值 "" (空字符串)

复合类型

  1. Array 数组
  2. Slice 切片
  3. Struct 结构体
  4. Pointer 指针
  5. Function 函数
  6. Interface 接口
  7. Map 映射 / 关联数组 / 字典 / 哈希表
  8. Channel types

类型转换

s := "1"
i := 1
strconv.Atoi(s)
strconv.ParseInt(s, 10, 64) // 转 int64
var itf interface{} = 1
s, ok := itf.(string)
i, ok := itf.(int)

数组 Array & 切片 Slice

  1. 数组有 len 和 cap 属性,cap 是数组的容量,len 是数组的长度。这个设计使得 Go 的数组有一定的拓展性,不像 C 那么死。
    如果 append 的数据太大,超出 cap 范围,就会重新分配内存。
  2. 切片 Slice 是一个引用类型,它指向一个底层数组,所以是共享内存的!在 Python 中切片生成新的数组。
[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64  // same as [2]([2]([2]float64))

// go/src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

映射 Map

map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}

make(map[string]int)
make(map[string]int, 100)

结构体 Struct

// A struct with 6 fields.
struct {
    x, y int
    u float32
    _ float32  // padding
    A *[]int
    F func()
}

结构体内存对齐

struct 的大小(unsafe.Sizeof(s))约等于各字段大小之和,但要注意:

  1. Go 语言数据类型的实际内存分配是会按照字来进行,而字又和 CPU 架构相关
    1. 64 位 CPU:一字代表 8B
    2. 32 位 CPU:一字代表 4B
  2. 如果相同的两个字段小于一字,可能会共用一字

函数 Function

func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)

可变参数:

func FuncName(a, b string, v ...int) {
    // v 是一个 int 数组
}
// 如果类型都不确定,只能用 `v ...interface{}` 了

接口 Interface

接口就是一组方法的集合。
所有类型都可以看作空接口(interface{})的实现。

// A simple File interface.
interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
    Close() error
}

interface {
    String() string
    String() string  // illegal: String not unique
    _(x int)         // illegal: method must have non-blank name
}

管道 Channel

PS: 有的地方说管道。

三个相关关键字:

  1. go 并发函数:协程 + 线程混合调度
  2. chan 管道:并发函数内外可以通过管道进行通讯
    • channel <- value 阻塞发送
    • x := <
    • <-channel // 接收并将其丢弃
    • x := <-channel // 接收并将其保存
    • x, ok := <-channel // 接收并将其保存,同时检查通道是否已经关闭或者为空
  3. select 通讯类型,作用和 switch 类似,但只能用于通道(为啥不复用 switch 关键字)
// a goroutine example
func main() {
    c := make(chan int) // 创建管道
    go func() {
        c <- 42
    }()
    fmt.Println(<-c)
}
package main

import (
    "fmt"
    "time"
)

func Producer(c chan<- int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
    fmt.Println("Producer: send over")
}

func Consumer(no int, c <-chan int) {
    for num := range c {
        fmt.Printf("Consumer#%d: get: %v\n", no, num)
    }
}

func test(c chan int) {
    for i := 100; i < 110; i++ {
        c <- i
    }
    fmt.Println("test: send over")
    // 如果没有 goroutine 在活跃状态,接收channel,则提示 deadlock
    // fatal error: all goroutines are asleep - deadlock!
    x, ok := <-c
    if !ok {
        fmt.Println("test: got err")
    }
    fmt.Printf("test: get: %v\n", x)
}

func main() {
    fmt.Println("start")
    c := make(chan int, 5)
    fmt.Printf("channel c: %v\n", c)
    go Consumer(1, c)
    fmt.Println("start Consumer 1")
    go Consumer(2, c)
    fmt.Println("start Consumer 2")
    go Producer(c)
    fmt.Println("start producer")
    test(c)
    fmt.Println("start test")
    fmt.Println("end")
    time.Sleep(time.Second * 1)
}

这个锁的情况挺有意思,下次另开一篇文章来说说。

其他

  1. 变量声明和初始化
  2. 数据类型转换
  3. 输出与字符串格式化
  4. 结构体(struct)和接口(interface
  5. 异常处理机制
  6. 包管理
  7. goroutine
  8. 反射
  9. 标准库

需要注意的点

  1. Go 的很多语句需要接收 err, 然后判断是否为 nil
  2. 函数不支持默认值参数,不定长参数传递也不如 Python 灵活(*args, **kwargs

package & import

package xxx is not in GOROOT

重点:

  • GOPATH 模式下,所有包在 $GOPATH/src, $GOROOT/src
  • Module 模式(GO111MODULE=ON,Go 1.3+ 的默认行为)下,

详情参考 20210204, GO111MODULE 是什么?