Go 1.18 新特性多模块工作区教程
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 1.18 新特性多模块工作区教程相关的知识,希望对你有一定的参考价值。
参考技术A• 随着 2022 年 3 月 15 日 go 1.18 正式发布,新版本除了对性能的提升之外,还引入了很多新功能,其中就有 go 期盼已久的功能泛型(Generics),同时还引入的多模块工作区(Workspaces)和模糊测试(Fuzzing)。
• 关于泛型网上已经有很多介绍的教程了,这里我介绍一个实用的功能,多模块工作区的使用方法和教程。
• Go 多模块工作区能够使开发者能够更容易地同时处理多个模块的工作,如:
• 多模块工作区
• 开发流程演示
• 总结
• 参考文献
• go 使用的是多模块工作区,可以让开发者更容易同时处理多个模块的开发。在 Go 1.17 之前,只能使用 go.mod replace 指令来实现,如果你正巧是同时进行多个模块的开发,使用它可能是很痛苦的。每次当你想要提交代码的时候,都不得不删除掉 go.mod 中的 replace 才能使模块稳定的发布版本。 •在使用 go 1.18 多模块工作区功能的时候,就使用这项工作变得简单容易处理。下面我来介绍怎么使用这一功能。• Go 多模块工作区文档、代码示例[5]
• 首先 我们需要 go 1.18 或更高版本 go 安装[6]
• 通常情况下,建议不要提交 go.work 文件到 git 上,因为它主要用于本地代码开发。
• 推荐在: $GOPATH 路径下执行,生成 go.work 文件
• go work init 初始化工作区文件,用于生成 go.work 工作区文件
• go work use 添加新的模块到工作区
• go work edit 用于编辑 go.work 文件
• go work sync 将工作区的构建列表同步到工作区的模块
• go env GOWORK
• 文件结构和 go.mod 文件结构类似,支持 Go 版本号、指定工作区和需要替换的仓库 •文件结构示例:
• 可以使用 go work use hello 添加模块,也可以手动修改 go.work 工作区添加新的模块 •在工作区中添加了模块路径,编译的时候会自动使用 use 中的本地代码进行代码编译,和 replaces 功能类似。
• replaces 命令与 go.mod 指令相同,用于替换项目中依赖的仓库地址 •需要注意的是 replaces 和 use 不能同时指定相同的本地路径
• 错误示例
• 在同时使用 go.work 和 go.mod replace 功能的的时候分别指定不同的代码仓库路径, go.work 优先级高于 go.mod 中定义
• 在代码构建时候使用的是 go.work 指定的 example1 仓库的代码, go.work 优先级别更高
• 在 Go 1.18 go run 和 go build 都会默认使用工作区功能 • GOWORK 也可以指定配置 go.work 文件位置
• Go 全局变量 GOWORK 设置 off 则可以禁用工作区功能
• 演示如何使用多模块工作区功能。在现在微服务盛行的年代,一个人会维护多个代码仓库,很多的时候是多个仓库进行同时开发
• 假设我们现在进行 hello 仓库开发,实现的功能是,实现将输入的字符串反转并输出,字符串反转功能依赖于 github.com/link1st/example (下文统称 example )公共仓库实现
• 新建 hello 项目
• main.go 代码
• 运行代码 go run main.go -str "hello world" 或 go run github.com/link1st/link1st/workspaces/hello -str "hello world" 可以看到输出了 hello world 反转以后的字符串
• 到这里,最初的功能已经完成,但是后续需求变动,不仅需要输出反转以后的字符串,还需要将字符串大写
• 我们则需要去 example 仓库中添加开发 将字符串大写的功能
• vim example/stringutil/to_upper.go 代码如下
• 由于代码还在本地调试,未提交 git 仓库中,这个时候就需要用到 Go 多模块工作区的功能了。
• 进入项目根目录,初始化我们现在正在开发的模块
• 文件结构如下
• 回到 hello 项目, vim main.go 将字符串大写的功能添加上。
• 运行代码
• 到这里,演示的代码已经全部完成
• 使用 Go 多模块工作区的功能,可以让我们轻松在多个模块之间切换工作,更能适应现代微服务架构开发。
[1] Go 1.18 新特性多模块工作区教程: https://github.com/link1st/link1st/tree/master/workspaces
[2] Go 1.18 is released!: https://go.dev/blog/go1.18
[3] Tutorial: Getting started with multi-module workspaces: https://go.dev/doc/tutorial/workspaces
[4] go-1.18-features: https://sebastian-holstein.de/post/2021-11-08-go-1.18-features/
Go教程(十三)goroutine和channel
一次只做一件事情并不是完成任务最快的方法.一些大的任务可以拆解成若干个小任务.goroutine可以让程序同时处理几个不同的任务.goroutine使用channel来协调它们的工作.channel允许goroutine互相发送数据并同步.这样一个goroutine就不会领先于另一个goroutine.它允许我们充分利用具有多处理器的计算机,让程序运行得尽可能的快.
多任务
我们有a.txt,b.txt,c.txt三个文件,我们需要读取它们的内容至内存,然后再计算它们的大小
package main
import (
"bufio"
"fmt"
"os"
"time"
)
func GetFileLen(name string) (length uint64, err error)
var filelength uint64
filelength = 0
fmt.Println("Opening name", name)
file, err := os.OpenFile(name, os.O_RDONLY, os.FileMode(0600))
if err != nil
return filelength, err
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan()
filelength += uint64(len(scanner.Text()))
fmt.Println(name, " length ", filelength)
return filelength, nil
func main()
PrintTime()
l1, _ := GetFileLen("a.txt") // 1.6G
l2, _ := GetFileLen("b.txt") // 1.6G
l3, _ := GetFileLen("c.txt") // 1.6G
fmt.Println(l1, l2, l3)
PrintTime()
func PrintTime()
t := time.Now().Unix()
fmt.Println(t)
结果如下
PS D:\\goproj\\routine> go run .\\main.go
1648263533
Opening name a.txt
a.txt length 1861221360
Opening name b.txt
b.txt length 1861221360
Opening name c.txt
c.txt length 1861221360
1861221360 1861221360 1861221360
1648263542
如上所知,我们完成所有文件的读取需要9秒钟的时间
如图所示,现在的执行顺序是这样子的
如果读取文件的任务能同时运行,那么整个任务可以在更短的时间内完成.如下图所示
利用goroutine的并发性
当我们调用GetFileLen
时,程序必须在那里等待文件打开并读取内容,这个时候其实程序本身是不作任何事情的.但是由于程序是阻塞的,所以它卡在原地了.
并发性允许程序暂停一个任务并处理其他的任务.等待用户输入的程序可能在后台执行其他处理.程序在读取文件同时更新进度条.
支持并行运行的程序能够同时运行多个任务.一台只有一个处理器的计算机同一时刻只能处理一个任务.但是现在大多数计算机都是多核的.计算机由操作系统负责在不同的处理器之间分配.
在Go中,并发任务被称为goroutine,其他编程语言中这个相同的概念叫线程.但是goroutine比线程占用更少的内存.启动和停止的时间更少,这也就是意味着在同样硬件的情况下,可以运行更多的goroutine.
启动另一个goroutine非常简单,它只要在函数前面加go关键字.
go GetFileLen("a.txt")
每个go的main函数也是一个goroutine,所以在main中启动的任何一个goroutine都是第2个.goroutine允许并发:暂停一个任务来处理其他的任务,也允许多个任务同时执行.
调用 goroutine
func main()
PrintTime()
// l1, _ := GetFileLen("a.txt")
// l2, _ := GetFileLen("b.txt")
// l3, _ := GetFileLen("c.txt")
// fmt.Println(l1, l2, l3)
go GetFileLen("a.txt")
go GetFileLen("b.txt")
go GetFileLen("c.txt")
time.Sleep(5 * time.Second) // 一定要加上,因为不加程序将直接退出.
PrintTime()
输出结果如下
1648290898
Opening name c.txt
Opening name a.txt
Opening name b.txt
c.txt length 1861221360
a.txt length 1861221360
b.txt length 1861221360
1648290903
从上面的结果我们看出来.我们调用的顺序是a b c,但是出来的顺序却是c a b.这由于我们无法决定哪个goroutine先运行哪个后运行.完全取决于go自身的调度机制.
如下图所示
由于main goroutine的运行时间要远低于其他goroutine的运行时间,所以如果不加以控制,
main goroutine完成之后就会退出,而其它的goroutine输出的结果也就看不到了.这也是为什么要加Sleep的原因所在.
我们也可以把时间改成time.Sleep(4 * time.Second),就是4秒钟,如果改成2秒或者1秒,是看不到输出的.因为读取文件大概需要3秒钟.所以用休眠的方式并不好,因为不同计算硬件对读取大文件的操作时间是不确定的,说不定在一些老旧的电脑上读取这些文件需要30秒.因此我们需要用更精确的办法来控制.
goroutine是不允许有返回值的.这实际上是正确的.由于代码在goroutine中运行,所以你不能指望它以函数的形式马上给你返回值,因为什么时候返回到调用它的goroutine是不确定的,所以它不能保证返回值什么时候准备好.
使用channel发送和接收值
goroutine之间的通讯方式被称为chnnel.它不仅允许你将一个值从一个goroutine发送给另一个goroutine并且保证了在接收该值的goroutine使用这个值之前,它一定是被发送过来的.
它实际上是解决了goroutine没有返回值的问题.channel声明如下
var myChannel chan uint64 // 声明channel变量
myChannel = make(chan uint64) // 创建channel变量
myChannel1 := make(chan uint64) // 短变量声明
myChannel <- 20 // 发送值给channel
var u uint64
u <- myChannel // 从channel中接收值
我们改造一下GetFileLen
使它接收channel,然后再在main goroutine中接收这个值
func GetFileLen(name string, c chan uint64) (length uint64, err error)
var filelength uint64
filelength = 0
fmt.Println("Opening name", name)
file, err := os.OpenFile(name, os.O_RDONLY, os.FileMode(0600))
if err != nil
return filelength, err
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan()
filelength += uint64(len(scanner.Text()))
fmt.Println(name, " length ", filelength)
c <- filelength // 发送值给channel
return filelength, nil
func main()
PrintTime()
// 创建channel
c := make(chan uint64)
d := make(chan uint64)
e := make(chan uint64)
go GetFileLen("a.txt", c)
go GetFileLen("b.txt", d)
go GetFileLen("c.txt", e)
fmt.Println(<-c, <-d, <-e) // 接收channel的值
// time.Sleep(4 * time.Second)
PrintTime()
结果如下
1648297266
Opening name c.txt
Opening name b.txt
Opening name a.txt
b.txt length 1861221360
c.txt length 1861221360
a.txt length 1861221360
1861221360 1861221360 1861221360
1648297269
可以看到,这次用的时间是3秒.所有的值都求出来了,理论上来讲.即使再有多个GetFileLen也是3秒会完成.
使用goroutine同步
channel在接收值的时候会阻塞当前的上下文,等待接收端goroutine的处理.利用这个特性,我们可以尝试修改一下程序,让它按照我们的意图在接收端输出受控制的顺序
func GetFileLen(name string, c chan string) (length uint64, err error)
var filelength uint64
filelength = 0
fmt.Println("Opening name", name)
file, err := os.OpenFile(name, os.O_RDONLY, os.FileMode(0600))
if err != nil
return filelength, err
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan()
filelength += uint64(len(scanner.Text()))
// fmt.Println(name, " length ", filelength)
// c <- filelength // 发送值给channel
// 发送值给channel
c <- fmt.Sprintf("%s length %s", name, strconv.FormatUint(filelength, 10))
// fmt.Println(S)
return filelength, nil
func main()
PrintTime()
// l1, _ := GetFileLen("a.txt")
// l2, _ := GetFileLen("b.txt")
// l3, _ := GetFileLen("c.txt")
// fmt.Println(l1, l2, l3)
c := make(chan string)
d := make(chan string)
e := make(chan string)
go GetFileLen("a.txt", c)
go GetFileLen("b.txt", d)
go GetFileLen("c.txt", e)
fmt.Println(fmt.Sprint("receive : ", <-c))
fmt.Println(fmt.Sprint("receive : ", <-d))
fmt.Println(fmt.Sprint("receive : ", <-e))
// time.Sleep(4 * time.Second)
PrintTime()
运行的结果如下,现在无论运行多少次,receive 的顺序都不变了,但是Opening的顺序是会变的
1648299196
Opening name a.txt
Opening name c.txt
Opening name b.txt
receive : a.txt length 1861221360
receive : b.txt length 1861221360
receive : c.txt length 1861221360
1648299199
1648299504
Opening name c.txt
Opening name a.txt
Opening name b.txt
receive : a.txt length 1861221360
receive : b.txt length 1861221360
receive : c.txt length 1861221360
1648299508
大家可以调整一下c d e 的顺序,调整以后输出a b c的顺序也会跟着变化.这说明在goroutine的接收方顺序是可以同步的,但是调用goroutine的顺序是无法控制的
- go不保证何时在goroutine之间切换,也不保证它将持续运行一个goroutine多久,这使得goroutine更高效的运行.
- goroutine通过channel通讯和同步或暂停其他goroutine
- 默认情况下,在goroutine上向channel发送值时,会暂停当前的goroutine,直到接收到该值为止.
- 接收channel的值也会导致当前goroutine阻塞,直到这个值被发送到那个channel为止.
以上是关于Go 1.18 新特性多模块工作区教程的主要内容,如果未能解决你的问题,请参考以下文章