Golang进阶知识
Goroutine
go命令启动的是协程,一个进程里面可以有多个线程,一个线程里面可以有多个协程。我的理解是线程就是依赖操作系统管理,开销较大。协程是让用户这边管理,在一个线程内主动切换任务,开销较小,但是管理起来比较麻烦,编程语言需要花不少功夫为我们做实现(比如协程的管理,以及跨平台的支持)。
我记得Java以前有过绿色线程,然后后面还是改成了本地线程。JDK21又带来了虚拟线程--这种由JVM而不是OS管理的线程。不管Java是懒得弄还是确实不太好实现,go实现了goroutine确实让go具有卖点。
在他的使用上,其实和线程差不多,直接就是go func。go后面可以跟一个已经定义好的函数,也可以当场定义一个匿名函数然后跑它。
package main
import (
"fmt"
"time"
)
func hello(i int){
fmt.Println("hello " + fmt.Sprint(i))
}
func HelloGoRoutine(){
for i := 0; i < 5; i++ {
go func(j int){
hello(j)
}(i)
}
time.Sleep(time.Second)
}
func main() {
HelloGoRoutine();
}
输出如下,可以看到是没有顺序的,几个协程并发执行。
hello 2
hello 1
hello 3
hello 4
hello 0
channel
对于线程通信,Golang采用了channel来进行数据的发送和接收。
在下面的例子中,第一个线程向src中传入i,另一个线程从src中取出i,然后将 传入dest,第三个线程从dest中取出并打印i。整个流程的数据传递简洁清晰。
package main
func main() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
}
这个例子可以看到,channel的创建使用chan := make(chan 数据类型)创建,再多的一个参数是这个channel的缓冲区大小,即可以临时放多少个数据。如果无缓冲,那如果chan里的数据没有被消费,sender这边会阻塞,而有缓冲就是sender还可以继续发,直到缓冲区满才也会阻塞。
缓冲区是用来控制两端速度的,因为一般业务中生产者只需要产生一个组数据,这个操作离散、间断而简单、速度快。而消费者需要用这个数据做各种复杂操作,因此消费速度会低于生产速度。缓冲区可以给消费者一定的时间去消费。
对于chan的读取,例子采用了左指向的箭头表示数据的写入,用range来读取chan里的值,在这个for循环中,当chan被关闭并且所有数据都已被接收后,循环会自动终止。这里用range表示的很直观,就好像在持续监听一个管道的出水口一样,实际上也可以用左箭头,即下面这种形式进行一次读取,ok是可以看到channel的情况。
value, ok := <-ch
if !ok {
fmt.Println("Channel is closed and no more values to receive")
}
所以chan的关闭也是一个很重要的事。例子中采用了defer close(channel)的形式,defer也是go一个很有用的关键字,可以在函数结束之前调用尾随的操作,适合做一些资源的回收销毁。对于管道资源的显式关闭,可以回收资源,同时也能作为一种信号,比如sender关闭,就会通知receiver结束监听。不论是代码语义上还是程序安全上这都是很重要的。
锁
在被java的一堆种类繁多、定义繁杂的锁折磨之后,我看到了golang中简洁易懂的锁。其实这个锁也不小,就是在临界区开头锁住,在结束之后解锁让各线程争夺。除此之外保证线程安全的还有读写锁sync.RWMutex、原子变量、条件变量等。
var(
lock sync.Mutex
)
lock.Lock()
doSomething()
lock.Unlock()
抛开这些,如果你写的程序在线程安全方面有问题,go提供了竞态检测工具,只需要go run -race main.go,就可以在运行时检测程序是否有线程安全问题,非常好用!
线程屏障
喜欢多线程开发的开发人员,肯定需要让一个或者一组同时start的线程,能被程序在一个地方拦住,然后接下来的流程变回串行。比如耳熟能详的pthread_barrier、await、join等。在golang中,我们要使用WaitGroup。
其实用法和pthread_barrier差不多,就是在线程外管理一个线程共享的计数器,初始先定好计数器大小为线程大小,当前线程结束后调用Done()将计数器-1,同时等待其他线程结束(被屏障拦住了)。当计数器为0时,表示所有线程都结束了,程序继续向下串行执行。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
work := func(id int) {
defer wg.Done()
fmt.Printf("Worker %d\n", id)
}
numWorkers := 5
wg.Add(numWorkers)
// 启动多个 goroutine
for i := 0; i < numWorkers; i++ {
go work(i)
}
wg.Wait()
fmt.Println("All workers have completed")
}
输出如下
Worker 0
Worker 4
Worker 3
Worker 2
Worker 1
All workers have completed
实战
在学完进阶知识之后,回头看实战,其实项目内容就很简单了。
猜数游戏
这个程序好像很多课程都会拿来用啊,我之前看的一个rust课程也是用这个作为演示项目。思路很简单,用当前时间作为种子生成随机的答案数。在死循环内使用用户输入作为阻塞,用户输入并回车后,将用户输入转为数字并与答案数比较,如果相等就break,否则就是打印提示信息。
这里主要是学习了api的用法,比如
rand.Seed(time.Now().UnixNano())获取当前时间,并将这个设为种子- 除了
fmt.Scanf之外还有下面这种方式读取用户输入reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') input = strings.Trim(input, "\r\n") - 用strconv包进行字符串的解析。
guess, err := strconv.Atoi(input) - go可以直接用
for { ... }来做死循环,比起while(true){}要简洁许多。
在线词典
这个项目展示了用Golang进行网络请求和Json解析。 golang代码中发送一个网络请求的过程:
- 使用http包创建client对象
- 将发送数据(比如一个struct)转为json字符串,继而转为bytes
- 创建request对象,设置目标url、method、数据bytes、请求头
- 发送请求,并接收响应
- 将响应中的数据转为struct
- 使用这个响应继续之后的流程
这里就需要关注struct上的字段标签,这个可以让golang的反射机制使用。标准库json的json.Marshal Unmarshal会在将这个struct进行序列化为json和从json反序列化成struct时,将这个标签作为key。
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
Socks5代理
这个项目名称看起来高大上,其实就是做了一个转发服务。这个程序会指定监听某个网络端口,在其他服务访问这个网络端口时,会先到socks5这里,socks5可以决定是否将该请求继续转发给被代理服务,或者直接截断。
这个项目就有点厉害了,因为他除了单纯的转发之外,还实现了一个协议进行鉴权,实现协议是我没有做过的事,看完之后豁然开朗,其实就是定义了一个数据包格式,除了数据体之外,还有一些特定位置、特定长度的字节用来表示一些信息。不是特定的客户端,或者发送的不是这种格式的数据包,其请求都会被拦下来。也算是提高了网络接口的安全性。
除此之外还学到了net包的使用,可以用他来做端口监听,这也是golang实现网络接口的一个基础包。