Go by Example 完整学习笔记

·

Go by Example 完整学习笔记 #

以下内容整理自 Go by Example 网站的 18 个页面的代码、说明和输出。


Structs #

Go 的 structs(结构体) 是类型化的字段集合,用于将数据组合成记录。

package main

import "fmt"

// person 结构体包含 name 和 age 字段
type person struct {
    name string
    age  int
}

// newPerson 用给定的名字构造一个新的 person 结构体
func newPerson(name string) *person {
    p := person{name: name}
    p.age = 42
    return &p
}

func main() {
    // 按位置顺序创建结构体
    fmt.Println(person{"Bob", 20})

    // 按字段名初始化
    fmt.Println(person{name: "Alice", age: 30})

    // 省略的字段为零值
    fmt.Println(person{name: "Fred"})

    // & 前缀生成结构体指针
    fmt.Println(&person{name: "Ann", age: 40})

    // 使用构造函数
    fmt.Println(newPerson("Jon"))

    // 用点号访问字段
    s := person{name: "Sean", age: 50}
    fmt.Println(s.name)

    // 结构体指针自动解引用
    sp := &s
    fmt.Println(sp.age)

    // 结构体是可变的
    sp.age = 51
    fmt.Println(sp.age)

    // 匿名结构体
    dog := struct {
        name   string
        isGood bool
    }{
        "Rex",
        true,
    }
    fmt.Println(dog)
}

运行输出:

$ go run structs.go
{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
&{Jon 42}
Sean
50
51
{Rex true}

Methods #

Go 支持在结构体类型上定义 methods(方法)

package main

import "fmt"

// rect 结构体
type rect struct {
    width, height int
}

// area 方法具有 *rect 指针接收者
func (r *rect) area() int {
    return r.width * r.height
}

// perim 方法具有值接收者
func (r rect) perim() int {
    return 2*r.width + 2*r.height
}

func main() {
    r := rect{width: 10, height: 5}

    // 调用为结构体定义的 2 个方法
    fmt.Println("area: ", r.area())
    fmt.Println("perim:", r.perim())

    // Go 自动处理值和指针之间的转换
    rp := &r
    fmt.Println("area: ", rp.area())
    fmt.Println("perim:", rp.perim())
}

运行输出:

$ go run methods.go
area:  50
perim: 30
area:  50
perim: 30

说明:

  • 方法可以定义为指针接收者或值接收者类型
  • 指针接收者可以避免方法调用时的复制,或允许方法修改接收的结构体
  • Go 自动处理方法调用中值和指针之间的转换

Interfaces #

Interfaces(接口) 是方法签名的命名集合。

package main

import (
    "fmt"
    "math"
)

// 几何形状的基本接口
type geometry interface {
    area() float64
    perim() float64
}

// rect 和 circle 类型
type rect struct {
    width, height float64
}
type circle struct {
    radius float64
}

// 在 rect 上实现 geometry 接口
func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

// 在 circle 上实现 geometry 接口
func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

// measure 函数可以处理任何 geometry
func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

// 类型断言
func detectCircle(g geometry) {
    if c, ok := g.(circle); ok {
        fmt.Println("circle with radius", c.radius)
    }
}

func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    measure(c)

    detectCircle(r)
    detectCircle(c)
}

运行输出:

$ go run interfaces.go
{3 4}
12
14
{5}
78.53981633974483
31.41592653589793
circle with radius 5

说明:

  • 在 Go 中实现接口,只需实现接口中的所有方法
  • 类型断言(type assertion)可用于了解接口值的运行时类型
  • switch 也是处理接口类型的一种方式

Enums #

Enumerated types(枚举类型) 是 sum types(和类型)的特例。Go 没有单独的枚举类型语言特性,但可以使用现有语言习惯用法轻松实现枚举。

package main

import "fmt"

// ServerState 枚举类型,底层为 int
type ServerState int

// 使用 iota 定义枚举值
const (
    StateIdle      ServerState = iota
    StateConnected
    StateError
    StateRetrying
)

// 实现 fmt.Stringer 接口
var stateName = map[ServerState]string{
    StateIdle:      "idle",
    StateConnected: "connected",
    StateError:     "error",
    StateRetrying:  "retrying",
}

func (ss ServerState) String() string {
    return stateName[ss]
}

func main() {
    ns := transition(StateIdle)
    fmt.Println(ns)

    ns2 := transition(ns)
    fmt.Println(ns2)
}

// transition 模拟服务器的状态转换
func transition(s ServerState) ServerState {
    switch s {
    case StateIdle:
        return StateConnected
    case StateConnected, StateRetrying:
        // 假设在此检查一些条件以确定下一个状态...
        return StateIdle
    case StateError:
        return StateError
    default:
        panic(fmt.Errorf("unknown state: %s", s))
    }
}

运行输出:

$ go run enums.go
connected
idle

说明:

  • iota 关键字自动生成连续常量值:0, 1, 2, …
  • 通过实现 fmt.Stringer 接口,枚举值可以打印或转换为字符串
  • Go 的类型系统为枚举提供了一定程度的编译时类型安全

Struct Embedding #

Go 支持 struct embedding(结构体嵌入),以表达更无缝的类型组合。

package main

import "fmt"

type base struct {
    num int
}

func (b base) describe() string {
    return fmt.Sprintf("base with num=%v", b.num)
}

// container 嵌入了 base(看起来像一个没有名字的字段)
type container struct {
    base
    str string
}

func main() {
    // 创建结构体字面量时需要显式初始化嵌入类型
    co := container{
        base: base{
            num: 1,
        },
        str: "some name",
    }

    // 可以直接访问 base 的字段
    fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)

    // 也可以使用嵌入类型名称的完整路径
    fmt.Println("also num:", co.base.num)

    // base 的方法也成为 container 的方法
    fmt.Println("describe:", co.describe())

    type describer interface {
        describe() string
    }

    // container 现在实现了 describer 接口,因为它嵌入了 base
    var d describer = co
    fmt.Println("describer:", d.describe())
}

运行输出:

$ go run struct-embedding.go
co={num: 1, str: some name}
also num: 1
describe: base with num=1
describer: base with num=1

说明:

  • 嵌入(embedding)不同于 //go:embed 指令
  • 嵌入结构体的方法也会成为外部结构体的方法
  • 嵌入结构体的结构体可以实现嵌入结构体所实现的接口

Generics #

从 Go 1.18 开始,Go 支持 generics(泛型),也称为 type parameters(类型参数)

package main

import "fmt"

// SlicesIndex 是一个泛型函数,接受任何 comparable 类型的切片和元素,
// 返回 v 在 s 中第一次出现的索引,如果不存在则返回 -1
func SlicesIndex[S ~[]E, E comparable](s S, v E) int {
    for i := range s {
        if v == s[i] {
            return i
        }
    }
    return -1
}

// List 是一个泛型类型:任意类型值的单向链表
type List[T any] struct {
    head, tail *element[T]
}

type element[T any] struct {
    next *element[T]
    val  T
}

// 在泛型类型上定义方法
func (lst *List[T]) Push(v T) {
    if lst.tail == nil {
        lst.head = &element[T]{val: v}
        lst.tail = lst.head
    } else {
        lst.tail.next = &element[T]{val: v}
        lst.tail = lst.tail.next
    }
}

// AllElements 将 List 的所有元素作为切片返回
func (lst *List[T]) AllElements() []T {
    var elems []T
    for e := lst.head; e != nil; e = e.next {
        elems = append(elems, e.val)
    }
    return elems
}

func main() {
    var s = []string{"foo", "bar", "zoo"}

    // 调用泛型函数时,可以依赖类型推断
    fmt.Println("index of zoo:", SlicesIndex(s, "zoo"))

    // 也可以显式指定类型
    _ = SlicesIndex[[]string, string](s, "zoo")

    // 使用泛型类型
    lst := List[int]{}
    lst.Push(10)
    lst.Push(13)
    lst.Push(23)
    fmt.Println("list:", lst.AllElements())
}

运行输出:

$ go run generics.go
index of zoo: 2
list: [10 13 23]

说明:

  • comparable 约束意味着可以使用 ==!= 运算符比较该类型的值
  • 泛型类型的方法必须保留类型参数位置(使用 List[T] 而非 List
  • 类型推断通常可以自动推断类型参数

Range over Iterators #

从 Go 1.23 开始,Go 支持 iterators(迭代器),使我们几乎可以 range 任何东西。

package main

import (
    "fmt"
    "iter"
    "slices"
    "strings"
)

type List[T any] struct {
    head, tail *element[T]
}

type element[T any] struct {
    next *element[T]
    val  T
}

func (lst *List[T]) Push(v T) {
    if lst.tail == nil {
        lst.head = &element[T]{val: v}
        lst.tail = lst.head
    } else {
        lst.tail.next = &element[T]{val: v}
        lst.tail = lst.tail.next
    }
}

// All 返回一个迭代器
func (lst *List[T]) All() iter.Seq[T] {
    return func(yield func(T) bool) {
        for e := lst.head; e != nil; e = e.next {
            if !yield(e.val) {
                return
            }
        }
    }
}

// genFib 返回斐波那契数列的迭代器
func genFib() iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1
        for {
            if !yield(a) {
                return
            }
            a, b = b, a+b
        }
    }
}

func main() {
    lst := List[int]{}
    lst.Push(10)
    lst.Push(13)
    lst.Push(23)

    // 在 range 循环中使用迭代器
    for e := range lst.All() {
        fmt.Println(e)
    }

    // Collect 将迭代器所有值收集为切片
    all := slices.Collect(lst.All())
    fmt.Println("all:", all)

    // strings.SplitSeq 遍历分割后的部分
    for part := range strings.SplitSeq("go-by-example", "-") {
        fmt.Printf("part: %s\n", part)
    }

    // 遍历斐波那契数列
    for n := range genFib() {
        if n >= 10 {
            break
        }
        fmt.Println(n)
    }
}

运行输出:

$ go run range-over-iterators.go
10
13
23
all: [10 13 23]
part: go
part: by
part: example
0
1
1
2
3
5
8

说明:

  • 迭代器函数接受另一个函数(通常称为 yield)作为参数
  • yield 返回 false 时表示提前终止
  • 标准库包如 slicesstrings 现在也提供了迭代器辅助函数

Errors #

在 Go 中,通过明确、单独的返回值来传达错误是惯用做法。

package main

import (
    "errors"
    "fmt"
)

// 按照惯例,错误是最后一个返回值,类型为 error
func f(arg int) (int, error) {
    if arg == 42 {
        // errors.New 用给定的错误消息构造一个基本的 error 值
        return -1, errors.New("can't work with 42")
    }
    // 错误位置的 nil 值表示没有错误
    return arg + 3, nil
}

// 哨兵错误(sentinel error)
var ErrOutOfTea = errors.New("no more tea available")
var ErrPower = errors.New("can't boil water")

func makeTea(arg int) error {
    if arg == 2 {
        return ErrOutOfTea
    } else if arg == 4 {
        // 使用 %w 包装错误以添加上下文
        return fmt.Errorf("making tea: %w", ErrPower)
    }
    return nil
}

func main() {
    for _, i := range []int{7, 42} {
        // 在 if 行中内联错误检查
        if r, e := f(i); e != nil {
            fmt.Println("f failed:", e)
        } else {
            fmt.Println("f worked:", r)
        }
    }

    for i := range 5 {
        if err := makeTea(i); err != nil {
            // errors.Is 检查错误链中是否匹配特定错误值
            if errors.Is(err, ErrOutOfTea) {
                fmt.Println("We should buy new tea!")
            } else if errors.Is(err, ErrPower) {
                fmt.Println("Now it is dark.")
            } else {
                fmt.Printf("unknown error: %s\n", err)
            }
            continue
        }
        fmt.Println("Tea is ready!")
    }
}

运行输出:

$ go run errors.go
f worked: 10
f failed: can't work with 42
Tea is ready!
Tea is ready!
We should buy new tea!
Tea is ready!
Now it is dark.

说明:

  • errors.New 构造一个基本的 error
  • 包装错误(%w)创建逻辑链,可用 errors.Iserrors.As 查询
  • 哨兵错误是用于表示特定错误条件的预声明变量

Custom Errors #

可以通过实现 Error() 方法来自定义错误类型。

package main

import (
    "errors"
    "fmt"
)

// 自定义错误类型通常以 "Error" 为后缀
type argError struct {
    arg     int
    message string
}

// 添加 Error 方法使 argError 实现 error 接口
func (e *argError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.message)
}

func f(arg int) (int, error) {
    if arg == 42 {
        // 返回自定义错误
        return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}

func main() {
    _, err := f(42)
    // errors.AsType 检查错误是否匹配特定错误类型并转换为该类型的值
    if ae, ok := errors.AsType[*argError](err); ok {
        fmt.Println(ae.arg)
        fmt.Println(ae.message)
    } else {
        fmt.Println("err doesn't match argError")
    }
}

运行输出:

$ go run custom-errors.go
42
can't work with it

说明:

  • 自定义错误类型通过实现 Error() string 方法来满足 error 接口
  • errors.AsTypeerrors.Is 的更高级版本,用于检查并转换错误类型

Goroutines #

Goroutine 是一个轻量级的执行线程。

package main

import (
    "fmt"
    "time"
)

func f(from string) {
    for i := range 3 {
        fmt.Println(from, ":", i)
    }
}

func main() {
    // 常规同步调用
    f("direct")

    // 在 goroutine 中调用
    go f("goroutine")

    // 启动匿名函数的 goroutine
    go func(msg string) {
        fmt.Println(msg)
    }("going")

    // 等待 goroutine 完成
    time.Sleep(time.Second)
    fmt.Println("done")
}

运行输出:

$ go run goroutines.go
direct : 0
direct : 1
direct : 2
goroutine : 0
going
goroutine : 1
goroutine : 2
done

说明:

  • 使用 go f(s) 在 goroutine 中调用函数
  • goroutine 是并发执行的,输出可能会交错
  • 更健壮的等待方式使用 WaitGroup

Channels #

Channels(通道) 是连接并发 goroutine 的管道。

package main

import "fmt"

func main() {
    // 使用 make(chan val-type) 创建新通道
    messages := make(chan string)

    // 从新的 goroutine 向通道发送值
    go func() {
        messages <- "ping"
    }()

    // 从通道接收值
    msg := <-messages
    fmt.Println(msg)
}

运行输出:

$ go run channels.go
ping

说明:

  • 通道由它们传递的值类型化
  • 默认情况下,发送和接收会阻塞,直到发送方和接收方都就绪
  • 这个特性使得无需额外的同步就可以等待消息

Channel Buffering #

默认情况下通道是 unbuffered(无缓冲) 的。Buffered channels(缓冲通道) 可以接受有限数量的值而无需对应的接收者。

package main

import "fmt"

func main() {
    // 创建一个缓冲大小为 2 的字符串通道
    messages := make(chan string, 2)

    // 因为通道有缓冲,可以在没有并发接收者的情况下发送值
    messages <- "buffered"
    messages <- "channel"

    // 稍后正常接收这两个值
    fmt.Println(<-messages)
    fmt.Println(<-messages)
}

运行输出:

$ go run channel-buffering.go
buffered
channel

Channel Synchronization #

可以使用通道来同步跨 goroutine 的执行。

package main

import (
    "fmt"
    "time"
)

// worker 函数,done 通道用于通知完成
func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")

    // 发送值通知完成
    done <- true
}

func main() {
    done := make(chan bool, 1)
    go worker(done)

    // 阻塞直到从通道收到通知
    <-done
}

运行输出:

$ go run channel-synchronization.go
working...done

说明:

  • 如果移除 <-done 行,程序可能在 worker 完成之前就退出
  • 等待多个 goroutine 完成时,建议使用 WaitGroup

Channel Directions #

当将通道用作函数参数时,可以指定通道是仅发送还是仅接收值,这增加了程序的类型安全性。

package main

import "fmt"

// ping 函数只接受用于发送值的通道
func ping(pings chan<- string, msg string) {
    pings <- msg
}

// pong 函数接受一个接收通道和一个发送通道
func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}

func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "passed message")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}

运行输出:

$ go run channel-directions.go
passed message

Select #

Go 的 select 让你可以等待多个通道操作。

package main

import (
    "fmt"
    "time"
)

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    // 每个通道经过一段时间后会收到一个值
    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    // 使用 select 同时等待两个值
    for range 2 {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

运行输出:

$ time go run select.go
received one
received two
real 0m2.245s

说明:

  • 总执行时间仅为约 2 秒,因为 1 秒和 2 秒的 Sleep 是并发执行的

Timeouts #

Timeouts(超时) 对于连接外部资源或需要限制执行时间的程序非常重要。由于通道和 select,实现超时在 Go 中既简单又优雅。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 模拟外部调用,2 秒后返回结果
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c1 <- "result 1"
    }()

    // 使用 select 实现超时
    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout 1")
    }

    // 如果允许更长的超时时间,接收将成功
    c2 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "result 2"
    }()
    select {
    case res := <-c2:
        fmt.Println(res)
    case <-time.After(3 * time.Second):
        fmt.Println("timeout 2")
    }
}

运行输出:

$ go run timeouts.go
timeout 1
result 2

说明:

  • 使用缓冲通道是一种常见模式,用于防止在通道未被读取时发生 goroutine 泄漏
  • time.After 在指定超时时间后发送一个值

Non-Blocking Channel Operations #

可以使用带有 default 子句的 select 来实现 非阻塞 的发送、接收和多路 select。

package main

import "fmt"

func main() {
    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
    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    case sig := <-signals:
        fmt.Println("received signal", sig)
    default:
        fmt.Println("no activity")
    }
}

运行输出:

$ go run non-blocking-channel-operations.go
no message received
no message sent
no activity

Closing Channels #

Closing(关闭) 通道表示不再有值会被发送到该通道,这对于向通道接收方传达完成状态很有用。

package main

import "fmt"

func main() {
    jobs := make(chan int, 5)
    done := make(chan bool)

    // 工作 goroutine
    go func() {
        for {
            // 两值形式的接收:more 在通道已关闭且值已全部接收时为 false
            j, more := <-jobs
            if more {
                fmt.Println("received job", j)
            } else {
                fmt.Println("received all jobs")
                done <- true
                return
            }
        }
    }()

    // 发送 3 个作业然后关闭通道
    for j := 1; j <= 3; j++ {
        jobs <- j
        fmt.Println("sent job", j)
    }
    close(jobs)
    fmt.Println("sent all jobs")

    // 等待 worker 完成
    <-done

    // 从已关闭的通道读取会立即成功,返回底层类型的零值
    _, ok := <-jobs
    fmt.Println("received more jobs:", ok)
}

运行输出:

$ go run closing-channels.go
sent job 1
received job 1
sent job 2
received job 2
sent job 3
received job 3
sent all jobs
received all jobs
received more jobs: false

说明:

  • 两值形式 j, more := <-jobs 中,more 在通道关闭且所有值已接收时为 false
  • 从已关闭的空通道读取返回零值,第二个返回值表示值是否由成功的发送操作送达