嵌入式 C/C++ 开发者转 Go 笔记
作者背景:嵌入式 Linux 工程师,熟悉 C/C++、交叉编译、底层硬件、Node.js/Electron。业余转向 Go 服务端/后端开发。
本文档汇总学习路线、部署、语法对照、模块机制、以及与 C/C++ 的思维差异,所有内容来自 2026-09 学习会话。
配套练习代码见E:Obisidiango-practice(含README.md部署教程、SYNTAX.md语法速查、01_basics~06_http六个脚本)。
〇、为什么转 Go 有天然优势(先建立信心)
- 交叉编译出单二进制:
GOOS=linux GOARCH=arm64 go build直接丢到服务器/板子跑,无运行时依赖——你本就熟GOOS/GOARCH。 - 底层直觉直接复用:你对线程/锁/内存、系统调用、
perf性能分析的理解,映射到 goroutine/GC/pprof几乎零成本。 - 工具链省心:
gofmt强制统一格式、go vet静态检查、go test -race查竞态,思路和你做嵌入式调优一脉相承。
核心认知:你是在「映射」旧知识,不是从零学。Go 把 C/C++ 的概念用更省心的方式重做了一遍。
一、学习路线总览(7 阶段)
| 阶段 | 主题 | 关键抓手 |
|---|---|---|
| 0 | 环境 | 装 Go、配 PATH、跑通 go run |
| 1 | 语法 | go.dev/tour + 《The Go Programming Language》 |
| 2 | 语言机制 | 切片/接口/并发,配合《Go 语言设计与实现》 |
| 3 | 标准库后端 | net/http + database/sql,做第一个真项目 |
| 4 | Web 框架 | Gin 接路由、SQLite/Postgres |
| 5 | 工程化 | 测试、CI、pprof、日志、配置 |
| 6 | 进阶 | cgo 复用 C 库、微服务、部署运维 |
第一个真项目建议做设备遥测 + 指令后端:设备经 HTTP/MQTT 上报 JSON → 存 SQLite/Postgres → 暴露管理 API 查设备、下发指令。直接复用嵌入式领域知识,简历可讲「端到端」。
二、Linux 部署 Go(含练习脚本,完整内联)
面向嵌入式 Linux 工程师的入门套路:先在本机/服务器上 go run 把语法练熟,再编译成单二进制部署。
你本来就熟交叉编译,所以部署那一段对你来说会很顺。
2.1 在 Linux 上安装 Go
以 amd64 为例;arm64 把链接里的 amd64 换成 arm64 即可(和你平时 GOARCH=arm64 一致)。
cd /tmp
wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
把 Go 加进 PATH(写到 ~/.bashrc 或 ~/.profile):
export PATH=$PATH:/usr/local/go/bin
export GOPATH=$HOME/go
重载后验证:
source ~/.bashrc
go version # 应输出 go version go1.22.5 linux/amd64
go env GOROOT GOPATH
提示:建议用官方 tarball 而不是发行版自带的旧版本。Go 向后兼容很强,装新不装旧。
2.2 模块与工作区
Go 用 module 管理依赖(取代老式的 GOPATH 模式)。在任意目录初始化一次即可:
cd ~/go-practice
go mod init go-practice # 生成 go.mod,声明模块名
本目录结构:
go-practice/
├── README.md
├── go.mod
├── 01_basics/main.go # 变量 / 类型 / 控制流
├── 02_collections/main.go # 数组 / 切片 / map
├── 03_structs/main.go # 结构体 / 方法 / 接口(组合+鸭子类型)
├── 04_errors/main.go # 错误处理(多返回值 + error)
├── 05_concurrency/main.go # goroutine / channel / select / context
└── 06_http/main.go # 最小后端:GET /ping、POST /echo
2.3 编写与运行(你的日常开发循环)
go run ./01_basics # 编译并运行(不留下二进制),适合练语法
go run ./05_concurrency # 跑并发示例
go run ./06_http # 起一个 HTTP 服务,另开终端 curl 测试
06_http 起来后:
curl http://localhost:8080/ping
# -> pong
curl -X POST http://localhost:8080/echo -d '{"name":"frank"}'
# -> {"echo":"frank"}
写完顺手做两件事(Go 强制统一格式,省心):
go fmt ./... # 格式化
go vet ./... # 静态检查(悬空 channel、printf 参数不匹配等)
2.4 编译成单二进制(交叉编译你熟)
# 本机运行版本
go build -o bin/server ./06_http
# 交叉编译到 arm64 Linux(比如你的嵌入式板子/服务器)
GOOS=linux GOARCH=arm64 go build -o bin/server-arm64 ./06_http
# 想进一步瘦身
go build -ldflags="-s -w" -o bin/server ./06_http
产物是无依赖的单文件,丢到目标机 chmod +x 就能跑,不需要在目标机装 Go。
2.5 部署:直接运行 或 systemd 托管
方式 A:直接跑(验证用)
scp bin/server user@server:/opt/go-practice/
ssh user@server
cd /opt/go-practice && ./server
方式 B:systemd 托管(生产推荐)
在服务器上写 /etc/systemd/system/go-practice.service:
[Unit]
Description=go-practice device backend
After=network.target
[Service]
User=frank
WorkingDirectory=/opt/go-practice
ExecStart=/opt/go-practice/server
Restart=on-failure
Environment=PORT=8080
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
启用并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now go-practice
sudo systemctl status go-practice
journalctl -u go-practice -f # 看日志
生产环境通常在前面挂一层 nginx 做反向代理 + TLS,Go 服务只监听 127.0.0.1。
2.6 练习顺序建议
| 步骤 | 文件 | 练什么 |
|---|---|---|
| 1 | 01_basics |
:= 短声明、零值、多返回值、for/switch |
| 2 | 02_collections |
slice 的 append/reslice、map 的 ok 取值 |
| 3 | 03_structs |
值/指针接收者、接口多态、组合替继承 |
| 4 | 04_errors |
error 作为返回值、%w 包装、errors.Is |
| 5 | 05_concurrency |
goroutine + WaitGroup、channel、select、context |
| 6 | 06_http |
net/http 路由、JSON 编解码,接回你的后端目标 |
每跑完一个,改两行再跑,最能记住语法。第 6 个就是后端入口,之后可以接数据库(阶段 4)。
2.7 下一步
- 语法熟了 → 进路线图的「阶段 3/4」:用 Gin 替换手写
net/http,接database/sql+ SQLite/Postgres。 - 想复用 C 代码 → 了解 cgo(
import "C"),但先别急,Go 原生够用很久。 - 性能分析 →
go test -bench=.和pprof,和你做嵌入式性能调优思路一致。
三、fish shell 配置坑(PATH 整条覆盖)
现象:ls、[ 全报 command not found,因为 PATH 被整条覆盖丢了 /usr/bin、/bin。默认 shell 是 fish,不是 bash——fish 的 PATH 是列表,不能用冒号拼接、更不能整条替换。
补救(临时,重开前有效):
set -gx PATH /usr/local/go/bin /usr/local/bin /usr/bin /bin
永久修复 ~/.config/fish/config.fish(追加,永远用 $PATH 带旧值):
# 正确:在原有 PATH 后追加,空格分隔
set -gx PATH $PATH /usr/local/go/bin
# 错误写法(会丢系统路径):
# set PATH /usr/local/go/bin
# set -gx PATH $PATH:/usr/local/go/bin
# export PATH=$PATH:/usr/local/go/bin
改完 source ~/.config/fish/config.fish,echo $PATH 应同时含 /usr/bin、/bin、/usr/local/go/bin。
记忆点:给用户 Linux 命令一律给 fish 写法,避免 bash 版再坑。
四、Windows 部署差异对照
Go 代码本身不用改,差异全在操作系统外壳层(装 Go、起进程、防火墙、做服务)。
| 事项 | Linux | Windows |
|---|---|---|
| 装 Go | tarball + 手动 PATH | MSI 自动加 PATH |
| 产物 | server |
server.exe |
| 守护进程 | systemd unit | nssm / WinSW / svc 包 |
| 防火墙 | ufw / firewalld | Defender(New-NetFirewallRule) |
| 看日志 | journalctl -u |
日志文件 / 事件查看器 |
| 反向代理 | nginx | nginx-win / IIS / Caddy |
| 低端口绑定 | CAP_NET_BIND_SERVICE |
netsh http add urlacl |
关键点:
- 交叉编译出 Windows 包:
GOOS=windows GOARCH=amd64 go build -o bin/server.exe ./06_http。 - 无 systemd,常用 nssm 包成服务:
nssm install go-practice C:go-practiceserver.exe→nssm start go-practice。 - 防火墙放行:
New-NetFirewallRule -DisplayName "go-8080" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow。 - 优雅退出:Linux 收
SIGTERM/SIGINT;作为 Windows 服务运行时收不到 SIGTERM,需golang.org/x/sys/windows/svc处理 SCM 停止消息。 - 代码里路径用
path/filepath.Join,别硬编码/或C:。
最短方案:出 server.exe + nssm 托管 + Defender 放行端口 + 前挂 nginx for Windows。
五、用 Go 写桌面:框架选择
| 框架 | 技术路线 | 需 CGO | 跨平台 | 最适场景 |
|---|---|---|---|---|
| Wails | Go + 系统 WebView | 前端 JS | Win/Mac/Linux | Electron 平替、复用前端经验 |
| Fyne | 纯 Go 自绘 | 否 | 桌面+移动+wasm | 跨端、零前端、新手 |
| Gio | 纯 Go 即时模式 | 否 | 桌面+移动+嵌入 | 高性能/小包/自定义 UI |
| Gotk3/4 | GTK 绑定 | 是 | Linux 为主 | 原生 Linux 桌面 |
| Walk | Win32 绑定 | 是 | 仅 Windows | Windows 原生 |
| qt | Qt 绑定 | 是 | 全平台 | 重量级(不推荐新手) |
建议:想顺着 Electron 经验 → Wails(Go 版 Electron,没 Chromium 包袱,二进制小一个数量级);想纯 Go 还能出 Android/iOS → Fyne。
六、Go 基础语法速查(C/C++ 对照,完整内联)
约定:Go 行尾通常不加 ;(编译器自动插;一行多句才需要);用 tab 缩进;gofmt 强制统一格式,别手调。
对照练习脚本:01_basics(变量/流程) 02_collections(切片/map) 03_structs(结构体/接口) 04_errors 05_concurrency(源码见文末附录)。
6.1 最小程序
package main
import "fmt"
func main() {
fmt.Println("hello")
}
package main+func main()是入口(类似 C 的main)。- 非 main 包是库;包名 = 目录名。
- 可见性靠首字母大小写:大写导出(public),小写包内私有——替代
public/private。
6.2 变量与常量
var a int = 10 // 显式类型
var b = 20 // 类型推断
c := 30 // 短声明,仅函数内可用(最常用)
const Pi = 3.14 // 常量
var s string // 零值 ""(不像 C 是随机垃圾值)
var ok bool // 零值 false
:=是「声明 + 赋值」合一,必须至少有一个新变量。- 声明了却不用的变量 / 导入 = 编译报错(不是警告),强制整洁。
iota做枚举:
const (
A = iota // 0
B // 1
C // 2
)
6.3 基本类型
| Go | 说明 | 类似 C |
|---|---|---|
int/int8/16/32/64 |
平台相关,64 位机 int=64 |
int/intN_t |
uint/uintptr |
无符号 | unsigned |
float64/float32 |
默认用 float64 |
double/float |
string |
不可变 UTF-8 | const char* |
bool |
true/false |
_Bool |
byte |
= uint8 |
uint8_t |
rune |
= int32,一个 Unicode 码点 |
wchar_t(近似) |
- 类型转换必须显式:
int(f)、float64(i),没有 C 那种隐式窄化/拓宽。 string不可变;拼接用+或strings.Builder(循环里别反复+,像低效strcat)。
6.4 控制流
if x > 0 { // 条件不加括号
// ...
} else if x < 0 {
// ...
} else {
// ...
}
for i := 0; i < 10; i++ { } // 唯一循环关键字(没有 while)
for cond { } // while 的写法
for { } // 死循环 = while(true)
for i, v := range slice { } // range 遍历(像 for-each)
for k, v := range m { } // map 遍历顺序随机!
switch x { // 不用写 break,自动 break
case 1, 2: // 一个 case 可多值
// ...
default:
}
- 没有三元运算符
a ? b : c,老老实实写 if。 if可带初始化语句:if v, err := f(); err != nil { }。
6.5 函数
func add(a, b int) int { return a + b }
func divmod(a, b int) (int, int) { return a / b, a % b } // 多返回值
func foo() (out int) { out = 5; return } // 命名返回值
func sum(nums ...int) int { // 可变参数
t := 0
for _, n := range nums { t += n }
return t
}
func main() {
q, r := divmod(17, 5) // 多返回值解构
_ = q // _ 丢弃(避免未使用报错)
}
- defer:延迟到函数返回前执行,常用于释放资源(替代 RAII/析构):
f, _ := os.Open("x")
defer f.Close() // 函数结束自动关,类似 C++ 析构 / Rust drop
6.6 数组 / 切片 slice
var arr [3]int = [3]int{1, 2, 3} // 数组:定长、值类型(赋值会整体拷贝)
s := []int{1, 2, 3} // 切片:动态,最常用
s = append(s, 4) // 追加(容量不足自动扩容,像 vector)
sub := s[1:3] // reslice:共享底层数组(像 指针+长度)
copy(dst, src) // 深拷贝
len(s); cap(s) // 长度 / 容量
- 切片 =(指针 + 长度 + 容量)的结构体,传参不拷贝底层数据(类似
std::vector的视图语义),比 C 数组安全。 - 陷阱:
append可能返回新底层数组,记得s = append(s, x)把结果接回。
6.7 Map
m := make(map[string]int) // 必须 make 或字面量,不能往 nil map 写
m["a"] = 1
v := m["a"] // 不存在返回零值,不会崩
v, ok := m["b"] // ok 判断是否存在(惯用法)
delete(m, "a")
- map 遍历顺序随机(语言故意的,防止依赖次序),不像 C++
std::map有序。
6.8 指针
x := 10
p := &x // & 取地址,像 C
*p = 20 // * 解引用
- 有指针,没有指针算术(
p++非法)、没有->(用.直接访问字段,Go 自动解引用)、没有手动 free(GC 管)。 nil是空指针/空引用(类似NULL),但nil不等于数值0。
6.9 结构体与方法
type Point struct {
X, Y int
}
func (p Point) Dist() float64 { return 0 } // 值接收者(拷贝)
func (p *Point) Move(dx int) { p.X += dx } // 指针接收者(可改字段,像传 struct*)
- 没有「类继承」,用组合 + 接口:
type Car struct{ Engine }直接嵌入复用字段与方法。 - 没有构造函数,惯例写
NewXxx()工厂函数返回实例。
6.10 接口(鸭子类型)
type Stringer interface { String() string }
// 任何类型只要有 String() string 方法,就自动满足接口,无需声明 implements
- 接口是「方法集合」,取代 C++ 抽象基类 / 虚函数。
interface{}是空接口(任何类型都满足)= C 的void*但类型安全;Go 1.18+ 用any代替interface{}。
6.11 错误处理(不是异常)
f, err := os.Open("x")
if err != nil { return err } // 错误是普通值,作为最后一个返回值
- 没有
try/catch/throw。错误当值处理(像 C 返回码,但带类型与错误链)。 - 包装:
fmt.Errorf("读 %s: %w", name, err),用errors.Is/errors.As解包判断。 panic仅用于真正不可恢复(类似 C 的abort),别拿来当流程控制。
6.12 并发(关键是 goroutine)
go doWork() // 起一个协程,比 pthread 轻一个量级
ch := make(chan int) // 无缓冲 channel
ch <- 1; v := <-ch // 发 / 收(会阻塞同步)
ch := make(chan int, 4) // 带缓冲
select { case v := <-ch: } // 多路复用,像 select() 但为 channel
- goroutine + channel 是 CSP 模型,优先于共享内存 + 锁。
- 详细见
05_concurrency/main.go(源码见文末附录),跑一遍最直观。
6.13 包与导入
import (
"fmt"
"net/http"
alias "some/long/path" // 别名
)
- 一个目录一个包;
go.mod定义模块路径(详见第七节)。 - 循环导入不允许(直接编译报错)。
6.14 C/C++ 老手最常踩的坑
:=只能在函数内用;包级变量必须用var。++/--是语句不是表达式:i++可以,a := i++不行;没有--i。- 没有
while、do-while、没有?:三元运算符。 - 类型不隐式转换:
int和int64不能直接比较 / 运算。 - 声明未使用、导入未使用 → 编译失败(不是警告)。
nil只能赋给指针 / 切片 / map / chan / func / interface,不能和0比较。string不可变;要改字符用[]byte或strings.Builder。- map、slice、channel 必须用 make 或字面量初始化才能写,往 nil 上写会 panic。
- 方法接收者选择:要改对象或对象很大 → 用指针接收者。
- 数组是值类型,
func f(a [3]int)会整体拷贝;日常多用切片。
6.15 一句话路线
先 go run ./01_basics 把上面每节对应脚本跑一遍改两行 → 卡住就回来看本节 → 熟了进 06_http 接后端(源码见文末附录)。
七、模块与目录结构(go.mod / 多级目录 / internal)
- 一个模块 = 一个
go.mod,只放模块根目录。 子目录是「包(package)」不是「模块」,不写 go.mod。 - 你
go-practice/go.mod已是模块,01_basics~06_http都是其下的包;import 用模块路径/相对目录。 - 第三方库各自有 go.mod,但你不用写,是
go mod tidy把版本记进你的go.mod的require块。 - 多级目录完全允许,目录想套多深都行,每目录一个包。import 路径 = 模块路径 + 相对路径:
app/ ├── go.mod ├── internal/db/mysql/ → import "github.com/frank/app/internal/db/mysql" └── pkg/telemetry/device/ → import "github.com/frank/app/pkg/telemetry/device" internal/是特例:Go 强制internal下的包只能被其父目录及更上层引用,跨模块不可见——放内部实现很常用。- 多级目录可有多个
package main(不同目录),但go build ./...只把含func main()的产出可执行文件。
八、Go 没有宏:替代方案
Go 没有 C/C++ 预处理器宏(#define/#ifdef/#include),也根本没有预处理器——这是故意的设计取舍(宏隐藏控制流、破坏可读性)。替代机制:
| C/C++ 宏惯用法 | Go 的做法 |
|---|---|
#define MAX(a,b) ... 函数宏 |
泛型函数 func Max[T constraints.Ordered](a, b T) T |
#define PI 3.14 常量宏 |
const Pi = 3.14(带类型的真常量) |
#ifdef DEBUG 条件编译 |
//go:build debug 构建标签(go build -tags debug) |
#include "x.h" |
import "pkg/x"(包,非文本包含) |
enum {A,B,C} |
const (A = iota; B; C) |
| X-Macro / 代码生成 | //go:generate + 外部工具(如 stringer) |
template<typename T> |
泛型 [T any] |
要点:
- 泛型是类型检查后的真函数,非文本替换(不会踩
MAX(i++,j++)坑)。 //go:generate stringer -type=Status跑go generate ./...生成真实.go文件(要提交 git)。//go:build linux做平台分支,配//go:build windows同名函数不同实现。- 哲学:「能写真实函数就别用宏」——无预处理器、无文本替换宏、无条件编译宏。
九、第一个真项目 + 下一步
- 语法熟后 → 用 Gin 替手写
net/http,接database/sql+ SQLite/Postgres(阶段 3/4)。 - 想复用 C 代码 → 了解 cgo(
import "C"),但先别急,原生 Go 够用很久。 - 性能分析 →
go test -bench=.和pprof,与嵌入式调优思路一致。 - 想写桌面 → 见第五节,建议先 Wails(顺 Electron 经验)或 Fyne(纯 Go 跨端)。
练习节奏:go run ./01_basics 起,每跑完一个改两行再跑;跑到 06_http 时另开终端 curl http://localhost:8080/ping 应返回 pong;熟了进后端。
附:练习脚本清单
| 文件 | 练什么 |
|---|---|
go-practice/01_basics |
:=、零值、多返回值、for/switch |
go-practice/02_collections |
slice append/reslice、map ok 取值 |
go-practice/03_structs |
值/指针接收者、接口多态、组合替继承 |
go-practice/04_errors |
error 返回值、%w 包装、errors.Is |
go-practice/05_concurrency |
goroutine+WaitGroup、channel、select、context |
go-practice/06_http |
net/http 路由 + JSON 编解码(最小后端,可 curl) |
附录:练习脚本完整源码(内联,无需引用外部文件)
以下为六个练习脚本的完整源码,直接复制即可 go run,便于离线阅读与对照。
01_basics/main.go —— 变量 / 零值 / 多返回值 / 控制流
package main
import "fmt"
func main() {
// 1) 变量声明与类型推断
var name string = "frank" // 显式类型
age := 35 // 短声明 := ,自动推断为 int
const pi = 3.14159 // 常量
fmt.Println("name =", name, "age =", age, "pi =", pi)
// 2) 零值:未显式初始化的变量取类型零值
var x int
var s string
var b bool
fmt.Println("zero values:", x, s, b) // 0 "" false
// 3) 多返回值(Go 的惯用法,类似 C 的指针+返回码,但更直观)
q, r := divmod(17, 5)
fmt.Println("17 / 5 =", q, "余", r)
// 4) 控制流:if / for / switch
if age >= 18 {
fmt.Println("成年人")
}
// for 是 Go 唯一的循环关键字(没有 while)
sum := 0
for i := 1; i <= 5; i++ {
sum += i
}
fmt.Println("1..5 之和 =", sum)
// range 遍历切片
nums := []int{10, 20, 30}
for idx, v := range nums {
fmt.Printf("nums[%d] = %dn", idx, v)
}
// switch(不用写 break,分支自动 break)
switch age / 10 {
case 3:
fmt.Println("三十多岁")
default:
fmt.Println("其他年龄段")
}
}
// 函数可以返回多个值
func divmod(a, b int) (int, int) {
return a / b, a % b
}
02_collections/main.go —— 数组 / 切片 / map
package main
import "fmt"
func main() {
// 数组:长度固定,值类型
var arr [3]int = [3]int{1, 2, 3}
fmt.Println("array:", arr)
// 切片 slice:动态数组,日常最常用
s := make([]int, 0, 4) // len=0, cap=4
s = append(s, 1, 2, 3)
fmt.Println("slice:", s, "len=", len(s), "cap=", cap(s))
// reslice:类似「指针+长度」的视图,共享底层数组
sub := s[1:3]
fmt.Println("sub slice:", sub)
// map:哈希表
m := make(map[string]int)
m["alice"] = 90
m["bob"] = 75
fmt.Println("bob =", m["bob"])
// 安全的取值:逗号 ok 惯用法(避免零值误判)
if v, ok := m["carol"]; ok {
fmt.Println("carol =", v)
} else {
fmt.Println("carol 不存在")
}
delete(m, "bob")
fmt.Println("after delete, len =", len(m))
// 遍历 map:顺序随机,不要依赖遍历次序
for k, v := range m {
fmt.Printf("%s -> %dn", k, v)
}
}
03_structs/main.go —— 结构体 / 方法 / 接口(组合 + 鸭子类型)
package main
import "fmt"
// 结构体:相当于 C 的 struct,但还能给类型绑定方法
type Device struct {
ID string
Temp float64
}
// 值接收者:不会修改原对象(相当于传值)
func (d Device) Describe() string {
return fmt.Sprintf("device %s temp=%.1f", d.ID, d.Temp)
}
// 指针接收者:可修改原对象(相当于 C 传 struct*)
func (d *Device) Cooldown() {
d.Temp -= 5
}
// 接口:鸭子类型,无需显式声明「实现」
// 只要有 Report() string 方法的类型,就自动满足 Reporter
type Reporter interface {
Report() string
}
// Device 已经有 Report() string,因此自动满足 Reporter 接口
func (d Device) Report() string {
return d.Describe()
}
// 接收接口类型,实现多态(类似 C++ 虚函数,但不用继承)
func printReport(r Reporter) {
fmt.Println(r.Report())
}
func main() {
d := Device{ID: "sensor-01", Temp: 42.5}
fmt.Println(d.Describe())
d.Cooldown()
fmt.Println("after cooldown:", d.Temp)
printReport(d) // 接口多态
}
04_errors/main.go —— 错误处理(多返回值 + error + 包装)
package main
import (
"errors"
"fmt"
)
// 哨兵错误:供调用方用 errors.Is 精确判断
var ErrNotFound = errors.New("not found")
// 业务函数:错误作为最后一个返回值(Go 不用异常,类似 C 返回码但更结构化)
func fetch(id string) (string, error) {
if id == "" {
return "", errors.New("empty id")
}
if id == "x" {
// %w 包装底层错误,保留错误链,可被 errors.Is / errors.As 解包
return "", fmt.Errorf("fetch %s: %w", id, ErrNotFound)
}
return "value-" + id, nil
}
func main() {
for _, id := range []string{"a", "", "x"} {
v, err := fetch(id)
if err != nil {
// 需要区分错误类型时用 errors.Is,不要只比字符串
if errors.Is(err, ErrNotFound) {
fmt.Println("捕获到 NotFound:", err)
} else {
fmt.Println("出错:", err)
}
continue
}
fmt.Println("拿到:", v)
}
}
05_concurrency/main.go —— goroutine / channel / select / context
package main
import (
"context"
"fmt"
"sync"
"time"
)
// 并发:goroutine 比 pthread 轻量得多(初始栈仅几 KB,M:N 调度)
func main() {
// 1) goroutine + WaitGroup:等所有协程结束
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) { // 把循环变量 n 传进来,避免闭包捕获同一个 i
defer wg.Done()
fmt.Println("worker", n, "done")
}(i)
}
wg.Wait()
// 2) channel:goroutine 间通信(CSP 模型,取代共享内存+锁)
ch := make(chan int, 2) // 带缓冲的 channel
go func() {
ch <- 100
ch <- 200
close(ch) // 生产方关闭,消费方用 range 收尾
}()
for v := range ch {
fmt.Println("from channel:", v)
}
// 3) select:同时等待多个 channel / 超时
c1 := make(chan string)
go func() { time.Sleep(50 * time.Millisecond); c1 <- "hello" }()
select {
case msg := <-c1:
fmt.Println("recv:", msg)
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout")
}
// 4) context:取消/超时传播(服务端请求生命周期管理常用)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
defer cancel()
if err := doWork(ctx); err != nil {
fmt.Println("work cancelled:", err)
}
}
// 用 select 监听 ctx.Done() 实现超时取消
func doWork(ctx context.Context) error {
select {
case <-time.After(100 * time.Millisecond): // 模拟耗时任务
return nil
case <-ctx.Done(): // 超时触发
return ctx.Err()
}
}
06_http/main.go —— 最小后端(GET /ping、POST /echo)
package main
import (
"encoding/json"
"log"
"net/http"
)
// 最小后端示例:GET /ping 与 POST /echo
// 运行:go run ./06_http
// 测试:
// curl http://localhost:8080/ping
// curl -X POST http://localhost:8080/echo -d '{"name":"frank"}'
// 请求结构体,反引号里是 JSON 字段映射
type EchoReq struct {
Name string `json:"name"`
}
func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("pong"))
})
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "only POST", http.StatusMethodNotAllowed)
return
}
var req EchoReq
// 解析 JSON 请求体
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 返回 JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"echo": req.Name})
})
addr := ":8080"
log.Println("listening on", addr)
// 生产环境前面通常挂 nginx 做反向代理 + TLS
log.Fatal(http.ListenAndServe(addr, nil))
}
