当今很多高级编程语言都支持并发编程,但是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来执行,很少有编程语言能够提供如此简洁的并发开发方式:
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 http.ListenAndServe(":8080", nil)go语句可以指定函数的返回值的数量和类型吗?go语句是否只允许返回一个值并且类型是error的函数呢? go语句并不理会函数的返回值的数量和类型,在它看来这一切都是浮云,它都不关注。比如下面的div方法,返回值是两个,并没有问题,依然可以使用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函数那一句改造一下:
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语句的参数求值和正常的函数调用都是一样的。比如下面的例子:
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: 1defer语句也是类似的。 但是有时候出题者并不会出这么简单的题,比如下面的代码,我们修改了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语句:
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: BobGo总是传值的,即使是传递一个指针,也是把指针的值传递进去。这里go语句使用的指针指向的对象和外部调用者使用的对象是同一个对象,所以程序输出的结果是修改后的结果,学生的名字已经从“Alice”改成了“Bob”。