Go协程和channel的实现

 更新时间:2026年01月28日 11:26:22   作者:JetTsang  
本文主要介绍了GO利用channel协调协程的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一:🔥 协程

Goroutine 是 Go 运行时管理的轻量级线程

在 go 中,开启一个协程是非常简单的

package main

import (
  "fmt"
  "time"
)

func sing() {
  fmt.Println("唱歌")
  time.Sleep(1 * time.Second)
  fmt.Println("唱歌结束")
}

func main() {
  go sing()
  go sing()
  go sing()
  go sing()
  time.Sleep(2 * time.Second)
}

如果我把这个主线程中的延时去掉之后,你会发现程序没有任何输出就结束了

这是为什么呢

那是因为主线程结束协程自动结束,主线程不会等待协程的结束

🦋 WaitGroup

我们只需要让主线程等待协程就可以了,它的用法是这样的

package main

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

var (
  wait = sync.WaitGroup{}
)

func sing() {
  fmt.Println("唱歌")
  time.Sleep(1 * time.Second)
  fmt.Println("唱歌结束")
  wait.Done()
}

func main() {
  wait.Add(4)
  go sing()
  go sing()
  go sing()
  go sing()
  wait.Wait()
  fmt.Println("主线程结束")
}

二:🔥 channel

有没有想过一个问题,我在协程里面产生了数据,咋传递给主线程呢?

或者是怎么传递给其他协程函数呢?

这个时候 channel 来了

基本定义

package main

import "fmt"

func main() {
  var c chan int // 声明一个传递整形的通道
  // 初始化通道
  c = make(chan int, 1) //  初始化一个 有一个缓冲位的通道
  c <- 1
  //c <- 2 // 会报错 deadlock
  fmt.Println(<-c) // 取值
  //fmt.Println(<-c) // 再取也会报错  deadlock

  c <- 2
  n, ok := <-c
  fmt.Println(n, ok)
  close(c) // 关闭协程
  c <- 3   // 关闭之后就不能再写或读了  send on closed channel
  fmt.Println(c)
}

当然,在同步模式下,channel 没有任何意义

需要在异步模式下使用 channel,在协程函数里面写,在主线程里面接收数据

package main

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

var moneyChan = make(chan int) // 声明并初始化一个长度为0的信道

func pay(name string, money int, wait *sync.WaitGroup) {
  fmt.Printf("%s 开始购物\n", name)
  time.Sleep(1 * time.Second)
  fmt.Printf("%s 购物结束\n", name)

  moneyChan <- money

  wait.Done()
}

// 协程
func main() {
  var wait sync.WaitGroup
  startTime := time.Now()
  // 现在的模式,就是购物接力
  //shopping("张三")
  //shopping("王五")
  //shopping("李四")
  wait.Add(3)
  // 主线程结束,协程函数跟着结束
  go pay("张三", 2, &wait)
  go pay("王五", 3, &wait)
  go pay("李四", 5, &wait)

  go func() {
    defer close(moneyChan)
    // 在协程函数里面等待上面三个协程函数结束
    wait.Wait()
  }()

  for {
    money, ok := <-moneyChan
    fmt.Println(money, ok)
    if !ok {
      break
    }
  }

  //time.Sleep(2 * time.Second)

  fmt.Println("购买完成", time.Since(startTime))
  fmt.Println("moneyList", moneyList)
}

如果这样接收数据不太优雅,那还有更优雅的写法

  for money := range moneyChan {
    moneyList = append(moneyList, money)
  }

如果通道被 close,for 循环会自己结束,十分优雅

🦋 select

如果一个协程函数,往多个 channel 里面写东西,在主线程里面怎么拿数据呢?

go 为我们提供了 select,用于异步的从多个 channel 里面去取数据

package main

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

// 信道 存 int 类型
var moneyChan1 = make(chan int) // 声明并初始化一个长度为0的信道
var nameChan1 = make(chan string)
var doneChan = make(chan struct{})

func send(name string, money int, wait *sync.WaitGroup) {
	fmt.Printf("%s 开始购物\n", name)
	time.Sleep(1 * time.Second)
	fmt.Printf("%s 购物结束\n", name)

	moneyChan1 <- money // 信道赋值语句
	nameChan1 <- name

	wait.Done()
}

func main() {
	var wait sync.WaitGroup

	startTime := time.Now()
	wait.Add(3)
	// 协程
	go send("zhangsan", 2, &wait)
	go send("lisi", 3, &wait)
	go send("wangwu", 5, &wait)

	// 再开一个协程函数判断是否结束
	go func() {
		defer close(moneyChan1)
		defer close(nameChan1)
		defer close(doneChan)
		wait.Wait()
		// 再创建一个信道用于关闭
		// close(moneyChan)
	}()

	// 等价于下面的写法
	var moneyList []int
	var nameList []string

	// 多个 channel 的写法
	var event = func() {
		for {
			select {
			case money := <-moneyChan1:
				moneyList = append(moneyList, money)
			case name := <-nameChan1:
				nameList = append(nameList, name)
			case <-doneChan:
				return
			}
		}
	}
	event()

	fmt.Println("购买完成", time.Since(startTime))
	fmt.Println("moneyList", moneyList)
	fmt.Println("nameList", nameList)
}

🦋 协程超时处理

package main

import (
  "fmt"
  "time"
)

var done = make(chan struct{})

func event() {
  fmt.Println("event执行开始")
  time.Sleep(2 * time.Second)
  fmt.Println("event执行结束")
  close(done)
}

func main() {
  go event()

  select {
  case <-done:
    fmt.Println("协程执行完毕")
  case <-time.After(1 * time.Second):
    fmt.Println("超时")
    return
  }

}

三:🔥 线程安全

什么是线程安全?

现在有两个协程,同时触发,一个协程对一个全局变量进行 100 完成 ++ 操作,另一个对全局变量—的操作

那么,两个协程结束,最后的值应该是0才对

package main

import (
  "fmt"
  "sync"
)

var num int
var wait sync.WaitGroup

func add() {
  for i := 0; i < 1000000; i++ {
    num++
  }
  wait.Done()
}
func reduce() {
  for i := 0; i < 1000000; i++ {
    num--
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go add()
  go reduce()
  wait.Wait()
  fmt.Println(num)

}

但是你会发现,这个输出的结果完全无法预测

这是为什么呢?

根本原因是 CPU 的调度方法为抢占式执行,随机调度

🦋 同步锁

那么我们能不能通过给操作加锁来解决这个问题呢

答案是可以的

package main

import (
  "fmt"
  "sync"
)

var num int
var wait  sync.WaitGroup
var lock  sync.Mutex

func add() {
  // 谁先抢到了这把锁,谁就把它锁上,一旦锁上,其他的线程就只能等着
  lock.Lock()
  for i := 0; i < 1000000; i++ {
    num++
  }
  lock.Unlock()
  wait.Done()
}
func reduce() {
  lock.Lock()
  for i := 0; i < 1000000; i++ {
    num--
  }
  lock.Unlock()
  wait.Done()
}

func main() {
  wait.Add(2)
  go add()
  go reduce()
  wait.Wait()
  fmt.Println(num)

}

🦋 线程安全下的 map

如果我们在一个协程函数下,读写 map 就会引发一个错误

concurrent map read and map write

希望大家见到这个错误,就能知道,这个就是 map 的线程安全错误

package main

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

var wait sync.WaitGroup
var mp = map[string]string{}

func reader() {
  for {
    fmt.Println(mp["time"])
  }
  wait.Done()
}
func writer() {
  for {
    mp["time"] = time.Now().Format("15:04:05")
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go reader()
  go writer()
  wait.Wait()

}

我们不能在并发模式下读写 map

如果要这样做

  1. 给读写操作加锁
  2. 使用sync.Map

加锁

package main

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

var wait sync.WaitGroup
var mp = map[string]string{}
var lock sync.Mutex

func reader() {
  for {
    lock.Lock()
    fmt.Println(mp["time"])
    lock.Unlock()
  }
  wait.Done()
}
func writer() {
  for {
    lock.Lock()
    mp["time"] = time.Now().Format("15:04:05")
    lock.Unlock()
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go reader()
  go writer()
  wait.Wait()
}

sync.Map

package main

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

var wait sync.WaitGroup
var mp = sync.Map{}

func reader() {
  for {

    fmt.Println(mp.Load("time"))
  }
  wait.Done()
}
func writer() {
  for {
    mp.Store("time", time.Now().Format("15:04:05"))
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go reader()
  go writer()
  wait.Wait()

}

其实看它源码,它的内部也是用了同步锁的

到此这篇关于Go协程和channel的实现的文章就介绍到这了,更多相关Go协程和channel内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang中字符串(string)与字节数组([]byte)一行代码互转实例

    Golang中字符串(string)与字节数组([]byte)一行代码互转实例

    golang语言本身就是c的工具集,开发c的程序用到的大部分结构体,内存管理,携程等,golang基本都有,下面这篇文章主要给大家介绍了关于Golang中字符串(string)与字节数组([]byte)一行代码互转的相关资料,需要的朋友可以参考下
    2022-09-09
  • Golang 之协程的用法讲解

    Golang 之协程的用法讲解

    这篇文章主要介绍了Golang 之协程的用法讲解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go使用WebSocket实现一个公域聊天室

    Go使用WebSocket实现一个公域聊天室

    公域聊天室是一种实时通信服务,所有用户连接到同一个公共房间,本文下面就介绍一下Go使用WebSocket实现一个公域聊天室,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-03-03
  • Go实现简单的数据库表转结构体详解

    Go实现简单的数据库表转结构体详解

    这篇文章主要为大家介绍了Go实现简单的数据库表转结构体详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • gin 框架下使用URL重定向

    gin 框架下使用URL重定向

    本文主要介绍了gin 框架下使用URL重定向,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-05-05
  • 详解Golang如何优雅判断interface是否为nil

    详解Golang如何优雅判断interface是否为nil

    这篇文章主要为大家详细介绍了Golang如何优雅判断interface是否为nil的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起了解下
    2024-01-01
  • go语言int64整型转字符串的实现

    go语言int64整型转字符串的实现

    本文主要介绍了go语言int64整型转字符串的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • golang的序列化与反序列化的几种方式

    golang的序列化与反序列化的几种方式

    这篇文章主要介绍了golang的序列化与反序列化的几种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • 手把手带你走进Go语言之类型转换

    手把手带你走进Go语言之类型转换

    每个函数都可以强制将一个表达式转换成某种特定数据类型,本文给大家介绍了在Go语言中类型转换的具体用法,讲述的非常详细,对大家的学习或工作具有一定的参考借鉴价值
    2021-09-09
  • go如何使用cobra启动项目

    go如何使用cobra启动项目

    本文介绍了如何使用Go语言的Cobra库来启动项目,包括基本使用和项目开发中的具体实现,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-11-11

最新评论