Golang入门

一、关键字

Go语言中类似if和switch的关键字有25个,不能用于自定义名字,只能在特定语法结构中使用。

复制代码
break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

二、基本数据类型

类型分类 Go语言类型 默认值 Java对应类型 Java包装类 备注
布尔型 bool false boolean Boolean
整数类型 int 0 int Integer 平台相关(32或64位)
int8 0 byte Byte Go的int8范围同Java的byte(-128~127)
int16 0 short Short
int32 0 int Integer
int64 0 long Long
uint 0 无直接对应 Go特有无符号整数
uint8 0 无符号需求时用int Go的uint8常用作字节
uint16/uint32/uint64 0 无直接对应
byte 0 byte Byte byteuint8的别名
rune 0 char Character runeint32的别名,表示Unicode码点
浮点型 float32 0 float Float
float64 0 double Double
复数类型 complex64 (0+0i) 无直接对应 Go特有
complex128 (0+0i) 无直接对应
字符串 string ""(空字符串) String String Java的String是类而非基本类型
指针 指针类型(如*int nil 引用类型 Go指针更类似C/C++
其他引用类型 slice nil ArrayList/数组
map nil HashMap
channel nil 无直接对应 Go特有
function nil 函数式接口
interface nil Object/接口

三、声明常量和变量

1、常量(const关键字)

Go 复制代码
//声明一个常量
const a = 1

//声明多个常量
const (
	a = 1
	b = true
)

//声明可导出的常量
const (
	D = 10
	E = false
)

/*
常量的访问范围:
在 Go 中,常量的访问范围由其 首字母大小写 决定:
首字母大写:常量是 导出的(exported),可以被其他文件访问。
首字母小写:常量是 未导出的(unexported),只能在定义它的文件中访问。
*/

2、变量(格式:var 变量名字 类型 = 表达式)

Go 复制代码
//声明一个变量
var s string

//声明单个变量并初始化
var a int = 1
var b bool = true

//声明多个变量并初始化
var a, b = 10, false

//简短变量声明
 a, b := 10, false

//变量也可以调用函数并初始化
freq := rand.Float64() * 3.0

/*
1、在包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句被执行到时完成初始化。
2、":="是一个变量声明语句,而"="是一个变量赋值操作。
3、简短变量声明语句也可以用函数的返回值来声明和初始化变量
4、假设一个变量在上面已经声明了,那么下面针对这个变量的操作就是"赋值"行为
*/

3、指针(&表示取地址,*表示取值)

Go 复制代码
//示例1:&代表取地址,*代表取值
func main() {
	x := 1
	// 把x的地址赋值给p,相当于取x变量的内存地址。或者说"p指针保存了x变量的内存地址"
	p := &x
	fmt.Println(*p) // 那么*p就是x的值,输出"1"
	*p = 2          // 通过指针修改x的值
	fmt.Println(x)  // 那么x的值变成"2"
}
//示例2:
func main() {
	var x, y int
	fmt.Println(&x == &x)  // "true"   变量x地址相等
	fmt.Println(&x == &y)  // "false"  变量x,y地址不相等
	fmt.Println(&x == nil) // "false"  变量x地址不为空
}

//示例3:在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。
func f() *int {
	v := 1
	return &v
}
func main() {
	fmt.Println(f() == f()) // "false" 这里调用了两次f函数,虽然都是变量v,但是地址不一样
}

//示例4:指针包含了一个变量的地址,如果将指针作为参数调用函数,就可以在函数中通过该指针来更新变量的值。
func incr(p *int) int { //这里只改变了参数的变量值,并不改变p指针
	*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
	return *p
}
func main() {
	v := 1
	incr(&v)
	fmt.Println(v) // 所以这里第一次输出2
	fmt.Println(incr(&v)) // 在此调用+1输出了3
}

/*
1、变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受&取地址操作。
2、任何类型的指针的零值都是nil。如果p指向某个有效变量,那么p != nil测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。
*/

4、new函数(不是关键字)

new函数是创建变量的另一种方式,类似于一种语法糖,表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T。注意:由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。

Go 复制代码
func main() {
	p := new(int)   // 初始化变量p, 数据类型为int 类型,默认值为0
	fmt.Println(*p) // "0" 
	*p = 2          // 设置 int 匿名变量p的值为 2
	fmt.Println(*p) // "2" 
}

5、变量的生命周期

包一级:在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。

局部变量:局部变量的生命周期则是动态的,每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量,它们在函数每次被调用的时候创建。

垃圾回收:从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。(注意:和JVM的JIT逃逸分析类似,变量如果逃逸到函数之外,一般情况下会在栈上分配,也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间。未逃逸的就会在堆上分配。)

四、赋值

Go 复制代码
//示例1:最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。
func main() { //自增和自减是语句,而不是表达式
    var v int
	v = 1
	fmt.Println(v) // 输出 1
	v++            // 等价方式 v = v + 1;v 变成 2
	fmt.Println(v) // 输出 2
	v--            // 等价方式 v = v - 1;v 变成 1
	fmt.Println(v) // 输出 1
}
func main() { //直接交换x,y的值
	x, y := 3, 4
	fmt.Println("x:", x, "y:", y) // x: 3 y: 4
	x, y = y, x
	fmt.Println("x:", x, "y:", y) // x: 4 y: 3
}

//示例2:元组赋值
func main() { //直接交换a[0], a[1]的值
	a := []int{1, 2, 3, 4, 5}
	a[0], a[1] = a[1], a[0]
	fmt.Println(a) // 输出 [2 1 3 4 5]
}

func gcd(x, y int) int {
	for y != 0 {
		x, y = y, x%y
	}
	return x
}
func main() {
	fmt.Println("48 and 18 的最大公约数:", gcd(48, 18)) // 输出: 6
}

//示例3:多返回值必须用相同数量的变量接收,也就是左右变量数目一致,可以用下划线空白标识符_来丢弃不需要的值
func test() (int, int) {
	return 3, 4
}
func main() {
	a, b := test()
	fmt.Println(a, b) // 输出: 3 4
}


//示例4:显式赋值
func main() {
	medals := []string{"gold", "silver", "bronze"}
	// Medal 1: gold
	// Medal 2: silver
	// Medal 3: bronze
	for i, medal := range medals {
		fmt.Printf("Medal %d: %s\n", i+1, medal)
	}
}
func main() {
	medals := [3]string{}
	medals[0] = "gold"
	medals[1] = "silver"
	medals[2] = "bronze"
	// Medal 1: gold
	// Medal 2: silver
	// Medal 3: bronze
	for i, medal := range medals {
		fmt.Printf("Medal %d: %s\n", i+1, medal)
	}
}

五、复合数据类型

1、数组

数组的长度需要在编译阶段确定,一般是下面这种顺序初始化值

Go 复制代码
//声明数组有多种方式
func main() {
	//数组a
	a := []int{1, 2, 3}
	fmt.Println(a) // [1 2 3]
	//数组b
	b := [4]int{1, 2, 3, 4}
	fmt.Println(b) // [1 2 3 4]
	//数组c
	c := [...]int{1, 2, 3, 4}
	fmt.Println(c) // [1 2 3 4]
	//数组d
	d := make([]int, 4)
	d[0] = 1
	d[1] = 2
	d[2] = 3
	fmt.Println(d) // [1 2 3 0]
}

也可以指定一个索引和对应值列表的方式初始化

Go 复制代码
type Currency int

const (
	USD Currency = iota // 美元 跳过0
	EUR                 // 欧元 应该为1
	GBP                 // 英镑 应该为2
	RMB                 // 人民币 应该为3
)

func main() {
	symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
	fmt.Println(RMB, symbol[RMB]) // 输出:"3 ¥"
}

初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化

Go 复制代码
//定义了一个含有10个元素的数组r,最后一个元素被初始化为-1,其它元素都初始化为0
func main() {
	r := [...]int{9: -1}
	fmt.Println(r) // 输出: [0 0 0 0 0 0 0 0 0 -1]
}

当调用一个函数的时候,传递的是参数的副本,并不是原始对象,也就是说Go语言是值传递,并且Go语言传递大数组的效率比较低。所以在Go语言中,传递数组有以下三种方式:

Go 复制代码
//1、使用指针
func modifyArrayByPointer(arr *[3]int) {
	(*arr)[0] = 100 // 或 arr[0] = 100(语法糖)
}

func main() {
	arr := [3]int{1, 2, 3}
	modifyArrayByPointer(&arr)
	fmt.Println(arr) // [100 2 3]
}

//2、使用指针数组
// 对于非常大的数组, 使用指针传递以避免拷贝开销。这里为了测试设置比较小
func processLargeArray(arr *[5]int) {
	// 通过指针操作,避免拷贝
	for i := range arr {
		arr[i] = arr[i] * 2
	}
}

func main() {
	arr := [5]int{1, 2, 3}
	processLargeArray(&arr)
	fmt.Println(arr) // [2 4 6 0 0]
}

//3、使用切片slice
func modifySlice(s []int) {
	if len(s) > 0 {
		s[0] = 100 // 修改原始数据
	}
}

func main() {
	arr := [3]int{1, 2, 3}
	slice := arr[:] // 创建指向数组的切片
	modifySlice(slice)
	fmt.Println(arr) // 输出 [100 2 3]
}

2、Slice(切片--引用类型)

切片是一个轻量级的动态数组抽象,它不存储数据本身,而是:

  • 指向底层数组的指针(slice并没有指明序列的长度,而是会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组,返回的都是原始数组序列的子序列)

  • 长度(当前元素个数)

  • 容量(最大可容纳元素个数)

  • slice是间接引用,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素,唯一合法的比较操作是和nil比较

Go 复制代码
func nonempty(strings []string) []string {
	i := 0
	for _, s := range strings {
		if s != "" {
			strings[i] = s
			i++
		}
	}
	return strings[:i]
}

func main() {
	// 1. 从数组创建
	arr := [5]int{1, 2, 3, 4, 5}
	slice1 := arr[1:4]
	fmt.Println(slice1) // [2, 3, 4]

	// 2. 直接创建
	slice2 := []int{1, 2, 3}
	fmt.Println(slice2) // [1 2 3]

	// 3. 使用make函数
	slice3 := make([]int, 3, 5) // 长度3,容量5
	fmt.Println(slice3)         // [0 0 0]

	// 4. 从切片创建(共享底层数组)
	slice4 := slice1[1:3]
	fmt.Println(slice4) // [3 4]

	// 5. 不同方式创建空切片,并检查其长度和nil状态
	var slice5 []int         // len(slice5) == 0, slice5 == nil
	slice5 = nil             // len(slice5) == 0, slice5 == nil
	slice5 = []int(nil)      // len(slice5) == 0, slice5 == nil
	slice5 = []int{}         // len(slice5) == 0, slice5 != nil
	fmt.Println(len(slice5)) // 切片长度为:0

	// 6. slice之间不能比较,不能使用==操作符来判断两个slice是否含有全部相等元素
	// if slice1 == slice2 { // 编译错误:切片不能比较
	//     fmt.Println("slice1 和 slice2 相等")
	// }

	// 7. 使用append函数追加元素
	slice6 := []int{1, 2, 3}
	slice6 = append(slice6, 4, 5)
	fmt.Println(slice6) // [1 2 3 4 5]

	// 8. 使用copy函数复制切片
	src := []int{1, 2, 3}  // 源切片
	dest := make([]int, 2) // 目标切片,长度为2
	n := copy(dest, src)   // 复制元素
	fmt.Println(n, dest)   // 2 [1 2]

	// 9. 使用nonempty函数过滤空字符串
	data := []string{"one", "", "three"}
	// fmt.Printf("%q\n", nonempty(data)) // 输出 ["one" "three"]
	// fmt.Printf("%q\n", data)           // 输出 ["one" "three" "three"]
	data = nonempty(data)    // 重新赋值以保留过滤结果
	fmt.Printf("%q\n", data) // 输出 ["one" "three"]

	// 10. 一个slice可以用append函数来模拟一个stack
	var stack []int
	stack = append(stack, 1) // 入栈
	stack = append(stack, 2) // 入栈
	fmt.Println(stack)       // [1 2]

	x := stack[len(stack)-1] // 取出栈顶元素
	stack = stack[:len(stack)-1] // 出栈
	fmt.Println(x)           // 2
	fmt.Println(stack)       // [1]

	// 11. 遍历slice
	slice7 := []int{10, 20, 30}
	for i, v := range slice7 {
		fmt.Printf("索引 %d 的值是 %d\n", i, v)
		// 索引 0 的值是 10
		// 索引 1 的值是 20
		// 索引 2 的值是 30
	}
}

3、Map(并发不安全--引用类型)

  • Map是引用类型,零值为nil

  • 键必须是可比较的类型,值可以是任意类型

  • Map不是并发安全的,需要使用sync.Mutex或sync.Map

  • 遍历顺序是随机的

  • 使用delete()删除元素,删除不存在的键不会报错

  • **Go的Map底层是哈希表实现:

    • 多个桶(bucket),每个桶存储8个键值对
    • 使用链地址法解决哈希冲突
    • 负载因子达到6.5时触发扩容
    • 扩容时渐进式rehash**
Go 复制代码
func main() {
	// 声明方式1:使用make函数
	var m1 map[string]int     // 声明,此时m1为nil
	m1 = make(map[string]int) // 初始化
	m1["key1"] = 100
	fmt.Println(m1["key1"]) // 输出: 100

	// 声明方式2:字面量初始化
	m2 := map[string]int{
		"apple":  5,
		"banana": 3,
		"orange": 2,
	}
	fmt.Println(m2["banana"]) // 输出: 3

	// 声明方式3:指定初始容量
	m3 := make(map[string]int, 10) // 预分配容量,提升性能
	m3["item1"] = 50
	fmt.Println(m3["item1"]) // 输出: 50

	// 空map可以读取,但不能写入
	// m4["key"] = 1  // 运行时panic: assignment to entry in nil map
	var m4 map[string]int
	fmt.Println(m4 == nil)         // true
	fmt.Println(len(m4))           // 0
	fmt.Println(m4["nonexistent"]) // 读取不存在的键,返回零值: 0
	// m4是一个空指针,没有指向任何有效的hmap结构体。写入操作需要操作这个结构体的内部字段(buckets、count等),而nil指针无法安全访问这些字段。
	m4["nonexistent"] = 1 // 运行时panic: assignment to entry in nil map

	// 添加/修改
	m5 := make(map[string]int)
	m5["apple"] = 5
	m5["banana"] = 3
	m5["apple"] = 10 // 修改已有键的值
	// 查询
	value := m5["apple"]
	fmt.Println(value) // 10
	// 检查键是否存在
	value, exists := m5["orange"]
	if exists {
		fmt.Println("orange:", value)
	} else {
		fmt.Println("orange不存在") // orange不存在
	}
	// 删除
	delete(m5, "banana")
	fmt.Println(m5)           // map[apple:10]
	delete(m5, "nonexistent") // 删除不存在的键不会报错
	// 获取长度
	fmt.Println("map长度:", len(m5)) // map长度: 1

	// 遍历
	m6 := map[string]string{
		"red":   "#FF0000",
		"green": "#00FF00",
		"blue":  "#0000FF",
	}
	// 遍历所有键值对(顺序随机)
	for key, value := range m6 {
		fmt.Printf("%s: %s\n", key, value) // red: #FF0000  green: #00FF00  blue: #0000FF (顺序可能不同)
	}
	// 只遍历键
	for key := range m6 {
		fmt.Println("Key:", key) // red, green, blue
	}
	// 只遍历值
	for _, value := range m6 {
		fmt.Println("Value:", value) // #FF0000, #00FF00, #0000FF
	}
}
Go 复制代码
// 有效的键类型(可比较的类型):
// - 所有基本类型(除slice、map、function)
// - 数组(如果元素类型可比较)
// - 结构体(如果所有字段可比较)
// - 指针
// - 接口(如果动态值可比较)

// 无效的键类型(会导致编译错误):
// - slice
// - map
// - function
// - 包含上述类型的结构体

type Point struct {
	X, Y int
}

func main() {
	m := make(map[Point]string)
	m[Point{1, 2}] = "位置1"
	m[Point{3, 4}] = "位置2"
	println(m[Point{1, 2}]) // 输出: 位置1
	println(m[Point{3, 4}]) // 输出: 位置2
}
Go 复制代码
// 并发写会出现安全性问题
func main() {
	// 创建一个普通的 map
	m := make(map[int]int)

	// 两个 goroutine 并发写入同一个 map
	go func() {
		for i := 0; i < 1000; i++ {
			m[i] = i
		}
	}()

	go func() {
		for i := 1000; i < 2000; i++ {
			m[i] = i
		}
	}()

	time.Sleep(time.Second)
	// 可能触发:fatal error: concurrent map writes
}

// 方案1:使用sync.Mutex(推荐用于复杂场景)
// 结构体
type SafeMap struct {
	sync.RWMutex
	m map[string]int
}
// 构造函数:确保 map 被正确初始化
func NewSafeMap() *SafeMap {
	return &SafeMap{
		m: make(map[string]int),
	}
}
// set函数:设置键值对
func (sm *SafeMap) Set(key string, value int) {
	// 写锁保护
	sm.Lock()
	// 确保在函数退出时释放锁
	defer sm.Unlock()
	// 设置值
	sm.m[key] = value
}
// get函数:获取键对应的值
func (sm *SafeMap) Get(key string) (int, bool) {
	// 读锁保护
	sm.RLock()
	// 确保在函数退出时释放锁
	defer sm.RUnlock()
	// 获取值
	value, exists := sm.m[key]
	// 返回值和是否存在
	return value, exists
}
// 示例使用
func main() {
	// 创建 SafeMap 实例
	sm := NewSafeMap()
	// 设置和获取值
	sm.Set("test", 42)
	// 获取值
	if val, ok := sm.Get("test"); ok {
		println(val) // 输出 42
	}
}

// 方案2:使用sync.Map(适用于读多写少场景)
func main() {
	// 创建一个 sync.Map
	var syncMap sync.Map
	// 存储
	syncMap.Store("name", "测试")
	// 加载
	value, ok := syncMap.Load("name")
	fmt.Println(value, ok) // 输出: 测试 true
	value, ok = syncMap.Load("age")
	fmt.Println(value, ok) // 输出: <nil> false
	// 加载或存储
	actual, loaded := syncMap.LoadOrStore("name", "新值")
	fmt.Println(actual, loaded) // 输出: 测试 true
	actual, loaded = syncMap.LoadOrStore("age", 30)
	fmt.Println(actual, loaded) // 输出: 30 false
	// 更新
	syncMap.Store("name", "更新后的值")
	updatedValue, _ := syncMap.Load("name")
	fmt.Println(updatedValue) // 输出: 更新后的值
	// 删除
	syncMap.Delete("name") // 删除键为 "name" 的项
	// 遍历
	syncMap.Range(func(key, value interface{}) bool {
		fmt.Println("key:", key, "value:", value) // 输出: key: age value: 30
		return true
	})
}

4、结构体(深/浅拷贝)

  • 访问/修改字段:使用点操作符 .
  • 通过指针访问:Go 自动解引用
  • 如果所有字段都可比较(如基本类型、数组等,引用类型不可以),则结构体本身也可比较
  • 结构体不支持传统继承,嵌入实现组合复用(也就是常说的组合大于继承)
Go 复制代码
// User 声明一个可导出的结构体
type User struct {
	Name    string
	Age     int
	Sex     string
	Address Address // 嵌套结构体
	Address2        // 匿名成员
}

type Address struct {
	City    string
	Country string
}
type Address2 struct {
	City    string
	Country string
}

// DeepCopyJSON 深拷贝函数
func DeepCopyJSON(dst, src interface{}) error {
	// 序列化
	data, err := json.Marshal(src)
	if err != nil {
		return err
	}
	// 反序列化到新对象
	err = json.Unmarshal(data, dst)
	return err
}
func main() {
	// 结构体实例
	user := User{
		Name: "张三",
		Age:  18,
		Sex:  "男",
		Address: Address{
			City:    "北京",
			Country: "中国",
		},
	}
	fmt.Println(user.Name) // 张三
	fmt.Println(user.Age)  // 18
	fmt.Println(user.Sex)  // 男
	fmt.Println(user.Address.City)
	// 通过指针访问结构体成员
	user1 := &user
	// 这里会修改原始成员变量:原因是默认浅拷贝,指针都指向同一个结构体
	user1.Age = 19
	fmt.Println(user1.Name) // 张三
	fmt.Println(user.Age)   // 19
	fmt.Println(user1.Age)  // 19
	//1、手动深拷贝
	user2 := User{
		Name: user.Name,
		Age:  user.Age,
		Sex:  user.Sex,
		Address: Address{
			City:    user.Address.City,
			Country: user.Address.Country,
		},
	}
	user2.Age = 20
	fmt.Println(user2.Age)          // 20
	fmt.Println(user2.Address.City) // 北京
	//2、使用结构体指针深拷贝
	user3 := &user
	user3.Age = 21
	user3.Address.City = "上海"
	fmt.Println(user3.Age)          //
	fmt.Println(user3.Address.City) // 上海
	//3、序列化深拷贝
	var user4 User
	err := DeepCopyJSON(&user4, user)
	if err != nil {
		return
	}
	user4.Age = 22
	fmt.Println(user4.Age) // 22
}

5、JSON

  • Go 标准库 encoding/json 提供对 JSON 的编码(marshal) 和 解码(unmarshal) 支持
  • 解码时目标必须是指针,且JSON 中多余的字段会被自动忽略
  • 对于 HTTP 响应或大文件,推荐使用 json.Decoder / json.Encoder
  • 对可选字段使用 ,omitempty
Go 复制代码
type Product struct {
	ID          int       `json:"id"`
	Name        string    `json:"name"`
	Price       float64   `json:"price"`
	Tags        []string  `json:"tags,omitempty"` // 为空时忽略,omitempty:当字段为零值(如 false, 0, nil, "", 空 slice)时,不输出该字段
	CreatedAt   time.Time `json:"created_at"`
	SecretField string    `json:"-"` // 不序列化
}

func main() {
	product := Product{
		ID:          1,
		Name:        "product1",
		Price:       9.99,
		Tags:        []string{},
		CreatedAt:   time.Now(),
		SecretField: "secret",
	}
	// 编码
	marshal, _ := json.Marshal(product)
	println(string(marshal)) // {"id":1,"name":"product1","price":9.99,"created_at":"2025-12-17T16:57:38.654206+08:00"}
}

六、函数

1、函数声明

  • Go 没有默认参数、不支持函数重载
  • 调用时必须提供所有参数,且顺序固定(不能按名传参)
  • 命名返回值在复杂函数中可提高可读性,但在简单函数中可能冗余
  • 裸返回(return)在函数较长时可能降低可读性,建议谨慎使用
Go 复制代码
// 1、基本函数
func hypot(x, y float64) float64 {
	return math.Sqrt(x*x + y*y)
}

// 2、多返回值
func safeSqrt(x float64) (float64, error) {
	if x < 0 {
		return 0, errors.New("negative number")
	}
	return math.Sqrt(x), nil
}

// 3、命名返回值
func max(a, b int) (result int) {
	if a > b {
		result = a
	} else {
		result = b
	}
	return // 裸返回
}

// 4、忽略参数
func alwaysReturnFirst(_ int, y int) int {
	return y
}

// 5、引用类型参数可被修改
func resetSlice(s []int) {
	for i := range s {
		s[i] = 0 // 修改原 slice 元素
	}
}

//6、递归函数
func recursion(n int) int {
	if n <= 1 {
		return 1
	}
	return n * recursion(n-1)
}

func main() {
	fmt.Println(hypot(3, 4)) // 输出: 5

	if v, err := safeSqrt(-4); err != nil {
		fmt.Println("Error:", err) // 输出: Error: negative number
	} else {
		fmt.Println(v)
	}

	fmt.Println(max(10, 20))              // 输出: 20
	fmt.Println(alwaysReturnFirst(1, 99)) // 输出: 99

	nums := []int{1, 2, 3}
	resetSlice(nums)
	fmt.Println(nums) // 输出: [0 0 0]

    println(recursion(5)) // 输出:120
}

2、匿名函数

  • 可在函数内部定义(闭包)
  • 能捕获外部变量(形成闭包)
Go 复制代码
/**
 * 闭包
 * 匿名函数
 * 参数: int类型 n
 * 返回: int类型 的匿名函数
 */
func makeAdder(n int) func(int) int {
	// 返回一个匿名函数
	return func(x int) int {
		return x + n // 捕获 n
	}
}
func main() {
	adder := makeAdder(10)
	fmt.Println(adder(5)) // 15
}

3、可变参数函数

(...T) 表示可以接受多个该类型的函数,实际接收为 []T 切片

Go 复制代码
// 可变参数函数
func sum(nums ...int) int {
	total := 0
	// 遍历数组
	for _, n := range nums {
		total += n
	}
	return total
}
func main() {
	fmt.Println(sum(1, 2, 3, 4)) // 输出:10
	nums := []int{1, 2, 3}
	fmt.Println(sum(nums...)) // 输出:6
}

4、Deferred函数

  • defer 将函数调用推迟到外层函数返回前执行

  • 常用于资源清理(如关闭文件、解锁)

Go 复制代码
func readFile(filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}

	// 读取文件 
	_, err = f.Read(make([]byte, 1024))
	if err != nil {
		return err
	}
	defer f.Close() // 确保关闭
	
	return nil
}
func main() {
	if err := readFile("C:\\Pictures\\清乾隆 缂丝秋桃绶带图轴.jpg"); err != nil {
		panic(err)
	}
}

5、错误(error

  • 错误通过 error 接口返回(非异常)

  • 调用者必须显式检查错误

  • 避免使用 panic 处理常规错误

  • 推荐使用 fmt.Errorf 构造带上下文的错误信息。

    Go 复制代码
    func sqrt(x float64) (float64, error) {
    	if x < 0 {
    		// 推荐使用 fmt.Errorf 构造带上下文的错误信息。
    		return 0, fmt.Errorf("sqrt of negative number: %g", x)
    	}
    	return math.Sqrt(x), nil
    }

6、Panic异常(不可恢复的严重错误)

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起panic异常。

  • panic 终止当前函数并向上冒泡

  • 应仅用于不可恢复的严重错误(如程序 bug)

Go 复制代码
func mustOpen(name string) *os.File {
    f, err := os.Open(name)
    if err != nil {
        panic(err) // 不推荐用于常规错误
    }
    return f
}

7、Recover捕获异常

  • recover 只能在 defer 函数中调用

  • 可捕获 panic 并恢复程序执行

Go 复制代码
// safeCall 安全地调用指定的函数,捕获可能发生的恐慌并将其转换为错误返回
// 参数:
//   fn - 需要被安全调用的函数
// 返回值:
//   err - 如果函数执行过程中发生恐慌则返回包装后的错误,否则返回nil
func safeCall(fn func()) (err error) {
	// 使用defer和recover机制来捕获可能发生的恐慌
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("panic recovered: %v", r)
		}
	}()
	fn()
	return nil
}

func main() {
	safeCall(func() {
		panic("something went wrong") // 如果上面的函数没有defer和recover,就会报错:panic: something went wrong
	})
}

七、方法和接口

1、接口

  • 接口只包含方法签名,不包含实现
  • 可以嵌入其他接口(组合)
  • 只要一个类型实现了接口中的所有方法,就说明实现了该接口 --- 无需显式声明 implements
Go 复制代码
/*
定义接口Speaker,该接口有一个方法Speak()
在JAVA中需要先Implements实现这个接口,然后才能实现具体的方法。
在GO中不需要Implements,直接实现接口方法即可
*/
type Speaker interface {
	Speak() string
}

// 定义具体类型1
type Dog struct{}

// 实现接口1
func (d Dog) Speak() string { return "汪汪汪!" }

// 定义具体类型2
type Cat struct{}

// 实现接口2
func (c Cat) Speak() string { return "喵喵喵!" }

func main() {
	var s Speaker
	s = Dog{}
	fmt.Println(s.Speak()) // 汪汪汪!
	s = Cat{}
	fmt.Println(s.Speak()) // 喵喵喵!
}

2、方法

接收者 (Receiver) :方法声明时,在关键字 func 和函数名之间包含一个参数,称为接收者。

  • 值接收者 (Value Receiver):操作的是接收者的副本,不影响原对象。(只读操作,适用于小结构体)

  • 指针接收者 (Pointer Receiver):操作的是原对象的引用,操作会影响原对象。(读写操作,适用于大型结构体(避免拷贝开销))

绑定范围 :方法可以绑定到任何同一包内定义的具名类型 (Named Type)上,包括结构体、数值、字符串、甚至函数类型。但不能绑定到基础类型(如 int)或其它包中的类型。

组合与封装:通过在结构体中嵌入(Embedding)另一个类型,可以"继承"该类型的方法。

Go 复制代码
/*
定义接口Speaker,该接口有一个方法Speak()
在JAVA中需要先Implements实现这个接口,然后才能实现具体的方法。
在GO中不需要Implements,直接实现接口方法即可
*/
type Speaker interface {
	Speak() string
}

// ----------------------------
// 1. 值接收者:操作副本,不影响原对象
// ----------------------------
type Dog struct {
	Name  string
	count int // 用于记录 Speak 被调用次数(但不会真正更新原对象)
}

// 值接收者:d 是 Dog 的副本
func (d Dog) Speak() string {
	d.count++ // 修改的是副本的 count
	return fmt.Sprintf("%s(汪汪!已叫 %d 次)", d.Name, d.count)
}

// ----------------------------
// 2. 指针接收者:操作原对象,会修改它
// ----------------------------
type Bird struct {
	Name  string
	count int // 用于记录 Speak 被调用次数
}

// 指针接收者:b 指向原对象
func (b *Bird) Speak() string {
	b.count++ // 修改的是原对象的 count
	return fmt.Sprintf("%s(叽叽!已叫 %d 次)", b.Name, b.count)
}

func main() {
	// ====== 测试值接收者(Dog)======
	fmt.Println("=== Dog(值接收者)===")
	dog := Dog{Name: "旺财"}
	fmt.Println("第一次调用:", dog.Speak())     // 第一次调用: 旺财(汪汪!已叫 1 次)
	fmt.Println("第二次调用:", dog.Speak())     // 第二次调用: 旺财(汪汪!已叫 1 次)
	fmt.Println("原对象 count 值:", dog.count) // 仍然是 0!
	fmt.Println("=== Dog只修改了自己方法里面的count,原始值依旧是0 ===")

	// ====== 测试指针接收者(Bird)======
	fmt.Println("\n=== Bird(指针接收者)===")
	bird := Bird{Name: "小翠"}
	fmt.Println("第一次调用:", bird.Speak())     // 第一次调用: 小翠(叽叽!已叫 1 次)
	fmt.Println("第二次调用:", bird.Speak())     // 第二次调用: 小翠(叽叽!已叫 2 次)
	fmt.Println("原对象 count 值:", bird.count) // 已变为 2!
	fmt.Println("=== Bird是指针接收者,修改了原始值变为2 ===")

	// ====== 接口调用验证 ======
	fmt.Println("\n=== 通过接口调用 ===")
	var s Speaker

	s = dog                             // 值类型赋值给接口
	fmt.Println("接口调用 Dog:", s.Speak()) // 接口调用 Dog: 旺财(汪汪!已叫 1 次)

	s = &bird                                 // 注意:必须传指针才能满足接口(因为 Speak 在 *Bird 上)
	fmt.Println("接口调用 Bird:", s.Speak())      // 接口调用 Bird: 小翠(叽叽!已叫 3 次)
	fmt.Println("Bird 最终 count:", bird.count) // Bird 最终 count: 3
}

/**
=== Dog(值接收者)===
第一次调用: 旺财(汪汪!已叫 1 次)
第二次调用: 旺财(汪汪!已叫 1 次)
原对象 count 值: 0
=== Dog只修改了自己方法里面的count,原始值依旧是0 ===

=== Bird(指针接收者)===
第一次调用: 小翠(叽叽!已叫 1 次)
第二次调用: 小翠(叽叽!已叫 2 次)
原对象 count 值: 2
=== Bird是指针接收者,修改了原始值变为2 ===

=== 通过接口调用 ===
接口调用 Dog: 旺财(汪汪!已叫 1 次)
接口调用 Bird: 小翠(叽叽!已叫 3 次)
Bird 最终 count: 3
*/

八、Goroutines和Channels

1、Goroutine(协程)

  • 使用 go 关键字来启动一个协程
  • 轻量级:初始栈仅 2KB,可动态增长;成千上万个 goroutine 可同时运行。
  • 非阻塞:启动后立即返回,不等待函数执行完成。
  • 由 Go 运行时调度,运行在少量 OS 线程上(M:N 调度)。
  • 不要通过共享内存来通信,而应通过通信来共享内存
Go 复制代码
go f()        // 启动新 goroutine 执行 f()
go func() { ... }() // 启动匿名函数

2、Channel(通道)

  • 类型:chan T,用于在 goroutine 之间传递类型为 T 的值。

  • 同步机制:发送和接收操作默认是阻塞的,天然实现同步

  • 无缓冲通道:发送和接收必须同时就绪(同步点)。

  • 有缓冲通道:缓冲区满时发送阻塞,空时接收阻塞。

  • 关闭后:不能再发送(panic),但可继续接收剩余值,直到返回零值 + false

Go 复制代码
ch := make(chan int)      // 无缓冲通道
ch := make(chan int, 3)   // 有缓冲通道(容量 3)

3、select 语句(多路复用)

  • 类似 switch,但用于 channel 操作。
  • 可同时监听多个 channel 的发送/接收。
  • default 分支实现非阻塞操作。
Go 复制代码
// 该函数演示了Go语言中select语句的使用,用于在多个channel操作之间进行选择
func main() {
	// 初始化变量和channel
	x := 1
	ch1 := make(chan int, 3)
	ch2 := make(chan int, 3)

	// 使用select语句在多个channel操作之间进行非阻塞选择
	// select会随机选择一个可以执行的case,如果没有任何case可以执行则执行default分支
	select {
	case v := <-ch1:
		fmt.Println("Received from ch1:", v)
	case ch2 <- x:
		fmt.Println("Sent to ch2")
	default:
		fmt.Println("No communication ready")
	}
}

4、总结

1、常见并发模式:

  1. 生成器(Generator):goroutine 产生数据,通过 channel 输出。
  2. 工作池(Worker Pool):多个 worker 从任务队列(channel)消费。
  3. 扇入/扇出(Fan-in/Fan-out):合并多个输入 / 分发到多个处理者。
  4. 超时控制:结合 time.After 实现

|---------------------------------------|-----------------------|----------------------------------|
| 主 goroutine 退出,所有 goroutine 被强制终止 | 程序不会等待后台 goroutine 完成 | 使用 sync.WaitGroup 或 channel 同步 |
| 向已关闭的 channel 发送 → panic | | 只由发送方关闭 channel |
| 重复关闭 channel → panic | | 确保只关闭一次 |
| 无缓冲 channel 需要配对操作 | 单独发送或接收会永久阻塞 | 确保有对应的接收/发送方 |
| 忘记读取 channel 导致 goroutine 泄漏 | | 使用带缓冲 channel 或确保消费 |
| 在循环中启动 goroutine 捕获循环变量 | 闭包捕获的是变量地址,值可能变化 | 显式传参或复制变量 |

2、示例 1:基础 goroutine + channel 同步

Go 复制代码
// say函数用于打印指定字符串3次,每次间隔100毫秒,并通过done通道通知完成
// 参数s: 要打印的字符串
// 参数done: 用于通知任务完成的布尔类型通道
func say(s string, done chan bool) {
	// 循环打印字符串3次
	for i := 0; i < 3; i++ {
		fmt.Println(s)
		time.Sleep(100 * time.Millisecond)
	}
	// 向done通道发送true值,表示任务已完成
	done <- true
}


// main 函数是程序的入口点,演示了 goroutine 的并发执行
// 通过 channel 实现主 goroutine 和子 goroutine 之间的同步
func main() {
	// 创建一个布尔类型的 channel 用于 goroutine 间通信和同步
	done := make(chan bool)
	
	// 启动一个子 goroutine 执行 say 函数,传入 "world" 和 done channel
	go say("world", done)
	
	// 主 goroutine 直接执行 say 函数,传入 "hello" 和 nil
	say("hello", nil)
	
	// 阻塞等待子 goroutine 完成,从 done channel 接收值
	<-done
}

3、示例 2:工作池(Worker Pool)

Go 复制代码
// worker 是一个工作协程函数,用于处理作业队列中的任务
// id: 工作协程的唯一标识符
// jobs: 只读通道,用于接收待处理的作业任务
// results: 只写通道,用于发送处理结果
// wg: 等待组,用于协调所有工作协程的完成
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	// 循环处理作业队列中的任务,直到通道关闭
	for job := range jobs {
		fmt.Printf("Worker %d processing job %d\n", id, job)
		time.Sleep(time.Second) // 模拟耗时
		results <- job * 2
	}
}

// 该函数启动多个worker goroutine来处理作业,并收集处理结果
func main() {
	const numJobs = 5
	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)

	var wg sync.WaitGroup
	// 启动3个worker goroutine来处理作业
	for w := 1; w <= 3; w++ {
		wg.Add(1)
		go worker(w, jobs, results, &wg)
	}

	// 发送任务
	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs)

	// 等待所有 worker 结束
	wg.Wait()
	close(results)

	// 收集结果
	for res := range results {
		fmt.Println("Result:", res)
	}
}

4、示例 3:超时控制(select + time.After)

Go 复制代码
// 该函数演示了带超时机制的goroutine通信
// 通过channel和select语句实现主goroutine与子goroutine之间的数据传递和超时控制
func main() {
	// 创建一个无缓冲的字符串channel,用于goroutine间通信
	ch := make(chan string)

	// 启动一个匿名goroutine函数
	go func() {
		// 子goroutine休眠2秒模拟耗时操作
		time.Sleep(2 * time.Second)
		// 向channel发送结果数据
		ch <- "result"
	}()

	// 使用select语句实现超时控制
	// 监听多个channel操作,执行第一个准备好的操作
	select {
	// 从ch通道接收数据的情况
	case res := <-ch:
		fmt.Println("Got:", res)
	// 超时情况:After函数返回的通道在指定时间后可读
	case <-time.After(1 * time.Second):
		fmt.Println("Timeout!")
	}
} // 输出: Timeout!

5、示例4:实现简单聊天室

Go 复制代码
package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"strings"
)

// 消息类型:包含内容和来源
type clientMessage struct {
	text string
	user string
}

// 全局广播通道
var (
	entering = make(chan *client) // 用户进入
	leaving  = make(chan *client) // 用户离开
	messages = make(chan clientMessage) // 消息广播
)

// 客户端结构
type client struct {
	name    string
	conn    net.Conn
	message chan<- clientMessage // 发送消息到该客户端的通道(只写)
}

// 广播协程:管理在线用户列表并转发消息
func broadcaster() {
	clients := make(map[*client]bool) // 当前在线用户
	for {
		select {
		case msg := <-messages:
			// 广播消息给所有客户端
			for cli := range clients {
				cli.message <- msg
			}
		case cli := <-entering:
			// 新用户加入
			clients[cli] = true
			msg := fmt.Sprintf("当前在线用户 (%d): ", len(clients))
			var names []string
			for c := range clients {
				names = append(names, c.name)
			}
			msg += strings.Join(names, ", ")
			for cli := range clients {
				cli.message <- clientMessage{text: msg, user: "[系统]"}
			}
		case cli := <-leaving:
			// 用户离开
			delete(clients, cli)
			close(cli.message)
			msg := fmt.Sprintf("[系统] %s 已离开。当前在线: %d 人", cli.name, len(clients))
			for c := range clients {
				c.message <- clientMessage{text: msg, user: "[系统]"}
			}
		}
	}
}

// 客户端处理协程
func handleConn(conn net.Conn) {
	defer conn.Close()

	// 设置用户名
	scanner := bufio.NewScanner(conn)
	conn.Write([]byte("请输入你的昵称: "))
	if !scanner.Scan() {
		return
	}
	name := strings.TrimSpace(scanner.Text())
	if name == "" {
		name = "匿名用户"
	}

	// 创建客户端
	clientChan := make(chan clientMessage) // 用于向该客户端发送消息
	cli := &client{
		name:    name,
		conn:    conn,
		message: clientChan,
	}

	// 通知 broadcaster 有新用户加入
	entering <- cli

	// 启动一个 goroutine 向客户端写消息
	go func() {
		for msg := range clientChan {
			fmt.Fprintf(conn, "[%s] %s\n", msg.user, msg.text)
		}
	}()

	// 读取用户输入并广播
	messages <- clientMessage{text: "已加入聊天室", user: name}
	input := bufio.NewScanner(conn)
	for input.Scan() {
		text := strings.TrimSpace(input.Text())
		if text == "/quit" {
			break
		}
		messages <- clientMessage{text: text, user: name}
	}

	// 用户退出
	leaving <- cli
	messages <- clientMessage{text: "已离开聊天室", user: name}
}

func main() {
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal(err)
	}
	defer listener.Close()

	fmt.Println("聊天服务器启动,监听端口 8080...")
	go broadcaster()

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		go handleConn(conn) // 每个连接一个 goroutine
	}
}
bash 复制代码
#测试:

# 终端 1
telnet localhost 8080
# 输入昵称:Alice

# 终端 2
telnet localhost 8080
# 输入昵称:Bob

九、锁

1、竞争条件(Race Condition)

  • 定义:多个 goroutine 并发读写同一变量,结果依赖于执行时序。
  • 后果:程序行为不可预测(崩溃、数据错乱、静默错误)。
  • 检测工具go run -race(竞态检测器)
Go 复制代码
var balance = 100

// Deposit 向账户中存入指定金额
// amount: 存入的金额
// wg: 用于同步的等待组,函数执行完毕后会调用Done方法
func Deposit(amount int, wg *sync.WaitGroup) {
	defer wg.Done()
	// 多个 goroutine 同时修改balance变量,存在竞态条件风险
	balance += amount
}

// 该函数创建两个goroutine来执行存款操作,并等待它们完成
// 最后打印出最终的余额,但由于竞态条件,结果是不确定的
func main() {
	// 创建WaitGroup用于等待所有goroutine完成
	var wg sync.WaitGroup
	
	// 设置需要等待的goroutine数量为2
	wg.Add(2)
	
	// 启动第一个goroutine,存入10元
	go Deposit(10, &wg)
	
	// 启动第二个goroutine,存入20元
	go Deposit(20, &wg)
	
	// 等待所有goroutine完成
	wg.Wait()
	
	// 打印最终余额,由于存在竞态条件,结果可能是110、120或130等不确定值
	fmt.Println("Final balance:", balance) // 可能是 110、120、130?不确定!
}

2、sync.Mutex 互斥锁

  • 最基本的同步原语:同一时间只允许一个 goroutine 访问临界区
  • 方法:Lock() / Unlock()
  • 必须配对使用,推荐用 defer
Go 复制代码
// Deposit 向账户中存入指定金额
// 参数:
//	amount: 要存入的金额
//	wg: 用于同步的WaitGroup指针,函数执行完毕后会调用Done方法
func Deposit(amount int, wg *sync.WaitGroup) {
	defer wg.Done()
	// 加锁保护共享资源balance
	mu.Lock()
	defer mu.Unlock()
	balance += amount
}

// 该函数创建两个goroutine来执行存款操作,并等待它们完成
// 最后打印出最终的余额,但由于竞态条件,结果是不确定的
func main() {
	// 创建WaitGroup用于等待所有goroutine完成
	var wg sync.WaitGroup

	// 设置需要等待的goroutine数量为2
	wg.Add(2)

	// 启动第一个goroutine,存入10元
	go Deposit(10, &wg)

	// 启动第二个goroutine,存入20元
	go Deposit(20, &wg)

	// 等待所有goroutine完成
	wg.Wait()

	// 打印最终余额,现在使用了互斥锁,确保了结果是正确的
	fmt.Println("Final balance:", balance) // Final balance: 130
}

3、sync.RWMutex 读写锁

  • 适用于 读多写少 场景。
  • 允许多个 reader 同时读,但 writer 必须独占。
  • 方法:
  • RLock() / RUnlock()(读锁)
  • Lock() / Unlock()(写锁)
Go 复制代码
type SafeCache struct {
	mu    sync.RWMutex
	cache map[string]string
}

// Get 从缓存中获取与给定键关联的值
// 该方法是并发安全的,因为它使用了读锁
//
// 参数:
//   - key: 要在缓存中查找的字符串键
//
// 返回值:
//   - string: 与键关联的值,如果未找到键则返回空字符串
func (c *SafeCache) Get(key string) string {
	// 获取读锁以允许多个goroutine同时读取
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.cache[key]
}

// Set 在缓存中设置键值对
// 该方法是线程安全的,使用互斥锁确保并发访问安全
//
// 参数:
//   key:   用于存储值的字符串键
//   value: 要存储在缓存中的字符串值
func (c *SafeCache) Set(key, value string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.cache[key] = value
}

4、内存同步(Memory Synchronization)

  • 关键点:锁不仅保护临界区,还保证内存可见性
  • 没有同步的情况下,一个 goroutine 的写可能对另一个不可见(CPU 缓存、编译器优化)
  • sync 包的操作(如 mutex、channel)会建立 "happens-before" 关系,确保内存同步
  • 不要用"volatile"或"原子读"来替代锁 ------ Go 没有 volatile,普通读写不保证可见性

5、sync.Once 惰性初始化

  • 确保某段代码只执行一次,即使被多个 goroutine 调用
  • 常用于单例、配置加载、连接池初始化
Go 复制代码
var (
	once   sync.Once
	dbConn *DB
)

func GetDB() *DB {
	once.Do(func() {
		dbConn = connectToDB() // 只会执行一次
	})
	return dbConn
}

6、竞争条件检测

  • 使用 -race 标志编译/运行

  • 只能检测已发生的竞争,不能证明无竞争

  • 性能开销大(2~20x),仅用于开发/测试

Go 复制代码
go run -race main.go
go test -race ./...
go build -race

7、示例:并发的非阻塞缓存

结合 map + mutex + 函数闭包 实现带缓存的函数(memoization

Go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

type Memo struct {
	f     Func
	cache map[string]*entry
	mu    sync.RWMutex
}

type Func func(key string) (interface{}, error)

type entry struct {
	res   result
	ready chan struct{} // 用于等待结果就绪
}

type result struct {
	value interface{}
	err   error
}

// New 创建一个新的 Memo 实例,使用给定的函数进行初始化
// 它会初始化缓存映射表,用于存储记忆化的结果
//
// 参数:
//   f: 需要被记忆化的函数
//
// 返回值:
//   指向新创建的 Memo 实例的指针
func New(f Func) *Memo {
	return &Memo{f: f, cache: make(map[string]*entry)}
}

// Get 是线程安全的
// Get 从缓存中获取指定键的值,如果键不存在则执行慢函数计算结果并缓存
// key: 要获取值的键
// value: 获取到的值
// err: 获取过程中可能发生的错误
func (memo *Memo) Get(key string) (value interface{}, err error) {
	memo.mu.RLock()
	e := memo.cache[key]
	memo.mu.RUnlock()

	if e != nil {
		// 已存在:等待结果就绪(防止重复计算)
		<-e.ready
		return e.res.value, e.res.err
	}

	// 不存在:加写锁,双重检查
	memo.mu.Lock()
	e = memo.cache[key]
	if e == nil {
		// 第一次创建 entry
		e = &entry{ready: make(chan struct{})}
		memo.cache[key] = e
		memo.mu.Unlock()

		// 执行慢函数
		e.res.value, e.res.err = memo.f(key)
		close(e.ready) // 通知所有等待者
	} else {
		// 其他 goroutine 刚刚创建了 entry
		memo.mu.Unlock()
		<-e.ready
	}
	return e.res.value, e.res.err
}

// 参数:
//   key - 用于生成结果的输入字符串
// 返回值:
//   interface{} - 基于输入key生成的格式化结果字符串
//   error - 错误信息,当前实现始终返回nil
func slowFunc(key string) (interface{}, error) {
	// 模拟耗时操作,延迟1秒
	time.Sleep(1 * time.Second)
	return fmt.Sprintf("result for %s", key), nil
}

//Memoization模式的并发使用
// 该函数创建了一个带缓存的函数调用器,并通过多个goroutine并发调用
// 来展示缓存机制如何避免重复计算
func main() {
	memo := New(slowFunc)

	// 启动多个goroutine并发调用memo.Get方法
	// 通过WaitGroup等待所有goroutine执行完成
	// 所有goroutine都使用相同的key"test"来测试缓存效果
	var wg sync.WaitGroup
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func(key string) {
			defer wg.Done()
			res, _ := memo.Get(key)
			fmt.Println(res)
		}("test")
	}
	wg.Wait()
}

十、反射

反射:在运行时检查变量的类型和值,并能动态调用方法、修改字段

Go 的反射基于两个核心类型:

  • reflect.Type:表示类型的元信息(如名称、字段、方法等)
  • reflect.Value:表示值的可操作封装(可读、可写、可调用)

修改变量的值时必须传递指针(reflect.ValueOf(x).SetInt(20) → panic(不可寻址))

Go 复制代码
func main() {
	var x = 10
	t := reflect.TypeOf(x) // Type: int
	// ValueOf 返回的是副本,若想修改原变量,必须传指针
	v := reflect.ValueOf(x) // Value: 10

	fmt.Println(t.Name()) // 输出:"int"
	fmt.Println(v.Int())  // 输出:10
	// Elem() 解引用指针,使用指针修改变量值
	v1 := reflect.ValueOf(&x).Elem() // Value: 10
	if v1.CanSet() {
		v1.SetInt(20)
	}
	// 简写:reflect.ValueOf(&x).Elem().SetInt(20)
	fmt.Println(x) // 输出:20
}

使用反射遍历结构体字段(含标签)

Go 复制代码
type User struct {
	Name string `json:"name" validate:"required"`
	Age  int    `json:"age"`
}

// 通过反射获取其字段名、值和JSON标签
func main() {
	u := User{"Alice", 30}
	v := reflect.ValueOf(u)
	t := reflect.TypeOf(u)

	// 遍历结构体的所有字段,输出字段名、值和JSON标签
	for i := 0; i < v.NumField(); i++ {
		field := v.Field(i)
		fieldType := t.Field(i)
		fmt.Printf("字段名: %s\n", fieldType.Name)
		fmt.Printf("值: %v\n", field.Interface())
		fmt.Printf("JSON 标签: %s\n", fieldType.Tag.Get("json"))
	}
}

使用反射动态调用方法

Go 复制代码
type Greeter struct{}

func (g Greeter) SayHello(name string) string {
	return "Hello, " + name
}

// 创建Greeter实例,通过反射获取并调用其SayHello方法,最后输出方法调用结果
func main() {
	g := Greeter{}
	v := reflect.ValueOf(g)

	// 获取指定名称的方法
	method := v.MethodByName("SayHello")
	if !method.IsValid() {
		panic("方法不存在")
	}

	// 准备方法调用的参数并执行方法调用
	args := []reflect.Value{reflect.ValueOf("Go")}
	results := method.Call(args)

	// 输出方法返回的结果
	fmt.Println(results[0].String()) // 输出:Hello, Go
}

创建新实例(类似 new(T))

Go 复制代码
type User struct {
	Name string `json:"name" validate:"required"`
	Age  int    `json:"age"`
}

func main() {
	var u User
	t := reflect.TypeOf(u)

	// 创建指针:相当于 new(User)
	ptr := reflect.New(t)
	elem := ptr.Elem() // 获取 *User 指向的 User 值

	// 设置字段
	elem.FieldByName("Name").SetString("Bob")
	elem.FieldByName("Age").SetInt(25)

	// 转回 interface{}
	newUser := ptr.Interface().(*User)
	fmt.Printf("%+v\n", *newUser) // {Name:Bob Age:25}
}
相关推荐
小徐Chao努力3 小时前
【GO】Gin 框架从入门到精通完整教程
开发语言·golang·gin
bybitq5 小时前
string,byte,rune,character?详解Golang编码-UTF-8
开发语言·后端·golang
robin59116 小时前
Rabbitmq-Golang使用简单模式
分布式·golang·rabbitmq
weixin_4624462317 小时前
用 Go 快速搭建一个 Coze (扣子)API 流式回复模拟接口(Mock Server)
开发语言·golang·状态模式
李迟18 小时前
Golang实践录:接口文档字段转结构体定义
开发语言·golang
资深web全栈开发20 小时前
Casbin 权限管理深度解析:优势与最佳实践
golang·casbin·权限设计·go库介绍
古城小栈1 天前
Go + 边缘计算:工业质检 AI 模型部署实践指南
人工智能·golang·边缘计算
ChineHe1 天前
Gin框架基础篇001_路由与路由组详解
后端·golang·gin
laozhoy11 天前
深入理解Go语言errors.As方法:灵活的错误类型识别
开发语言·后端·golang