适合并发编程的语言Golang

1/2/2026golang并发

当今很多高级编程语言都支持并发编程,但是Go语言绝对是很特殊的那一个,一开始它就从语言设计上为并发编程提供了最简单的方式,并且设计了对线程更轻量级的goroutine,还为CSP模型提供了容易使用的channel类型,并将其作为内建类型直接提供。

2019年,在Go Time对Rob Pike和Robert Griesemer的访谈节目中,Rob Pike 谈到发明Go语言的想法时说道,“我们听到了一个C++新版本发布的消息,我一直认为C++缺乏对新的多核机器的支持,我想尝试把多年来自己对并发编程的理解展现出来。因为我和Robert Griesemer在同一间办公室,所以2007年9月的一天我们坐下来讨论,从此Go 语言诞生了,所以你看发明Go语言的初衷之一就是方便进行并发编程的开发。”在2008年3月7日制定的一个初级的Go语言规范中,就明确指出Go要对多线程提供支持,并且提到对函数提供并发执行的能力,以及通过channel提供通信和同步。这些来自Rob Pike 开发Plan 9操作系统以及设计Squeak/NewSqueak 编程语言的方法和创新都被应用到了Go语言中。

在Go语言中,实现并发编程非常简单,在函数的执行语句的前面加上go关键字,该函数就会自动生成一个goroutine来执行,很少有编程语言能够提供如此简洁的并发开发方式:

go
package main import ( "fmt" "net/http" "time" ) func getFromBilibili() { resp, err := http.Get("https://www.bilibili.com") if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() fmt.Println("Response Status:", resp.Status) } func main() { // 并发输出 go func() { fmt.Println("Hello from a goroutine!") }() //  并发访问 go getFromBilibili() time.Sleep(2 * time.Second) // 等待 goroutine 完成 }

启动一个goroutine就是如此简单。在函数和方法的调用前面加上go,就会创建一个goroutine,这个goroutine会加入Go调度器的队列进行调度或者执行,调用者不会被阻塞,而是会继续执行,所以避免了程序直接退出。这里我们等待了1秒钟,将来会使用同步原语更好地控制程序的等待方式。函数执行结束后,此goroutine也会终止。

下面的一行语句就启动了一个监听8080端口的HTTP服务。

go
go http.ListenAndServe(":8080", nil)

go语句可以指定函数的返回值的数量和类型吗?go语句是否只允许返回一个值并且类型是error的函数呢? go语句并不理会函数的返回值的数量和类型,在它看来这一切都是浮云,它都不关注。比如下面的div方法,返回值是两个,并没有问题,依然可以使用go语句并发执行。

go
func main() { go div(1, 2) time.Sleep(2 * time.Second) } func div(x, y int) (int, error) { if y == 0 { return 0, fmt.Errorf("y cannot be zero") } return x / y, nil }

如果需要处理 error信息,则可以把go语句改造一下,使用匿名函数(anonymous function)将其封装起来。比如把调用div函数那一句改造一下:

go
func main() { go func() { result, err := div(1, 2) if err != nil { fmt.Println("Error:", err) } else { fmt.Println("Result:", result) } }() time.Sleep(2 * time.Second) } func div(x, y int) (int, error) { if y == 0 { return 0, fmt.Errorf("y cannot be zero") } return x / y, nil }

go语句经常让人迷惑的地方就是它的参数被求值的时机,因为函数是异步执行的,函数异步执行时这个参数可能会在后面某个时间才被使用,传入的参数在调用者所在的goroutine 中可能会被修改,所以有的面试官会故意制造一些混乱,问你这个参数求值的一些问题-别被迷惑,你只需要记住,go语句的参数求值和正常的函数调用都是一样的。比如下面的例子:

go
var list []int go func(l int) { time.Sleep(time.Second) fmt.Printf("passed len: %d, current list len: %d\n", l, len(list)) }(len(list)) list = append(list, 1) time.Sleep(time.Second * 2)

在这个例子中,调用go语句时传入len(list),这个时候list是空的,它的长度是0,所以此时参数的求值结果就是0。

这个goroutine运行输出结果时,list被调用者修改了,它的长度变成了1,所以输出结果下所示,goroutine开始时list长度为0,1秒后再检查就变成1了。

passed len: 0, current list len: 1

defer语句也是类似的。 但是有时候出题者并不会出这么简单的题,比如下面的代码,我们修改了go语句的函数对象的值,会对已经求值的go语句造成影响吗?

go
list := []int{1} foo := func(l int) { time.Sleep(time.Second) fmt.Printf("passed len: %d, current list len: %d\n", l, len(list)) } go foo(len(list)) foo = func(l int) { fmt.Printf("passed len: %d, current list len: %d\n", l*100, len(list)*100) } time.Sleep(time.Second * 2) foo(len(list))

结果为:

passed len: 1, current list len: 1 passed len: 100, current list len: 100

在上面的代码中,go语句执行的还是被修改前的值。

或者,还会考查你对Go参数传递指针和传递值的理解,结合go语句:

go
type Student struct { Name string } func main() { s := &Student{Name: "Alice"} go func(s *Student) { time.Sleep(time.Second) fmt.Printf("student name: %s\n", s.Name) }(s) s.Name = "Bob" time.Sleep(2 * time.Second) }

 结果为

student name: Bob

Go总是传值的,即使是传递一个指针,也是把指针的值传递进去。这里go语句使用的指针指向的对象和外部调用者使用的对象是同一个对象,所以程序输出的结果是修改后的结果,学生的名字已经从“Alice”改成了“Bob”。