2024字节青训营笔记(二)Golang进阶与实践 | 豆包MarsCode AI刷题

105 阅读7分钟

Golang进阶知识

Goroutine

go命令启动的是协程,一个进程里面可以有多个线程,一个线程里面可以有多个协程。我的理解是线程就是依赖操作系统管理,开销较大。协程是让用户这边管理,在一个线程内主动切换任务,开销较小,但是管理起来比较麻烦,编程语言需要花不少功夫为我们做实现(比如协程的管理,以及跨平台的支持)。

image.png

我记得Java以前有过绿色线程,然后后面还是改成了本地线程。JDK21又带来了虚拟线程--这种由JVM而不是OS管理的线程。不管Java是懒得弄还是确实不太好实现,go实现了goroutine确实让go具有卖点。

在他的使用上,其实和线程差不多,直接就是go funcgo后面可以跟一个已经定义好的函数,也可以当场定义一个匿名函数然后跑它。

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,然后将 i2i^2传入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_barrierawaitjoin等。在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代码中发送一个网络请求的过程:

  1. 使用http包创建client对象
  2. 将发送数据(比如一个struct)转为json字符串,继而转为bytes
  3. 创建request对象,设置目标url、method、数据bytes、请求头
  4. 发送请求,并接收响应
  5. 将响应中的数据转为struct
  6. 使用这个响应继续之后的流程

这里就需要关注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可以决定是否将该请求继续转发给被代理服务,或者直接截断。

这个项目就有点厉害了,因为他除了单纯的转发之外,还实现了一个协议进行鉴权,实现协议是我没有做过的事,看完之后豁然开朗,其实就是定义了一个数据包格式,除了数据体之外,还有一些特定位置、特定长度的字节用来表示一些信息。不是特定的客户端,或者发送的不是这种格式的数据包,其请求都会被拦下来。也算是提高了网络接口的安全性。

image.png

除此之外还学到了net包的使用,可以用他来做端口监听,这也是golang实现网络接口的一个基础包。