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

  1. go 里的 switch 不需要 break

  2. 默认只执行一个语句,匹配到一个 case 就跑路了!

  3. 加逗号,表示任意匹配上一个你就进去这个 case

  4. 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

image

多个 goroutine 是并发的,但是在实际体感上我觉得跟并行差不多。
因为单核的话就切成很多个时间片嘛。然后多核其实就直接并行了,一个协程放一边。

channel

channel 用于 goroutine 间通信。

  1. 无缓冲通道

    ch := make(chan int)  // 创建一个无缓冲通道
    

    类比两个人手递手,发送方伸出手表示发送,但是要等待另一个人也伸出手。
    接收方伸出手接收,但是要等待发送方也伸出手发送,

  2. 缓冲通道

    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 同步
为了解决数据竞争的问题。

  1. 数据竞争

    // 错误示例:没有锁
    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
    
  2. 互斥锁
    就是保证了,一个时间只能有一个 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 变量接收后,调用 .支付() 一切完毕。至于你怎么把支付宝跟微信加上支付这个方法(对应接口)就用结构体写一下就好了(利用结构体把老的东西包起来,实现一个新的方法,这个有点适配器的意思,无论你原来长啥样,我包了一层后都有一个 .支付())
image

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

  1. select 就是看哪个没阻塞,然后在没阻塞的随机挑一个。
    如果 select 有 3 个 case,然后此时有 2 个 case 的 channel 里面都有数据(都没阻塞),那么就会用个随机数生成器,随机挑一个 case 执行。
    所以,当代码执行到 select 时:
    它会去挨个敲门问所有的 case 通道:“你有东西给我拿吗?
    如果都没东西,且没有 default,这个跑着 select 的协程就会原地卡住(阻塞休眠),把 CPU 让给别人。直到某一个通道被人塞进去了东西,它才会被瞬间唤醒。
  2. case <-channel 算真实的接收动作。本质就是 case _=<-channel 的简写。也就是说,每次你都会尝试往 channel 里面拿东西,如果拿到东西了,说明这个 case 就通了。
  3. make(chan int) 无缓冲通道容量为 0。发送方发送某个数据会使得发送方定身,等待接收方。接收方接收,也类似。
  4. make(chan in,3) 类似快递柜,只有当快递柜满了,就变成无缓冲通道了。
  5. 当尝试读取一个已经关闭的 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 的并发世界里,你只能无条件信任它们:

  1. Channel(通道):Go 并发哲学的核心,天生为并发设计,随便跨协程读写,绝对安全。

  2. sync 包里的神兵利器:sync.Mutex (锁), sync.WaitGroup, sync.Once, 以及专门为并发设计的 sync.Map。

  3. 被原子包 (sync/atomic) 保护的数字:用硬件指令强行续命的类型。

  4. 半个:只读不写的变量:只要一个变量初始化之后,所有人只读不写(比如配置文件加载后、不可变的字符串),那就是安全的。

posted @ 2026-04-08 21:48  FxorG  阅读(17)  评论(0)    收藏  举报