适合并发编程的语言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”。