本教程全面涵盖了Go语言基础的各个方面。一共80个例子,每个例子对应一个语言特性点,既适合新人快速上手,也适合工作中遇到问题速查知识点。
教程代码示例来自go by example,文字部分来自本人自己的理解。

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

系列文章快速跳转:
跟着实例学Go语言(一)
跟着实例学Go语言(二)
跟着实例学Go语言(三)
跟着实例学Go语言(四)

目录

  • 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 mainimport ("fmt""math"
)// 定义了interface:几何体,以及方法:计算面积、计算周长
// 定义顺序:type、interface名、interface
type geometry interface {area() float64perim() 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 mainimport "fmt"// 定义了基础类型
type base struct {num int
}func (b base) describe() string {return fmt.Sprintf("base with num=%v", b.num)
}// 定义了包装类型
type container struct {// 包装类型中包含了基础类型basestr 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 = cofmt.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 mainimport "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 []Tfor 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 mainimport ("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  intprob 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 mainimport ("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 mainimport "fmt"func main() {// channel是引用类型,需要通过make创建messages := make(chan string)// 发送ping消息,并阻塞等待接收方go func() { messages <- "ping" }()// 接收ping消息,在接收到消息前阻塞msg := <-messagesfmt.Println(msg)
}
$ go run channels.go
ping

27. Channel Buffering

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

package mainimport "fmt"func main() {// 指定缓冲队列最大长度为2messages := 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 mainimport ("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 mainimport "fmt"// 先发送消息给pings
func ping(pings chan<- string, msg string) {pings <- msg
}// 再从pings中取消息,发送给pongs
func pong(pings <-chan string, pongs chan<- string) {msg := <-pingspongs <- 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 mainimport ("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 tworeal    0m2.245s

31. Timeouts

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

package mainimport ("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 mainimport "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 mainimport "fmt"func main() {jobs := make(chan int, 5)done := make(chan bool)go func() {for {// 关闭channel且读完通道中所有消息后,more取到false,结束通信j, more := <-jobsif more {fmt.Println("received job", j)} else {fmt.Println("received all jobs")done <- truereturn}}}()for j := 1; j <= 3; j++ {jobs <- jfmt.Println("sent job", j)}// 用close函数关闭channelclose(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 mainimport "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 mainimport ("fmt""time"
)func main() {// 用time.NewTimer()定义2秒后的定时器timer1 := time.NewTimer(2 * time.Second)// 阻塞在这里,直到到达触发时间点<-timer1.Cfmt.Println("Timer 1 fired")timer2 := time.NewTimer(time.Second)// 用goroutine的方式避免阻塞主线程go func() {<-timer2.Cfmt.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 mainimport ("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 <- truefmt.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 mainimport ("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 = 5jobs := 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 5real    0m2.358s

38. WaitGroups

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

package mainimport ("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.WaitGroupfor i := 1; i <= 5; i++ {// 对每个goroutine,等待计数加1wg.Add(1)i := igo func() {// 延迟执行,完成工作后,将等待计数减1defer 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 mainimport ("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毫秒结束阻塞,接收一次请求<-limiterfmt.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 {<-burstyLimiterfmt.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 UTCrequest 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 mainimport ("fmt""sync""sync/atomic"
)func main() {var ops uint64var wg sync.WaitGroupfor 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语言(二)相关推荐

  1. 跟着google工程师学Go语言(二十四):单任务版爬虫

    欢迎来到:Google资深工程师深度讲解Go语言 视频地址:Google资深工程师深度讲解Go语言-单任务版爬虫 获取城市名称和链接: CSS选择器 浏览器,console: $('#cityList ...

  2. 跟vczh看实例学编译原理——二:实现Tinymoe的词法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe. 实现Tinymoe的第一步自然是一个词法分析器.词法分析其所作的事情很简单,就是把一份代码分割成若干个token ...

  3. c语言输入字符串_我们一起学C语言(四)

    C语言来喽~ 每日一句 我关心我自己, 愈是孤单, 愈是没有朋友, 愈是无助, 那我就愈是自尊. --<简爱> 表达式 在上一篇中,我们已经学习了运算符,接下来我们来看如何运用这些运算符写 ...

  4. ZYNQ7000 学习(二十八)C语言二维数组映射到显示器的原理分析以及实现实例 学

    C语言二维数组映射到显示器的原理分析以及实现实例 学习内容 本课将 在上一课的基础上 修改一下 AXI_LITE_SLAVE外设,不再使用寄存 器而直接对 VGA显存里的数据进行进行写操作,达到以数组 ...

  5. 想学C语言,跟着一个大佬一步步来,后来点错了一步,就一步错,步步错了。该怎么办呢?

    大一.大二的新生怎么学C语言呢?作为学长,我简单把我的想法说一下吧. 1. 自学才是硬道理.强大的自学能力是独自解决问题能力的根本,程序员需要拥有强大的独自解决问题的能力. 2. 入门阶段,codin ...

  6. “跟着菜鸟一起学R语言” 现已更名为“数据志”

    大家好,我的公众号"跟着菜鸟一起学R语言" 现已更名为"数据志",欢迎大家关注,谢谢.

  7. 二叉排序树查找的c语言程序,C语言二叉排序(搜索)树实例

    本文实例为大家分享了C语言二叉排序(搜索)树实例代码,供大家参考,具体内容如下 /**1.实现了递归 非递归插入(创建)二叉排序(搜索)树: 分别对应Insert_BinSNode(TBinSNode ...

  8. c语言统计二维数组中数字出现次数,C语言二维数组中的查找的实例

    C语言二维数组中的查找的实例 题目描述:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该 ...

  9. 从0开始学c语言-总结04-一维、二维数组简单汇总

    数组算是我们比较常用的知识,而对于数组的运用,如若和指针结合便会显得十分有趣.总结专栏不会详细解释为什么.(其实单纯看数组并看不出什么知识,这里总结的只是最基本用法和了解.)相关文章链接放在最后(被汇 ...

最新文章

  1. python字符串出栈方法_python字符串常用方法
  2. Linux 小知识翻译 - 「代理服务器」
  3. Quartz Java resuming a job excecutes it many times--转
  4. python超市管理系统_控制台超市系统(Python)
  5. MyBatis --教程
  6. django开发商城(提供初始数据,商城首页及购物车)
  7. logstash的output插件
  8. Clipsync – 同步 Win 和 Android 剪贴板
  9. 从研发角度谈存储技术的学习
  10. 1.软件架构设计:大型网站技术架构与业务架构融合之道 --- 五花八门的架构师职业
  11. excel基础知识大全_测量常用软件大全
  12. 心理学与生活 - 发展与教育
  13. 【环境安装】Ubuntu20.04 安装yasm-1.3.0
  14. 如何使用AI绘制网格花卉?
  15. Xmind基础教程-图标
  16. 用HTML+CSS简单做了张简历表格
  17. PyCharm关闭拼写检查(Typo提示)
  18. pythonapi_Python API
  19. 最清晰!一篇文章读懂 OceanBase 最新的产品家族
  20. web期末大作业 用HTML+CSS做一个漂亮简单的节日网页【传日文化节日中秋节】

热门文章

  1. Android 农历和节气相关工具类(记录)
  2. node 下载Url上的压缩包 解压并保存文件夹到本地
  3. 直接java调用tflite_Tensorflow Lite介绍
  4. php按中文排序,php按照中文首字母排序
  5. Word文档如何自动生成目录
  6. linux(centerOS6.5)安装zookeeper
  7. 参加ScrumMaster认证(CSM)心得
  8. 漫画编程java_【漫画】JAVA并发编程之并发模拟工具
  9. Polyspace的模块介绍
  10. Play a game(博弈)