Go 学习笔记
https://duoke360.com/tutorial/path/golang
go 基础
只记录一些我认为需要做笔记的。
iota
const (
// 把 iota 理解成行号,从第 0 行开始
// _ 可以跳过一行
// const 里面如果省略了值,则跟上一行相同
a1 = 100 // 第 0 行,iota=0,a1=100
a2 = iota // 第 1 行,iota=1,a2=1
a3, a4 = iota, iota // 第 2 行,a3=2,a4=2
a5, a6 // a5=3,a6=3
)
Sprintf
name := "tom"
age := "20"
msg := fmt.Sprintf("%s:%s", name, age)
//Sprintf 格式化并返回格式化后的字符串
fmt.Printf("msg: %v\n", msg)
分号
go 编译器会自动帮你加分号,所以基本不用写。
但是同一行如果有多个语句的话,那么就要利用分号去分隔。
隔离作用域
go 是类似 c++ 的,if 是会隔离作用域的,包括说 for 那些。这一个部分与 python 只有函数体,class 等才隔离作用域不同。
switch
-
go 里的 switch 不需要 break
-
默认只执行一个语句,匹配到一个 case 就跑路了!
-
加逗号,表示任意匹配上一个你就进去这个 case
-
fallthrough用来无视条件,强制接着执行下一个 case 的内容!
package main
import "fmt"
func main() {
// go 里的 switch 不需要 break
// 默认只执行一个语句
a := 10
switch a {
case 100, 10:
fmt.Println("100 or 10")
fallthrough
case 200:
fmt.Println("200")
case 300:
fmt.Println("300")
default:
fmt.Println("other")
}
}
100 or 10
200
i++;i--
go 中只有 i++,i--,并且只能作为单独的一个语句进行使用,没有返回值!因此不能作为表达式(没有值)
然后没有 ++i,--i,你都只是一个语句了,目的就是给 i 自增而已,当然没必要搞两种写法。
goto
了解即可,不要去写。
goto 的作用就是让你直接跳转到标签的那一行!
package main
import "fmt"
func f() {
a := 0
if a == 1 {
goto LABEL1
} else {
fmt.Println("other")
}
// 也执行这个标签里的东西,其实就是跳转到这个标签这一行
// 其实就是让你直接跳到某一行而已。
LABEL1:
fmt.Printf("next...")
}
// a=0,那么 else 了!然后输出 other,接着执行,输出 next...
func main() {
f()
}
slice
slice 是先创建一个底层数组,然后再返回一个结构体 (ptr+len+cap) 作为 slice。
所以 slice=结构体(ptr+len+cap)
s1 := make([]int, 3) // 步骤:
// 1. 在堆上创建数组 [0, 0, 0]
// 2. 创建切片结构体 {ptr: 数组地址, len: 3, cap: 3}
// 3. 返回这个切片
语法普遍规律
语法1:var 变量名 数据类型
这样可以去声明变量
var mp map[string]int
var age int
var arr []int // []int 这个数据类型就是 slice
语法2:数据类型{元素}
这样可以去给变量赋值,初始化变量
m2 := map[string]string{
"name": "kite",
"age": "20",
"email": "kite@gmail.com",
}
a:=[]int{1,2,3,4,5}
语法3:var 变量名 数据类型=数据类型{元素}
把上面两者结合
map
map 可理解为指向底层哈希表的指针。用 var mp map[string]int 的时候,只是声明了一个指针指向 nil,但是没有分配底层的哈希表内存,也就是没办法存储数据。
// 声明:只创建了"地图名字",没有"实际地图"
var mp map[string]int
// mp: | nil指针 |
// 初始化:创建实际的"地图"
mp = make(map[string]int)
// mp: | 指针 | → 哈希表内存
函数
基础
func 函数名(什么东西 是什么类型, 什么东西 是什么类型) 返回什么类型
func f1() {
fmt.Printf("hello this is a function\n")
}
func sum(a int, b int) (res int) {
res = a + b
return
}
func mul(a int, b int) int {
ans := a * b
return ans
}
func swap(a int, b int) (int, int) {
return b, a
}
函数类型与函数变量
搞出来一个函数类型了之后,就把它当成 int 之类的变量类型,可以去定义函数变量了,语法还是一样。
type 函数类型名称 func(参数列表类型) (返回列表类型)
其实你发现这个 type 就是给你的这个函数类型,起个别名而已
你直接
var f func(int, int) int
也没事
package main
import "fmt"
type fun func(int, int) int
func sum(a int, b int) int {
return a + b
}
func max(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
func main() {
var f fun
f = sum
s := f(1, 2)
}
go 闭包
按照 python 里的闭包理解即可,使用起来是一样的。底层实现略微有一些区别而已。
package main
import "fmt"
// 返回一个函数
func add() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = add()
fmt.Println(f(10))
fmt.Println(f(20))
fmt.Println(f(30))
fmt.Println("-----------")
f1 := add()
fmt.Println(f1(40))
fmt.Println(f1(50))
}
python 视角:
f:=add()的时候会调用一次add()然后返回值赋给f,也就是让f指向inner()了,不过此时的inner()是个匿名函数,因为go里不允许嵌套(除非匿名),然后x跟这个inner()函数对象绑定了,有个闭包单元格的东西,一起到堆上了。f1:=add(),又调用了一次,自然又创建了一个新的inner()函数对象在堆上,又绑定了一个新的x变量。
10
30
60
-----------
40
90
结构体
结构体指针
package main
import "fmt"
type Person struct {
age int
name string
}
func main() {
tom := Person{18, "tom"}
var ptr *Person
ptr = &tom
fmt.Printf("ptr: %p\n", ptr)
fmt.Printf("(*ptr): %v\n", (*ptr))
// new 实例化,然后返回一个实例的地址
ptr2 := new(Person)
fmt.Printf("ptr2: %p\n", ptr2)
// 会自动给你解引用
// 就不需要 (*ptr2).name="hi"
ptr2.name = "hi"
fmt.Printf("ptr2: %v\n", ptr2)
}
接口(interface)
基础
interface 就是一个新的类型,你可以拿它去定义一个新的接口变量,然后一个 interface 要去规定实现哪些函数。并且你只能把完整实现了这些函数的结构体实例赋值给接口变量。
比喻:接口变量是插座,只有合法的电器(结构体类型)才能成功插上去。
类比:把接口类型当成父类,然后结构体类型当成子类,父类对象是能够接收子类对象的(Python),也就是接口变量能接收结构体变量。
// 插座标准(接口)
type PowerSocket interface {
PlugIn() // 插入
GetPower() int // 获取电量
}
// 电器A:实现了插座标准
type AirConditioner struct{}
func (a AirConditioner) PlugIn() { fmt.Println("空调插电") }
func (a AirConditioner) GetPower() int { return 2000 }
// 电器B:实现了插座标准
type Lamp struct{}
func (l Lamp) PlugIn() { fmt.Println("台灯插电") }
func (l Lamp) GetPower() int { return 60 }
func main() {
var socket PowerSocket
// 可以用插座接空调
socket = AirConditioner{} // ✅
// 也可以用同一个插座接台灯
socket = Lamp{} // ✅
// 但不能用插座接水杯
// socket = Cup{} // ❌ 水杯没有PlugIn()和GetPower()方法
}
OCP
Open-Closed Principle:对扩展开放,对修改关闭。
核心:利用接口变量接收不同的结构体类型,然后不同的结构体类型对同一个方法的实现不同。类似于多态。
package main
import "fmt"
type Pet interface {
eat()
sleep()
}
type Dog struct {
name string
age int
}
func (d Dog) eat() {
fmt.Println("dog eat...")
}
func (d Dog) sleep() {
fmt.Println("dog sleep...")
}
type Cat struct {
name string
age int
}
func (d Cat) eat() {
fmt.Println("cat eat...")
}
func (d Cat) sleep() {
fmt.Println("cat sleep...")
}
func care(p Pet) {
// 核心就是这里的 p 是 Pet 类型的
// 可以接收 Dog&Cat
p.eat()
p.sleep()
}
func main() {
dog := Dog{}
cat := Cat{}
care(dog)
care(cat)
}
goroutine

多个 goroutine 是并发的,但是在实际体感上我觉得跟并行差不多。
因为单核的话就切成很多个时间片嘛。然后多核其实就直接并行了,一个协程放一边。
channel
channel 用于 goroutine 间通信。
-
无缓冲通道
ch := make(chan int) // 创建一个无缓冲通道类比两个人手递手,发送方伸出手表示发送,但是要等待另一个人也伸出手。
接收方伸出手接收,但是要等待发送方也伸出手发送, -
缓冲通道
ch := make(chan int, 10) // 创建可存放10个int的缓冲通道这个就跟快递柜一样了。
两个 goroutine,一个快递员,一个用户。
那么快递员可以存快递,用户取快递。
只要快递柜没满,双方就可以独立工作。
WaitGroup
package main
import (
"fmt"
)
func hello(i int) {
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
go hello(i) // 启动10个协程
}
// time.Sleep(time.Millisecond * 100)
// 主协程立即退出,程序结束
// 可能一个输出都看不到!
}
这样子的话,你创建的 10 个协程可能都没执行,因为你主线程直接 over 了!
WaitGroup 就是一个计数器,用来等待多个协程完成任务。
想象你请了10个工人干活,你要等所有工人都干完活才能下班。
每来一个工人,记个数(+1)
每走一个工人,记个数(-1)
最后检查:如果计数=0,说明工人都走了,可以下班了
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup // 1. 创建WaitGroup
func hello(i int) {
defer wg.Done() // 3. 协程结束时调用Done(计数器-1)
time.Sleep(time.Second) // 模拟耗时操作
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 2. 每启动一个协程,计数器+1
go hello(i)
}
wg.Wait() // 4. 阻塞等待,直到计数器=0
fmt.Println("所有协程完成!")
}
互斥锁(Mutex)
知识
Mutex:Mutual Exclusion Lock
sync:synchronization 同步
为了解决数据竞争的问题。
-
数据竞争
// 错误示例:没有锁 func add() { i++ // 并发执行时可能出现问题 } func sub() { i-- // 并发执行时可能出现问题 }在执行时可能会出现
初始 i=100 goroutine1: 读取 i=100 goroutine2: 读取 i=100 goroutine1: 计算 100+1=101 goroutine2: 计算 100-1=99 goroutine1: 写入 i=101 goroutine2: 写入 i=99 最终 i=99 或 101(取决于写入顺序) 预期应该是 100 -
互斥锁
就是保证了,一个时间只能有一个 goroutine 执行对数据的修改。var lock sync.Mutex // 创建一个互斥锁 func add() { lock.Lock() // 获取锁(如果锁被占用,则阻塞等待) i++ // 临界区:同一时间只有一个goroutine能执行这里 lock.Unlock() // 释放锁 }初始值:i = 100 执行:100次 i++,100次 i-- 数学上:100 + 100 - 100 = 100 由于有锁保护,每次操作都是原子的 所以最终结果一定是 100
互斥锁案例
// 实现一个互斥锁,使得 100 个 goroutine add 跟 sub 了之后,变量的值还没变
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex
var num = 100
func add() {
defer wg.Done()
lock.Lock() // 获取锁,如果别人把锁拿走了,那就一直等着,阻塞等待
num = num + 1 // 临界区:每个时刻只有一个 goroutine 可以执行这里
lock.Unlock() // 释放锁,把锁还给公家
}
func sub() {
defer wg.Done()
lock.Lock() // 获取锁,如果别人把锁拿走了,那就一直等着,阻塞等待
num = num - 1
lock.Unlock() // 释放锁,把锁还给公家
}
func main() {
for i := 1; i <= 100; i++ {
wg.Add(2)
go add()
go sub()
}
wg.Wait()
fmt.Printf("num: %v\n", num)
}
原子操作
非原子操作
你可能搞一半,系统就调度去执行别的 goroutine 了。
i = i + 1
// 分解为3步:
1. 从内存读取 i
2. 计算 i+1
3. 把结果写回 i
goroutine A: 执行第1步(读i=0)
goroutine A: 执行第2步(计算0+1=1)
[这时候被中断!]
goroutine B: 执行第1步(读i=0)← 还是老值!
goroutine B: 执行第2步(计算0+1=1)
goroutine B: 执行第3步(写i=1)
[切换回来]
goroutine A: 执行第3步(写i=1)← 覆盖了B的结果!
最终:i = 1
期望:i = 2
结果:丢失了一次加法!
原子操作
原子操作比起互斥锁,简单实现,更快。但是只能支持简单的操作。
atomic.AddInt32(&i, 1)
// 1步完成:读→计算→写
goroutine A: 执行atomic.AddInt32(&i, 1) [完整执行,i=1]
goroutine B: 执行atomic.AddInt32(&i, 1) [完整执行,i=2]
最终:i = 2 ✓
os
write
// 打开 test.txt,然后写入hello golang
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.OpenFile("./2026_5_7/test.txt", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
defer f.Close()
_, err = f.Write([]byte("hello golang"))
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
fmt.Println("数据写入成功!")
}
read
// 读取 test.txt 文件,每次读 4 个 byte
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.OpenFile("./2026_5_7/test.txt", os.O_RDWR, 0644)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
defer f.Close()
buf := make([]byte, 4)
for {
n, err := f.Read(buf)
if n == 0 || err != nil {
break
}
fmt.Println(string(buf[:n]))
}
fmt.Println("文件读取完毕")
}
mysql
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
func initDB() (err error) {
dsn := "root:me20070513@tcp(127.0.0.1:3306)/go_db?charset=utf8mb4&parseTime=True"
db, err = sql.Open("mysql", dsn)
if err != nil {
return err
}
err = db.Ping()
if err != nil {
return err
}
return nil
}
type user struct {
id int
name string
password string
}
// 查询一条用户数据
func queryRowDemo() {
sqlStr := "select id, name, password from users where id=?"
var u user
// 确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.password)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%s\n", u.id, u.name, u.password)
}
func insertNewUser() {
// 1. 准备带有 ? 的 SQL 骨架
sqlStr := "INSERT INTO users (name, password, email) VALUES (?, ?, ?)"
// 2. 扣动扳机(传入具体数据)
result, err := db.Exec(sqlStr, "老兵", "111111", "laobing@qq.com")
if err != nil {
fmt.Printf("插入失败, err:%v\n", err)
return
}
// 3. 核心大招:获取 MySQL 刚刚自动生成的 ID!
newID, err := result.LastInsertId()
if err != nil {
fmt.Printf("获取新ID失败, err:%v\n", err)
return
}
fmt.Printf("插入成功!这个新用户的 ID 是:%d\n", newID)
}
func deleteUser() {
// 准备开除年龄大于 35 岁的员工(带上你的 WHERE 瞄准镜)
sqlStr := "DELETE FROM users WHERE id > ?"
// 扣动扳机
result, err := db.Exec(sqlStr, 5)
if err != nil {
fmt.Printf("删除执行失败, err:%v\n", err)
return
}
// 核心大招:清点战场,看看干掉了几个人!
rows, err := result.RowsAffected()
if err != nil {
fmt.Printf("获取战果失败, err:%v\n", err)
return
}
fmt.Printf("报告!本次成功开除了 %d 名员工!\n", rows)
}
func main() {
err := initDB()
if err != nil {
fmt.Printf("err: %v\n", err)
} else {
fmt.Println("数据库连接成功")
}
queryRowDemo()
// insertNewUser()
deleteUser()
}
总结
其实已经告一段落了,语法的东西遇到对应的需求,再问 AI 即可,写这一篇也仅仅是为了记录一下一些案例以及自己的思考。不过,语法确实没什么需要思考的哈。
设计模式
工厂设计模式
就像汽车工厂一样,把内部的复杂制造汽车的过程都藏在了厂房内(黑盒),最后交付给我们手里的是一俩汽车,只需要会用方向盘、刹车、油门这样就好了,压根不用去理解里面是怎么样工作的内部细节。
就是把复杂功能打包成一个黑盒,方便调用。
package main
import (
"errors"
"fmt"
)
// 1. 定义接口 (契约)
type Fruit interface {
Plant() // 修正拼写:Plant(种植)
Pick()
}
// 2. 具体的结构体 (隐藏在工厂背后的工人)
type Apple struct{}
func (*Apple) Plant() { fmt.Println("种植苹果") }
func (*Apple) Pick() { fmt.Println("采摘苹果") }
type Orange struct{}
func (*Orange) Plant() { fmt.Println("种植桔子") }
func (*Orange) Pick() { fmt.Println("采摘桔子") }
// 3. 告别魔法数字!定义常量枚举
const (
TypeApple = "apple"
TypeOrange = "orange"
)
// 4. 终极工厂函数:返回 (实例, 错误) 双黄蛋!
func NewFruit(fruitType string) (Fruit, error) {
switch fruitType {
case TypeApple:
return &Apple{}, nil // 成功,返回苹果和 nil 错误
case TypeOrange:
return &Orange{}, nil // 成功,返回桔子和 nil 错误
default:
// 如果传入未知类型,优雅地返回错误,绝不返回 nil 让外面崩溃
return nil, errors.New("工厂目前不支持生产该类型的水果")
}
}
func main() {
// 调用方:我只要一个桔子,我不关心桔子是怎么构造的
myFruit, err := NewFruit(TypeOrange)
// 5. 永远先处理错误!(Go 语言保命铁律)
if err != nil {
fmt.Println("获取水果失败:", err)
return
}
// 安全调用
myFruit.Plant()
myFruit.Pick()
}
单例设计
就是让某个类只有一个实例。
package main
import (
"fmt"
"sync"
)
// golang技术栈(golang-tech-stack.com)-老郭
type Singleton interface {
dosomething()
}
// 首字母小写 私有的 不能导出
type singleton struct {
}
func (s *singleton) dosomething() {
fmt.Println("do some thing")
}
var (
once sync.Once
instance *singleton
)
func GetInstance() Singleton {
once.Do(
func() {
instance = &singleton{}
},
)
return instance
}
func main() {
s1 := GetInstance()
fmt.Printf("s1: %p\\n", s1)
s2 := GetInstance()
fmt.Printf("s2: %p\\n", s2)
}
抽象工厂
比如之前的工厂,你要苹果就给你苹果,要香蕉就给你香蕉,都是水果。
但是比如我们现在需要一个 windows 产品族,还有一个 Mac 产品族(Mac 风格的按键、文本框)
那么我们其实需要建两个工厂,一个 windows 工厂,一个 Mac 工厂,然后我们再抽象一下,再建一个总工厂的感觉。
然后你比如调用总工厂去生产一个按键,这样很方便啊!
package main
import "fmt"
// 1. 定义产品接口 (精简、独立)
type Button interface {
Click()
}
type Checkbox interface {
Check()
}
// 2. 定义【抽象工厂】接口 (这才是灵魂,规定了一个产品族必须包含什么)
type GUIFactory interface {
CreateButton() Button
CreateCheckbox() Checkbox
}
// ================= Mac 产品族实现 =================
type MacButton struct{}
func (b *MacButton) Click() { fmt.Println("点击 Mac 圆角按钮") }
type MacCheckbox struct{}
func (c *MacCheckbox) Check() { fmt.Println("勾选 Mac 蓝色复选框") }
// Mac 工厂 (隐式实现了 GUIFactory 接口)
type MacFactory struct{}
func (f *MacFactory) CreateButton() Button { return &MacButton{} }
func (f *MacFactory) CreateCheckbox() Checkbox { return &MacCheckbox{} }
// ================= Windows 产品族实现 =================
// (同理,略... WindowsButton, WindowsCheckbox, WindowsFactory)
// 3. 极简的“超级工厂”函数 (替代教程里那个可笑的 FactoryProducer)
func GetGUIFactory(osType string) (GUIFactory, error) {
switch osType {
case "mac":
return &MacFactory{}, nil
case "windows":
// return &WindowsFactory{}, nil
return nil, nil // 占位
default:
return nil, fmt.Errorf("不支持的操作系统类型: %s", osType)
}
}
// ================= 客户端代码 =================
func main() {
// 核心爽点:客户端代码一经写好,只需修改这里的参数,下面的所有产品全套自动切换!
factory, err := GetGUIFactory("mac")
if err != nil {
panic(err)
}
// 此时我知道 factory 是用来造 UI 的,但我根本不需要知道它是 Mac 还是 Windows
btn := factory.CreateButton()
chk := factory.CreateCheckbox()
// 生产出来的必定是同一套风格的产品
btn.Click()
chk.Check()
}
建造者(builder pattern)
为了避免以下写一大串构造函数的参数。
// 恶梦般的构造函数(俗称:伸缩型构造函数)
func NewCar(engine string, tire string, color string, spoiler bool, nos bool, bulletproof bool) *Car {
return &Car{...}
}
利用 ...Option 可变参数,把多个初始化函数打包成一个 slice 这样直接 for 遍历调用这些初始化函数即可。
package main
import (
"fmt"
"time"
)
// 1. 定义我们要构建的复杂对象
type Server struct {
Host string // 必填:主机地址
Port int // 必填:端口
Timeout time.Duration // 选配:超时时间 (默认 30秒)
MaxConn int // 选配:最大连接数 (默认 100)
TLS bool // 选配:是否开启加密 (默认 false)
}
// 2. 核心大招:定义一个“选项函数”类型 (它的作用就是去修改 Server 的内部状态)
type Option func(*Server)
// 3. 制造各种“选配包” (返回 Option 闭包函数)
func WithTimeout(t time.Duration) Option {
return func(s *Server) {
s.Timeout = t
}
}
func WithMaxConn(conn int) Option {
return func(s *Server) {
s.MaxConn = conn
}
}
func WithTLS() Option {
return func(s *Server) {
s.TLS = true
}
}
// 4. 终极构造函数:接收必填参数,加上无限个“选配包” (...Option 代表可变参数)
func NewServer(host string, port int, opts ...Option) *Server {
// 先给一个出厂默认配置
svr := &Server{
Host: host,
Port: port,
Timeout: 30 * time.Second,
MaxConn: 100,
TLS: false,
}
// 遍历用户传进来的所有“选配包”,挨个拆开,改装我们的 Server!
for _, opt := range opts {
opt(svr)
}
return svr
}
func main() {
// 场景 1:我只要一台丐版服务器 (全用默认选配)
s1 := NewServer("localhost", 8080)
fmt.Printf("丐版: %+v\n", s1)
// 场景 2:土豪客户,我要满配!(动态传入各种选配函数,极其优雅)
s2 := NewServer("api.github.com", 443,
WithTLS(),
WithTimeout(5*time.Second),
WithMaxConn(2000),
)
fmt.Printf("满配: %+v\n", s2)
}
原型模式(Prototype Pattern)
先搞出一个母本,然后剩下的就都是复制粘贴(利用深拷贝)这个母本。
适配器模式
利用 interface 去接收不同类型的支付方式,因为这个 interface 规定要有支付这个方法就 ok 了,然后你用 interface 变量接收后,调用 .支付() 一切完毕。至于你怎么把支付宝跟微信加上支付这个方法(对应接口)就用结构体写一下就好了(利用结构体把老的东西包起来,实现一个新的方法,这个有点适配器的意思,无论你原来长啥样,我包了一层后都有一个 .支付())

// 1. 公司内部规定的“Type-C”接口(Target)
type Payment interface {
Pay(amount float64) error
}
// ========= 支付宝的“转接头” =========
type AliPayAdapter struct {
client *AliPayClient // 内部把老接口包起来
}
// 实现我们公司规定的 Pay 方法
func (a *AliPayAdapter) Pay(amount float64) error {
// 在转接头内部,做一次“翻译”
fmt.Println("适配器启动:正在将内部 Pay 调用转换为 支付宝的 SendRequestToAli")
return a.client.SendRequestToAli(amount)
}
// ========= 微信支付的“转接头” =========
type WechatAdapter struct {
engine *WechatPayEngine
}
func (w *WechatAdapter) Pay(amount float64) error {
fmt.Println("适配器启动:正在将 float 元 转换为 int 分,并调用微信 DoWechatPay")
// 顺便把参数不兼容的问题也在这里偷偷解决了!
feeInCent := int(amount * 100)
return w.engine.DoWechatPay(feeInCent)
}
装饰器模式
利用闭包,将传入的函数对象绑定在对应的闭包单元格里,然后返回一个 新加1+老函数+新加2 三个部分的新函数,就实现了一个装饰效果。
// 🎁 装饰器 A:耗时统计装饰器
// 接收一个老函数,返回一个被包装过的新函数!
func WithTimeLog(handler HttpHandler) HttpHandler {
return func() { // 这个返回的闭包,就是一层“新衣服”
start := time.Now()
fmt.Println("-> [耗时装饰器: 开始] 记录起跑时间")
handler() // 执行被包裹在里面的核心业务
fmt.Printf("<- [耗时装饰器: 结束] 业务总耗时: %v\n", time.Since(start))
}
}
// 🎁 装饰器 B:登录鉴权装饰器
func WithAuth(handler HttpHandler) HttpHandler {
return func() {
fmt.Println("-> [鉴权装饰器: 开始] 检查 Token...")
// 假设 Token 没问题,继续往下走
handler()
fmt.Println("<- [鉴权装饰器: 结束] 扫尾工作")
}
}
func main() {
// 🎯 终极套娃开始!(这就叫洋葱模型)
// 我们把 CreateOrder 穿上鉴权衣服,再穿上耗时监控衣服
decoratedOrderApi := WithTimeLog(WithAuth(CreateOrder))
// 客户端一调用,奇迹发生了!
decoratedOrderApi()
}
并发编程
提示
搭配前面 go 基础中并发编程一块食用。
select
- select 就是看哪个没阻塞,然后在没阻塞的随机挑一个。
如果 select 有 3 个 case,然后此时有 2 个 case 的 channel 里面都有数据(都没阻塞),那么就会用个随机数生成器,随机挑一个 case 执行。
所以,当代码执行到 select 时:
它会去挨个敲门问所有的 case 通道:“你有东西给我拿吗?
如果都没东西,且没有 default,这个跑着 select 的协程就会原地卡住(阻塞休眠),把 CPU 让给别人。直到某一个通道被人塞进去了东西,它才会被瞬间唤醒。 case <-channel算真实的接收动作。本质就是case _=<-channel的简写。也就是说,每次你都会尝试往channel里面拿东西,如果拿到东西了,说明这个 case 就通了。make(chan int)无缓冲通道容量为 0。发送方发送某个数据会使得发送方定身,等待接收方。接收方接收,也类似。make(chan in,3)类似快递柜,只有当快递柜满了,就变成无缓冲通道了。- 当尝试读取一个已经关闭的 channel 的时候,不会阻塞,并且会读到这个 channel 元素类型对应的零值。
停止一个 goroutine
package main
import (
"context"
"fmt"
"time"
)
func printNum(ctx context.Context, n int) {
for i := 1; i <= n; i++ {
// select 是专门为 Channel 设计的“开关”。
// 它会同时监听下面所有的 case,谁先有数据(或被关闭),就执行谁。
select {
case <-ctx.Done():
// 核心:一旦 main 里的 cancel() 被调用,ctx.Done() 这个通道就会被关闭。
// 在 Go 里,从关闭的通道读数据会立刻返回。
// 所以,只要 cancel 执行了,这一行就会被“激活”!
fmt.Println("协程被取消")
return // 撤退!协程到此正式安全结束。
default:
// 如果上面的 case 没动静(即还没被取消),select 就会走 default。
// 这保证了协程不会死等取消信号,而是能继续干活。
fmt.Printf("子协程中的数字:%d\n", i)
time.Sleep(500 * time.Millisecond) // 模拟干活耗时
}
}
}
func main() {
// 1. context.Background():
// 这是“根”上下文。你可以把它想象成一棵树的树根,它永远不会被取消。
// 2. context.WithCancel(parent):
// 基于树根,派生出一个“带开关”的子节点。
// ctx 是传给小弟看的“令牌”;cancel 是留在老大(main)手里的“遥控器”。
ctx, cancel := context.WithCancel(context.Background())
go printNum(ctx, 10) // 把令牌发给小弟
time.Sleep(3 * time.Second) // 老大先歇 3 秒
// 3. cancel():
// 按下遥控器!瞬间,所有拿着相关令牌(ctx)的小弟,
// 都会在 select 里的 <-ctx.Done() 那里收到信号。
cancel()
time.Sleep(1 * time.Second)
fmt.Println("主协程退出")
}
go 并发安全数据类型
🏆 老兵总结:Go 语言里真正安全的只有这三个半!
忘掉那篇烂教程,把下面老兵总结的这“三个半”刻在脑子里。在 Go 的并发世界里,你只能无条件信任它们:
-
Channel(通道):Go 并发哲学的核心,天生为并发设计,随便跨协程读写,绝对安全。
-
sync 包里的神兵利器:sync.Mutex (锁), sync.WaitGroup, sync.Once, 以及专门为并发设计的 sync.Map。
-
被原子包 (sync/atomic) 保护的数字:用硬件指令强行续命的类型。
-
半个:只读不写的变量:只要一个变量初始化之后,所有人只读不写(比如配置文件加载后、不可变的字符串),那就是安全的。

浙公网安备 33010602011771号