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时表示提前终止- 标准库包如
slices和strings现在也提供了迭代器辅助函数
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.Is和errors.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.AsType是errors.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 - 从已关闭的空通道读取返回零值,第二个返回值表示值是否由成功的发送操作送达