当前位置: 首页 > news >正文

跟着实例学Go语言(二)

本教程全面涵盖了Go语言基础的各个方面。一共80个例子,每个例子对应一个语言特性点,非常适合新人快速上手。
教程代码示例来自go by example,文字部分来自本人自己的理解。

本文是教程系列的第二部分,共计20个例子、约1.2万字。

目录

  • 21. Interfaces
  • 22. Struct Embedding
  • 23. Generics
  • 24. Errors
  • 25. Goroutines
  • 26. Channels
  • 27. Channel Buffering
  • 28. Channel Synchronization
  • 29. Channel Directions
  • 30. Select
  • 31. Timeouts
  • 32. Non-Blocking Channel Operations
  • 33. Closing Channels
  • 34. Range over Channels
  • 35. Timers
  • 36. Tickers
  • 37. Worker Pools
  • 38. WaitGroups
  • 39. Rate Limiting
  • 40. Atomic Counters

21. Interfaces

下面的例子展示了用interface关键字定义接口。接口是一组方法签名的集合,用于实现多态。Go不像其他语言那样需要通过extend关键字显式指定类型的继承关系。如果一个struct类型实现了interface中定义的所有方法,那么就认为这个struct类型属于interface所定义的类型。传统的面向对象语言逻辑是:A is B if it’s a child of B;而Go的逻辑是:A is B if it acts like B。

package main

import (
    "fmt"
    "math"
)

// 定义了interface:几何体,以及方法:计算面积、计算周长
// 定义顺序:type、interface名、interface
type geometry interface {
    area() float64
    perim() float64
}

// 定义了struct:矩形和圆形,分别实现了geometry定义的所有接口
type rect struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

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

	// 矩形和圆形都可以作为几何体类型的参数传递
    measure(r)
    measure(c)
}
$ go run interfaces.go
{3 4}
12
14
{5}
78.53981633974483
31.41592653589793

22. Struct Embedding

下面的例子展示了结构体嵌套的做法。Go允许struct或者interface的嵌套使用,从而组合成更复杂的结构。这有点像Java中的外部类和内部类,外部结构和内部结构可以互相访问对方的字段和方法。这也可以看做是Go通过组合的方式来实现其他语言中的继承。

package main

import "fmt"

// 定义了基础类型
type base struct {
    num int
}

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

// 定义了包装类型
type container struct {
	// 包装类型中包含了基础类型
    base
    str string
}

func main() {

    co := container{
        base: base{
            num: 1,
        },
        str: "some name",
    }

	// num变量可以被直接或间接访问
    fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)

    fmt.Println("also num:", co.base.num)

    fmt.Println("describe:", co.describe())

    type describer interface {
        describe() string
    }

    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

23. Generics

泛型是Go 1.18版本引入的有争议但很有用的特性。泛型可以看做是类型参数,经常与map、slice等数据结构伴随使用。泛型用中括号表示,而不是其他语言中常用的尖括号。

package main

import "fmt"

// 泛型声明放在函数名后或者类型名后,可以为泛型参数指定要实现的接口名
func MapKeys[K comparable, V any](m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

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

func (lst *List[T]) GetAll() []T {
    var elems []T
    for e := lst.head; e != nil; e = e.next {
        elems = append(elems, e.val)
    }
    return elems
}

func main() {
    var m = map[int]string{1: "2", 2: "4", 4: "8"}

    fmt.Println("keys:", MapKeys(m))

    _ = MapKeys[int, string](m)

    lst := List[int]{}
    lst.Push(10)
    lst.Push(13)
    lst.Push(23)
    fmt.Println("list:", lst.GetAll())
}
$ go run generics.go
keys: [4 1 2]
list: [10 13 23]

24. Errors

Go提供了error和panic两种异常处理的方式。不同点在于,前者用于意料中的错误,通过函数返回值获取;后者用于意料外的严重错误,会中断程序执行,但可用recover捕捉恢复。Go建议对于一般错误,尽量使用error来处理。

package main

import (
    "errors"
    "fmt"
)

func f1(arg int) (int, error) {
    if arg == 42 {
		// 使用errors.New()生成新的error,并作为第二个值返回
        return -1, errors.New("can't work with 42")

    }

    return arg + 3, nil
}

type argError struct {
    arg  int
    prob string
}

func (e *argError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
    if arg == 42 {

        return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}

func main() {

    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    for _, i := range []int{7, 42} {
        if r, e := f2(i); e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }

    _, e := f2(42)
    if ae, ok := e.(*argError); ok {
        fmt.Println(ae.arg)
        fmt.Println(ae.prob)
    }
}
$ go run errors.go
f1 worked: 10
f1 failed: can't work with 42
f2 worked: 10
f2 failed: 42 - can't work with it
42
can't work with it

25. Goroutines

协程是Go的核心特性之一,它是一种轻量级的线程和任务调度机制,运行在操作系统级别的线程之上。它可以灵活地处理阻塞,从而减少大量异步代码的编写。我们可以开启大量的协程,而不用担心内存资源占用和线程上下文切换的代价。

package main

import (
    "fmt"
    "time"
)

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {

    f("direct")

	// 可以通过go执行函数来启动协程
    go f("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("going")

	// 注意协程不会阻止主线程的结束,即使协程还未执行完。所以这里需要sleep 1秒
    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

26. Channels

通道可用于在多个goroutine之间通信。发送方发送消息到通道中,接收方再从通道中接收消息。当使用非缓存通道时,发送和接收消息是同步操作,若有一方未完成都会造成对方阻塞。

package main

import "fmt"

func main() {
	// channel是引用类型,需要通过make创建
    messages := make(chan string)
	
	// 发送ping消息,并阻塞等待接收方
    go func() { messages <- "ping" }()
	// 接收ping消息,在接收到消息前阻塞
    msg := <-messages
    fmt.Println(msg)
}
$ go run channels.go 
ping

27. Channel Buffering

缓冲通道可以支持异步发送,无需等待接收方响应。在创建时可指定最大缓冲队列大小,若队列中消息长度超过最大值,则发送还是会被阻塞。接收方无论是哪种情况,只要没有消息可以接收,就一定会被阻塞。

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

28. Channel Synchronization

Go鼓励通过通信的方式(CSP模型)实现同步,而非传统的共享内存。下面的例子展示了通过将主线程阻塞在接收端,从而保证goroutine先于主线程执行完。

package main

import (
    "fmt"
    "time"
)

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)
	// 主线程阻塞在接收端,直至goroutine发送结束消息
    <-done
}
$ go run channel-synchronization.go      
working...done  

29. Channel Directions

通道在作为参数传递给函数时,可以指定通道的方向,函数内部只能按指定的方向发送或者接收数据。下面的例子展示了通过pings、pongs两个通道实现乒乓消息(先发送后回收)的效果。

package main

import "fmt"

// 先发送消息给pings
func ping(pings chan<- string, msg string) {
    pings <- msg
}

// 再从pings中取消息,发送给pongs
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)
    // 最后从pongs中取消息
    fmt.Println(<-pongs)
}
$ go run channel-directions.go
passed message

30. Select

通过select可以实现在多个channel上等待。用法有点类似于网络通信中的select模型,只要有任意一个通道接收到数据,就可以从select中接触阻塞并读取这次到达的数据。

package main

import (
    "fmt"
    "time"
)

func main() {

    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
    	// sleep 1秒,模拟耗时操作
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()
	// 循环两次,保证从两个channel都读到数据
    for i := 0; i < 2; i++ {
        select {
        // 用select结合case实现多通道等待
        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

31. Timeouts

超时可结合select使用,用于限制从channel获取消息的最大时间。

package main

import (
    "fmt"
    "time"
)

func main() {

    c1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c1 <- "result 1"
    }()

    select {
    case res := <-c1:
        fmt.Println(res)
    // 限制超时时间为1秒
    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

32. Non-Blocking Channel Operations

通常通道的发送和接收都是阻塞的。但是我们可以使用select和default来实现非阻塞操作,发送和接收都不再阻塞,而是在尝试失败后立即走default定义的默认逻辑。

package main

import "fmt"

func main() {
    messages := make(chan string)
    signals := make(chan bool)

    select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    // 没有收到消息,不会阻塞,而是走default逻辑
    default:
        fmt.Println("no message received")
    }

    msg := "hi"
    select {
    case messages <- msg:
        fmt.Println("sent message", msg)
    // 没有接收方接收消息,不会阻塞,而是走default逻辑
    default:
        fmt.Println("no message sent")
    }

    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

33. Closing Channels

关闭通道后,通道不再允许发送消息。这时接收方读取完通道中所有消息后,得到结束信号,做通信结束的后续操作。

package main

import "fmt"

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

    go func() {
        for {
        	// 关闭channel且读完通道中所有消息后,more取到false,结束通信
            j, more := <-jobs
            if more {
                fmt.Println("received job", j)
            } else {
                fmt.Println("received all jobs")
                done <- true
                return
            }
        }
    }()

    for j := 1; j <= 3; j++ {
        jobs <- j
        fmt.Println("sent job", j)
    }
    // 用close函数关闭channel
    close(jobs)
    fmt.Println("sent all jobs")

	// 用消息阻塞保证goroutine先于主线程执行完 
    <-done
}
$ 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

34. Range over Channels

使用range也可用于从通道接收消息,包括在已关闭的通道上。

package main

import "fmt"

func main() {

    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue)

	// 即使通道已关闭,仍然可以用range接收消息
    for elem := range queue {
        fmt.Println(elem)
    }
}
$ go run range-over-channels.go
one
two

35. Timers

定时器用于在将来的某个时间点执行代码。通过从通道接收到消息的方式表示达到触发时间点,接收到的消息为当前时间。

package main

import (
    "fmt"
    "time"
)

func main() {
	// 用time.NewTimer()定义2秒后的定时器
    timer1 := time.NewTimer(2 * time.Second)
	// 阻塞在这里,直到到达触发时间点
    <-timer1.C
    fmt.Println("Timer 1 fired")

    timer2 := time.NewTimer(time.Second)
    // 用goroutine的方式避免阻塞主线程
    go func() {
        <-timer2.C
        fmt.Println("Timer 2 fired")
    }()
    // 用Stop()停止定时器
    stop2 := timer2.Stop()
    if stop2 {
        fmt.Println("Timer 2 stopped")
    }

    time.Sleep(2 * time.Second)
}
$ go run timers.go
Timer 1 fired
Timer 2 stopped

36. Tickers

周期定时器用于在将来周期性地执行某个任务,直至我们让它停下。

package main

import (
    "fmt"
    "time"
)

func main() {
	// 定义了一个周期为500毫秒的周期定时器
    ticker := time.NewTicker(500 * time.Millisecond)
    done := make(chan bool)

    go func() {
        for {
            select {
            case <-done:
                return
            // 每间隔500毫秒从定时器通道接收到消息
            case t := <-ticker.C:
                fmt.Println("Tick at", t)
            }
        }
    }()

	// 1.6秒后停止定时器
    time.Sleep(1600 * time.Millisecond)
    ticker.Stop()
    done <- true
    fmt.Println("Ticker stopped")
}
$ go run tickers.go
Tick at 2012-09-23 11:29:56.487625 -0700 PDT
Tick at 2012-09-23 11:29:56.988063 -0700 PDT
Tick at 2012-09-23 11:29:57.488076 -0700 PDT
Ticker stopped

37. Worker Pools

工人池(协程池)用于使用固定数量的协程处理任务,防止协程数量太多。在部分特殊场景需要使用。协程池并非Go语言原生概念,而是基于已有语言特性搭建的开发模型。

package main

import (
    "fmt"
    "time"
)

// worker从jobs通道获取任务,并将执行结果发送到results通道
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        time.Sleep(time.Second)
        fmt.Println("worker", id, "finished job", j)
        results <- j * 2
    }
}

func main() {

    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
	// 开启worker数量为3的协程池
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
	// 将新任务添加到jobs通道中
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= numJobs; a++ {
        <-results
    }
}
$ time go run worker-pools.go 
worker 1 started  job 1
worker 2 started  job 2
worker 3 started  job 3
worker 1 finished job 1
worker 1 started  job 4
worker 2 finished job 2
worker 2 started  job 5
worker 3 finished job 3
worker 1 finished job 4
worker 2 finished job 5

	

real    0m2.358s

38. WaitGroups

前面的例子展示了如何用通道实现主线程和单个goroutine之间的等待。下面将会展示如何使用WaitGroup实现对多个goroutine的等待。

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int) {
    fmt.Printf("Worker %d starting\n", id)

    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {

    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
    	// 对每个goroutine,等待计数加1
        wg.Add(1)

        i := i
        go func() {
        	// 延迟执行,完成工作后,将等待计数减1
            defer wg.Done()
            worker(i)
        }()
    }
	// 当等待计数为0时,结束等待
    wg.Wait()

}
$ go run waitgroups.go
Worker 5 starting
Worker 3 starting
Worker 4 starting
Worker 1 starting
Worker 2 starting
Worker 4 done
Worker 1 done
Worker 2 done
Worker 5 done
Worker 3 done

39. Rate Limiting

速率限制是互联网和软件工程中的概念,用于防止对资源的使用超过一定限度。Go通过周期定时器和通道可以方便地实现这一功能。

package main

import (
    "fmt"
    "time"
)

func main() {

    requests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        requests <- i
    }
    close(requests)

    limiter := time.Tick(200 * time.Millisecond)

    for req := range requests {
    	// 每200毫秒结束阻塞,接收一次请求
        <-limiter
        fmt.Println("request", req, time.Now())
    }

    burstyLimiter := make(chan time.Time, 3)
	// 先允许执行三次
    for i := 0; i < 3; i++ {
        burstyLimiter <- time.Now()
    }
	// 然后每200毫秒允许执行一次
    go func() {
        for t := range time.Tick(200 * time.Millisecond) {
            burstyLimiter <- t
        }
    }()

    burstyRequests := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        burstyRequests <- i
    }
    close(burstyRequests)
    for req := range burstyRequests {
        <-burstyLimiter
        fmt.Println("request", req, time.Now())
    }
}
$ go run rate-limiting.go
request 1 2012-10-19 00:38:18.687438 +0000 UTC
request 2 2012-10-19 00:38:18.887471 +0000 UTC
request 3 2012-10-19 00:38:19.087238 +0000 UTC
request 4 2012-10-19 00:38:19.287338 +0000 UTC
request 5 2012-10-19 00:38:19.487331 +0000 UTC

request 1 2012-10-19 00:38:20.487578 +0000 UTC
request 2 2012-10-19 00:38:20.487645 +0000 UTC
request 3 2012-10-19 00:38:20.487676 +0000 UTC
request 4 2012-10-19 00:38:20.687483 +0000 UTC
request 5 2012-10-19 00:38:20.887542 +0000 UTC

40. Atomic Counters

Go中的同步方式除了通过channel通信,还可以通过原子计数器。通过原子技术器访问或者修改数值变量,可以让这些操作成为原子性的。

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {

    var ops uint64

    var wg sync.WaitGroup

    for i := 0; i < 50; i++ {
        wg.Add(1)

        go func() {
            for c := 0; c < 1000; c++ {
				// 通过AddUnit64实现原子性增加,若是原子读取可用LoadUint64,注意这里需要传指针参数 
                atomic.AddUint64(&ops, 1)
            }
            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println("ops:", ops)
}
$ go run atomic-counters.go
ops: 50000

相关文章:

  • 跟着实例学Go语言(二)
  • 树的递归算法与非递归(迭代)的转化重点理解代码(上篇)
  • OpenCV3图像处理笔记
  • 【专业术语】(计算机 / 深度学习与目标检测 / 轨道交通)
  • vue学习笔记——简单入门总结(四)
  • Allegro如何缩放数据操作指导
  • 【计算机视觉】图像形成与颜色
  • 资料库的webrtc文件传输
  • Java并发编程—线程详解
  • 家庭用户无线上网案例(AC通过三层口对AP进行管理)
  • (免费分享)基于springboot,vue公司财务系统
  • kubernetes-Service详解
  • 深度学习中的正则化——L1、L2 和 Dropout
  • 孪生神经网络
  • 微机-------CPU与外设之间的数据传送方式
  • BDD - SpecFlow ExternalData Plugin 导入外部测试数据
  • 图像处理:模糊图像判断
  • About 12.4 This Week
  • qmake source code 解读
  • ES6的symbol及es2021