为什么Go语言天然支持高并发

为什么Go语言天然支持高并发

引言

Go(Golang)语言很多人听过但没具体用过,大多数都听说过“Go语言天然支持高并发”,背后的原因是什么呢?

本文带着这个问题,做一个Go语言的基本了解和入门。

1. Go语言基本简介

Go语言起源 2007 年,在 2009 年由谷歌开源,主要目标是“兼具Python等动态语言的开发速度和C/C++等编译型语言的性能与安全性”[1]。

下图展示Go语言的基本定位,横轴是对计算机的效率,纵轴是人类编程的容易程度,C/C++ 虽然效率很高,但特性过于复杂,想要熟练掌握绝非易事。

相比其他语言,Go语言主要有以下优点:

  • 自带垃圾回收(gc)。
  • 简单的思想,没有继承,多态,类等。
  • 支持交叉编译(如在 Windows 下编译 Linux 程序)
  • 语法层支持并发,Kuber***es等高并发系统由Go进行编写
  • 内置一套实用工具,如代码格式化、单元测试、文档生成等

2. 第一个go程序

下面配置一下go运行环境,并运行第一个go程序。

首先从go语言官网[2]下载go,当前最新稳定版本是go1.25.2

如果下载.zip,需要再手动配置环境变量,如果下载安装包.msi会自动配置环境变量。

安装完之后,可使用如下命令测试是否安装成功:

go version

在VScode中,安装Go插件:

安装完后,根据弹出的提示,自动安装Go语言开发工具包:

配置完成后,创建main.go,让程序打印"Hello World!"

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt" // 导入 fmt, Go 标准库

func main() { // main函数,是程序执行的入口
	fmt.Println("Hello World!") // 在终端打印 Hello World!
}

Go 从 1.13 开始引入了模块(Go Modules) 概念,所有 Go 项目都要有一个go.mod文件来描述项目依赖,因此在运行前,需要先使用如下命令,初始化一个mod文件:

go mod init first_go

之后构建当前模块:

go build

构建得到first_go.exe,运行改程序,就能看到控制台输出的“Hello World!”。

first_go.exe

3. go语言基础

文档[1]详细介绍了go语言的数据类型、流程控制、函数等操作,和别的语言差不多。除此之外,go语言还有一些其它特性,比如通过init()函数实现做一些自动初始化工作,如初始化全局变量、加载配置文件等操作。

go工具包包含了以下一些命令:

命令 作用 示例 说明
build 编译当前包及其依赖 go build 编译但不安装,生成可执行文件
clean 清理编译生成的临时文件 go clean 删除 .exe.a 等构建产物
doc 查看包或函数文档 go doc fmt.Println 类似 man 命令,快速查文档
env 打印 Go 环境变量 go env 查看 GOPATH、GOROOT 等配置
bug 打开 Go 官方错误报告页面 go bug 帮助你上报 bug 到 Go 官方
fix 自动更新旧语法 go fix 将老版本 Go 代码更新为新语法
fmt 自动格式化代码 go fmt ./... 保持代码风格统一(缩进、空格等)
generate 执行代码生成指令 go generate 运行源代码中的 //go:generate 指令
get 下载并安装依赖 go get github.***/gin-gonic/gin 获取远程包并更新 go.mod
install 编译并安装包 go install 把可执行文件安装到 $GOPATH/bin
list 列出包信息 go list ./... 查看项目中有哪些包
run 编译并立即运行 go run main.go 适合快速测试和调试
test 运行测试代码 go test ./... 自动执行 _test.go 文件中的单元测试
tool 运行底层 Go 工具 go tool ***pile main.go 调用 Go 内置工具(调试、编译分析)
version 查看 Go 版本 go version 输出当前 Go 编译器版本
vet 静态检查代码错误 go vet ./... 检查潜在 bug(如未使用变量等)

常用的一些方法如下:

目标 命令
初始化项目 go mod init myproject
格式化代码 go fmt ./...
编译运行 go run main.go
生成可执行文件 go build
执行单元测试 go test ./...
清理项目产物 go clean
查看版本和环境 go version && go env

4. go的并发编程

下面回到正题,为什么说“go语言天然支持高并发”呢?

因为go语言中,可以通过goroutine去自动进行任务调度。

比如在C++中,进行并发编程时,往往需要自己维护一个线程池,而 goroutine 是由Go的运行时调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。

要使用 goroutine,在函数前面加个go就可以了。

4.1 GMP 模型

Go 的调度器基于 GMP 模型,它是 3 个核心结构的组合:

组件 作用
G(Goroutine) 任务单元,存放栈、状态、执行上下文
M(Machine) 代表 OS 线程,真正执行代码的实体
P(Processor) 调度器的抽象,负责管理本地可运行的 G 队列(run queue)并与 M 绑定

执行一个 go func(),GMP 调度的调度流程如下:

go func() →
  创建 G(goroutine 对象) →
    放入当前 P 的本地队列 →
      M 从 P 的队列中取出 G →
        执行 G →
          阻塞?(syscall、IO 等)→
            M 阻塞,P 被摘除并转交其他 M;
            runtime 新建或复用空闲 M 绑定该 P;
            被阻塞的 G 等待 syscall 返回;
          完成?→
            回收 G 资源,标记可复用;
          长时间运行?→
            触发抢占调度,G 被挂起,P 执行下一个 G;
        结束或主动让出 CPU(runtime.Gosched) →
      M 从 P 的队列继续取下一个 G 执行 →
    若本地队列为空 →
      从全局队列或其他 P “偷取” G →
        重复循环执行调度。

4.2 goroutine 轻量高效的原因

通过 GMP 调度流程,可以看出 Go 并不是简单的 “N 个协程绑定 1 个线程”,而是采用了更高效的M:N 调度模型,即 N 个 goroutine 可以在 M 个系统线程上动态调度运行。

根据文档[1]的定义,一个线程可以分为 “内核态”线程和 “用户态”线程,内核态线程依然叫 “线程 (thread)”,用户态线程叫 “协程 (co-routine)”。

M:N 调度模型可以进一步可视化成下图,goroutine 的切换完全在用户态完成,避免陷入内核态,切换只需保存少量寄存器和栈信息(2KB 左右),速度极快,而系统线程至少要分配 1MB 栈空间,这就是goroutine 更轻量的原因。

此外,通过 GMP 的智能调度,G 被阻塞(syscall、IO)后,P 会马上交给别的 M,避免单点瓶颈,实现了高并发下的“非阻塞调度”。

以上两点促成了 goroutine 的轻量高效。

4.3 并发模拟测试

下面来进行并发场景下的模拟测试,让 Go 和 C++ 都创建 1 万个并发任务,每个任务休眠一小段时间(模拟 I/O),然后打印总耗时,拿 Go 和 C++ 进行对比。

Go:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	const n = 10000
	var wg sync.WaitGroup
	wg.Add(n)

	start := time.Now()
	for i := 0; i < n; i++ {
		go func(i int) {
			time.Sleep(10 * time.Millisecond) // 模拟I/O任务
			wg.Done()
		}(i)
	}

	wg.Wait()
	fmt.Printf("Go 启动 %d 个 goroutine 总耗时:%v\n", n, time.Since(start))
}

C++:

#include <iostream>
#include <thread>
#include <vector>
#include <chrono>

int main() {
    const int n = 10000;
    auto start = std::chrono::high_resolution_clock::now();

    std::vector<std::thread> threads;
    threads.reserve(n);

    for (int i = 0; i < n; ++i) {
        threads.emplace_back([]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 模拟I/O
        });
    }

    for (auto &t : threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "C++ 启动 " << n << " 个 thread 总耗时:"
              << std::chrono::duration<double, std::milli>(end - start).count() << "ms\n";
}

运行结果:

C++ 启动 10000 个 thread 总耗时:2102.45ms
Go 启动 10000 个 goroutine 总耗时:38.0771ms

结果表明,goroutine 比 thread 快了数十倍。

当然,C++ 可以通过额外安装库asio来模拟 goroutine 的调度:

#include <asio.hpp>
#include <iostream>
#include <chrono>
#include <vector>

int main() {
    const int num_tasks = 10000;
    asio::io_context io;
    asio::executor_work_guard<asio::io_context::executor_type> guard(io.get_executor());

    auto start = std::chrono::high_resolution_clock::now();

    // 创建异步计数器
    int counter = 0;

    for (int i = 0; i < num_tasks; ++i) {
        // 使用 steady_timer 异步延时模拟 Go sleep
        auto timer = std::make_shared<asio::steady_timer>(io, std::chrono::milliseconds(10));
        timer->async_wait([&counter, timer](const asio::error_code&) {
            counter++;
        });
    }

    // 启动线程池
    const int num_threads = std::thread::hardware_concurrency();
    std::vector<std::thread> threads;
    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back([&io]() { io.run(); });
    }

    // 释放 guard 让 io_context 可以退出
    guard.reset();

    // 等待所有线程结束
    for (auto &t : threads) t.join();

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> duration = end - start;

    std::cout << "C++ Asio 启动 " << num_tasks
              << " 个异步任务总耗时: "
              << duration.count() << " ms" << std::endl;

    return 0;
}

运行结果:

C++ Asio 启动 10000 个异步任务总耗时: 81.9486 ms

结果仍然不如 Go 的goroutine,而且代码复杂度也更高。

总结

在并发场景下,Go语言有天然优势,通过高度工程优化的goroutine,能够在代码量不多的同时,加快程序执行效率。

参考

[1] Go语言中文文档:https://www.topgoer.***/
[2] Go语言官网:https://go.dev/dl/

转载请说明出处内容投诉
CSS教程网 » 为什么Go语言天然支持高并发

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买