Posted in

嵌入式 C/C++ 开发者转 Go 语言学习笔记:路线、部署与语法对照

嵌入式 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.fishecho $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.exenssm 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++ 老手最常踩的坑

  1. := 只能在函数内用;包级变量必须用 var
  2. ++ / --语句不是表达式i++ 可以,a := i++ 不行;没有 --i
  3. 没有 whiledo-while、没有 ?: 三元运算符。
  4. 类型不隐式转换:intint64 不能直接比较 / 运算。
  5. 声明未使用、导入未使用 → 编译失败(不是警告)。
  6. nil 只能赋给指针 / 切片 / map / chan / func / interface,不能和 0 比较。
  7. string 不可变;要改字符用 []bytestrings.Builder
  8. map、slice、channel 必须用 make 或字面量初始化才能写,往 nil 上写会 panic。
  9. 方法接收者选择:要改对象或对象很大 → 用指针接收者
  10. 数组是值类型,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.modrequire 块。
  • 多级目录完全允许,目录想套多深都行,每目录一个包。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=Statusgo generate ./... 生成真实 .go 文件(要提交 git)。
  • //go:build linux 做平台分支,配 //go:build windows 同名函数不同实现。
  • 哲学:「能写真实函数就别用宏」——无预处理器、无文本替换宏、无条件编译宏。

九、第一个真项目 + 下一步

  1. 语法熟后 → 用 Gin 替手写 net/http,接 database/sql + SQLite/Postgres(阶段 3/4)。
  2. 想复用 C 代码 → 了解 cgoimport "C"),但先别急,原生 Go 够用很久。
  3. 性能分析 → go test -bench=.pprof,与嵌入式调优思路一致。
  4. 想写桌面 → 见第五节,建议先 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))
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注