引言
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/