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 新特性多模块工作区教程的主要内容,如果未能解决你的问题,请参考以下文章

Go 1.18 版本新特性详解!

Go 1.18 版本新特性详解!

Go官方 - 1.18 发布说明

分享 2 个 Go1.18 新特性的官方教程

golang 发布 1.11,带来新特性 modules

Go 1.18 终于来了!