基础语法
变量声明
Go 语言有多种声明变量的方式:
package main
import "fmt"
func main() {
// 方式1:使用 var 关键字
var name string = "Go"
var age int = 10
// 方式2:类型推断
var language = "Golang"
// 方式3:短变量声明(最常用)
version := "1.21"
// 方式4:声明多个变量
var (
x int = 1
y int = 2
)
fmt.Println(name, age, language, version, x, y)
}
基本数据类型
数值类型
package main
import "fmt"
func main() {
// 整型
var i int = 42
var i8 int8 = 127
var i16 int16 = 32767
var i32 int32 = 2147483647
var i64 int64 = 9223372036854775807
// 无符号整型
var u uint = 42
var u8 uint8 = 255
var u16 uint16 = 65535
var u32 uint32 = 4294967295
var u64 uint64 = 18446744073709551615
// 浮点型
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793
// 复数
var c64 complex64 = 1 + 2i
var c128 complex128 = 1 + 2i
fmt.Println(i, i8, i16, i32, i64)
fmt.Println(u, u8, u16, u32, u64)
fmt.Println(f32, f64)
fmt.Println(c64, c128)
}
字符串和布尔类型
package main
import "fmt"
func main() {
// 字符串
var str string = "Hello, Go!"
message := "Welcome to Go programming"
// 多行字符串
multiline := `这是一个
多行字符串
示例`
// 布尔类型
var isTrue bool = true
isFalse := false
fmt.Println(str)
fmt.Println(message)
fmt.Println(multiline)
fmt.Println(isTrue, isFalse)
}
常量
常量使用 const 关键字声明,值在编译时确定且不可修改。
package main
import "fmt"
func main() {
// 单个常量
const PI = 3.14159
const MaxConnections = 100
// 常量组
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)
fmt.Println(PI, MaxConnections)
fmt.Println(StatusOK, StatusNotFound, StatusError)
}
运算符
算术运算符
package main
import "fmt"
func main() {
a, b := 10, 3
fmt.Println("加法:", a + b) // 13
fmt.Println("减法:", a - b) // 7
fmt.Println("乘法:", a * b) // 30
fmt.Println("除法:", a / b) // 3
fmt.Println("取余:", a % b) // 1
}
比较运算符
package main
import "fmt"
func main() {
a, b := 10, 20
fmt.Println("等于:", a == b) // false
fmt.Println("不等于:", a != b) // true
fmt.Println("小于:", a < b) // true
fmt.Println("小于等于:", a <= b) // true
fmt.Println("大于:", a > b) // false
fmt.Println("大于等于:", a >= b) // false
}
逻辑运算符
package main
import "fmt"
func main() {
a, b := true, false
fmt.Println("与:", a && b) // false
fmt.Println("或:", a || b) // true
fmt.Println("非:", !a) // false
}
字符串操作
package main
import (
"fmt"
"strings"
)
func main() {
str := "Hello, Go Programming!"
// 字符串长度
fmt.Println("长度:", len(str))
// 字符串拼接
str2 := str + " Welcome!"
fmt.Println("拼接:", str2)
// 字符串分割
parts := strings.Split(str, ", ")
fmt.Println("分割:", parts)
// 字符串包含
fmt.Println("包含 Go:", strings.Contains(str, "Go"))
// 字符串替换
replaced := strings.Replace(str, "Go", "Golang", 1)
fmt.Println("替换:", replaced)
// 大小写转换
fmt.Println("大写:", strings.ToUpper(str))
fmt.Println("小写:", strings.ToLower(str))
// 去除空格
str3 := " Hello "
fmt.Println("去除空格:", strings.Trim(str3, " "))
}
数组
package main
import "fmt"
func main() {
// 声明数组
var arr1 [5]int
arr1[0] = 1
arr1[1] = 2
// 数组初始化
arr2 := [5]int{1, 2, 3, 4, 5
// 让编译器计算长度
arr3 := [...]int{1, 2, 3}
fmt.Println("数组1:", arr1)
fmt.Println("数组2:", arr2)
fmt.Println("数组3:", arr3)
fmt.Println("数组2长度:", len(arr2))
}
切片(Slice)
package main
import "fmt"
func main() {
// 创建切片
slice1 := []int{1, 2, 3, 4, 5
// 使用 make 创建切片
slice2 := make([]int, 3, 5)
// 切片操作
fmt.Println("完整切片:", slice1)
fmt.Println("子切片 [1:3]:", slice1[1:3])
fmt.Println("子切片 [:3]:", slice1[:3])
fmt.Println("子切片 [2:]:", slice1[2:])
// 添加元素
slice1 = append(slice1, 6, 7)
fmt.Println("追加后:", slice1)
// 切片长度和容量
fmt.Println("长度:", len(slice1))
fmt.Println("容量:", cap(slice1))
}
切片的内部结构
切片在 Go 内部是一个结构体,包含三个字段:
type sliceHeader struct {
ptr unsafe.Pointer // 指向底层数组的指针
len int // 切片长度(可访问的元素数量)
cap int // 切片容量(底层数组从 ptr 开始的总大小)
}
切片是对底层数组的引用,多个切片可以共享同一个底层数组。修改一个切片会影响其他共享底层数组的切片。
切片的实现原理
1. 切片的创建:
// 字面量创建
s := []int{1, 2, 3} // 创建切片和底层数组
// make 创建
s := make([]int, 3, 5) // len=3, cap=5
// 从数组/切片创建
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // 共享 arr 的底层数组
2. 切片的扩容机制:
当使用 append 添加元素且容量不足时,Go 会自动扩容。扩容策略如下:
package main
import "fmt"
func main() {
s := []int{1}
fmt.Printf("len=%d, cap=%d
", len(s), cap(s)) // len=1, cap=1
s = append(s, 2)
fmt.Printf("len=%d, cap=%d
", len(s), cap(s)) // len=2, cap=2
s = append(s, 3)
fmt.Printf("len=%d, cap=%d
", len(s), cap(s)) // len=3, cap=4 (扩容)
s = append(s, 4, 5)
fmt.Printf("len=%d, cap=%d
", len(s), cap(s)) // len=5, cap=8 (扩容)
}
扩容规则(Go 1.20+):
- 新容量 ≥ 2×旧容量
- 新容量 ≥ 旧容量 + 新元素数量
- 对于大切片,扩容因子约为 1.25
3. 切片的共享问题:
package main
import "fmt"
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[1:3] // [2, 3]
// 修改 slice2 会影响 slice1
slice2[0] = 99
fmt.Println("slice1:", slice1) // [1, 99, 3, 4, 5]
fmt.Println("slice2:", slice2) // [99, 3]
}
切片最佳实践
1. 预分配容量:
// 不推荐:频繁扩容
s := []int{}
for i := 0; i < 1000; i++ {
s = append(s, i) // 多次扩容,性能差
}
// 推荐:预分配容量
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i) // 无需扩容,性能好
}
2. 避免大内存泄漏:
func processLargeData() {
// 假设 data 是一个很大的切片
data := make([]byte, 1000000)
// 只需要前 100 个字节
header := data[:100]
// 问题:header 仍然引用整个 data,导致 data 无法被 GC
// 解决方案:复制需要的数据
header = make([]byte, 100)
copy(header, data[:100])
}
3. 使用 copy 而不是 append:
// 不推荐
result := []int{}
for _, v := range source {
result = append(result, v) // 可能多次扩容
}
// 推荐
result := make([]int, 0, len(source))
for _, v := range source {
result = append(result, v)
}
// 或者直接使用 copy
result := make([]int, len(source))
copy(result, source)
4. 切片作为函数参数:
// 切片是引用传递,但 len 和 cap 是值传递
func modifySlice(s []int) {
// 修改元素会影响原切片
s[0] = 999
// append 不会影响原切片(除非发生扩容)
s = append(s, 100)
}
// 如果需要修改原切片,返回新的切片
func appendSlice(s []int, values ...int) []int {
return append(s, values...)
}
5. 检查切片是否为空:
// 不推荐
if s != nil { // nil 切片和空切片不同
// 处理
}
// 推荐
if len(s) > 0 { // 检查长度
// 处理
}
6. 使用 range 遍历切片:
// range 返回索引和值的副本
for i, v := range slice {
// v 是副本,修改 v 不会影响原切片
}
// 如果需要修改元素,使用索引
for i := range slice {
slice[i] = newValue
}
7. 切片截取的内存共享陷阱:
func getFirstN(data []byte, n int) []byte {
// 不推荐:返回的切片仍然引用整个 data
return data[:n]
// 推荐:复制数据,避免内存泄漏
result := make([]byte, n)
copy(result, data[:n])
return result
}
映射(Map)
package main
import "fmt"
func main() {
// 创建 map
m1 := make(map[string]int)
m1["apple"] = 5
m1["banana"] = 3
// map 字面量
m2 := map[string]int{
"red": 255,
"green": 128,
"blue": 64,
}
// 读取值
value, exists := m1["apple"]
fmt.Println("apple 的值:", value, "存在:", exists)
// 删除键
delete(m1, "banana")
// 遍历 map
for key, val := range m2 {
fmt.Println(key, ":", val)
}
fmt.Println("map 长度:", len(m2))
}
Map 的内部结构
Map 在 Go 内部使用哈希表实现,主要包含以下结构:
type hmap struct {
count int // map 中元素的数量
flags uint8 // 状态标志
B uint8 // 桶数组大小的对数(bucket 数组大小 = 2^B)
noverflow uint16 // 溢出的桶数量
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // 桶数组的指针
oldbuckets unsafe.Pointer // 扩容时的旧桶数组
nevacuate uintptr // 扩容搬迁进度
extra *mapextra // 额外信息(溢出桶等)
}
Map 的查找、插入和删除操作的平均时间复杂度为 O(1)。当 map 中的元素数量超过负载因子时,会自动扩容。
Map 的实现原理
1. 哈希计算:
Go 使用哈希函数计算 key 的哈希值,然后根据哈希值确定元素在桶数组中的位置。
// 哈希计算示例
// 1. 计算 key 的哈希值
hash := hashFunction(key, hmap.hash0)
// 2. 取哈希值的低 B 位确定桶索引
// 例如:B=3 时,bucket 数组大小为 8
bucketIndex := hash & (uintptr(1)<1)
// 3. 取哈希值的高 8 位作为 tophash
top := uint8(hash >> (unsafe.Sizeof(hash)*8 - 8))
不同类型的 key 使用不同的哈希算法:
- 整数:直接使用整数值作为哈希
- 字符串:使用 FNV-1a 算法
- 指针:使用指针地址
- 结构体:递归计算各字段的哈希
2. 桶(Bucket)结构详解:
每个桶可以存储最多 8 个键值对(bucketSize = 8)。桶的内存布局经过优化:
type bmap struct {
// tophash 数组:存储每个键的哈希值高 8 位
// 用于快速判断键是否可能存在于桶中
tophash [bucketSize]uint8
// keys 和 values 数组交错存储,减少填充
// 实际内存布局:[tophash0][tophash1]...[key0][key1]...[val0][val1]...
keys [bucketSize]keyType
values [bucketSize]valueType
// overflow 指向溢出桶链表
// 当桶满时,新元素存储在溢出桶中
overflow *bmap
}
3. 查找过程详解:
package main
import "fmt"
func main() {
m := make(map[string]int)
m["apple"] = 5
m["banana"] = 3
m["cherry"] = 7
// 查找 "banana" 的详细过程:
// 步骤 1:计算 "banana" 的哈希值
hash := hashFunction("banana", hash0)
fmt.Printf("哈希值: %d
", hash)
// 步骤 2:计算桶索引
bucketIndex := hash & (bucketMask)
fmt.Printf("桶索引: %d
", bucketIndex)
// 步骤 3:在桶中查找
bucket := buckets[bucketIndex]
for i := 0; i < bucketSize; i++ {
// 先检查 tophash 快速过滤
if bucket.tophash[i] != top {
continue
}
// 再比较完整的 key
if bucket.keys[i] == "banana" {
// 找到匹配,返回值
return bucket.values[i]
}
}
// 步骤 4:检查溢出桶链表
for bucket = bucket.overflow; bucket != nil; bucket = bucket.overflow {
// 在溢出桶中重复步骤 3
// ...
}
// 步骤 5:未找到,返回零值
return zeroValue
}
4. 插入过程详解:
package main
import "fmt"
func main() {
m := make(map[string]int)
// 插入 "orange" 的过程:
// 步骤 1:计算哈希和桶索引
hash := hashFunction("orange", hash0)
bucketIndex := hash & bucketMask
top := uint8(hash >> (unsafe.Sizeof(hash)*8 - 8))
// 步骤 2:在桶中查找空位
bucket := buckets[bucketIndex]
var insertPos int = -1
for i := 0; i < bucketSize; i++ {
if bucket.tophash[i] == empty {
// 找到空位
insertPos = i
break
}
}
if insertPos != -1 {
// 桶中有空位,直接插入
bucket.tophash[insertPos] = top
bucket.keys[insertPos] = "orange"
bucket.values[insertPos] = 10
} else {
// 桶已满,创建溢出桶
if bucket.overflow == nil {
bucket.overflow = newbmap()
}
// 在溢出桶中插入
insertIntoOverflow(bucket.overflow, "orange", 10)
}
// 步骤 3:检查是否需要扩容
if hmap.noverflow >= float64(hmap.count)/loadFactor {
grow(hmap)
}
}
5. 删除过程详解:
package main
import "fmt"
func main() {
m := make(map[string]int)
m["apple"] = 5
// 删除 "apple" 的过程:
// 步骤 1:定位 key 的位置
hash := hashFunction("apple", hash0)
bucketIndex := hash & bucketMask
bucket := buckets[bucketIndex]
for i := 0; i < bucketSize; i++ {
if bucket.tophash[i] != top {
continue
}
if bucket.keys[i] == "apple" {
// 找到 key,标记为已删除
bucket.tophash[i] = emptyOne // 特殊标记:已删除
bucket.keys[i] = zeroKey
bucket.values[i] = zeroValue
hmap.count--
break
}
}
// 注意:删除操作不会立即回收空间
// 已删除的位置会被 emptyOne 标记
// 后续插入时可以复用这些位置
}
6. 扩容机制详解:
Go 的 map 有两种扩容方式:
等量扩容(rehash):
- 触发条件:溢出桶过多(noverflow >= 1 << (B & 15))
- 目的:减少溢出桶,提高查找效率
- 过程:重新分配相同大小的桶数组,重新哈希所有元素
增量扩容(grow):
- 触发条件:元素数量超过负载因子(count / bucketSize > loadFactor)
- 负载因子通常为 6.5
- 目的:增加桶数量,减少哈希冲突
- 过程:桶数组大小翻倍(B++),渐进式搬迁元素
package main
import "fmt"
func main() {
m := make(map[int]int)
// 观察扩容过程
for i := 0; i < 100; i++ {
beforeB := m.B
m[i] = i * i
afterB := m.B
if beforeB != afterB {
fmt.Printf("扩容: B 从 %d 变为 %d
", beforeB, afterB)
}
}
}
7. 渐进式扩容详解:
增量扩容时,Go 采用渐进式搬迁策略:
type hmap struct {
// ... 其他字段
oldbuckets unsafe.Pointer // 旧桶数组
nevacuate uintptr // 搬迁进度:已搬迁的桶数量
noldbuckets uintptr // 旧桶数组大小
}
// 搬迁过程
func evacuate(hmap *hmap) {
// 每次操作最多搬迁 2 个桶
for i := 0; i < 2; i++ {
// 搬迁旧桶数组的第 nevacuate 个桶
oldbucket := (*bmap)(add(hmap.oldbuckets,
uintptr(hmap.nevacuate)*bucketSize))
// 将桶中的所有元素重新哈希到新桶数组
evacuateOne(oldbucket, hmap)
hmap.nevacuate++
// 搬迁完成,释放旧桶数组
if hmap.nevacuate == hmap.noldbuckets {
hmap.oldbuckets = nil
}
}
}
渐进式扩容的优势:
- 避免单次操作的性能抖动
- 分散扩容开销到多次操作
- 提高系统的响应性
8. 溢出桶链表:
当桶满时,新元素存储在溢出桶中,形成链表:
// 桶结构
Bucket {
tophash: [uint8] // [top1, top2, top3, top4, top5, top6, top7, top8]
keys: [KeyType] // [key1, key2, key3, key4, key5, key6, key7, key8]
values: [ValueType] // [val1, val2, val3, val4, val5, val6, val7, val8]
overflow: *Bucket // -> 溢出桶1 -> 溢出桶2 -> ...
}
// 查找时的遍历顺序:
// 1. 主桶
// 2. 溢出桶1
// 3. 溢出桶2
// ...
9. 空位标记:
Go 使用特殊的 tophash 值标记空位:
const (
emptyRest = 0 // 空位,后面没有元素
emptyOne = 1 // 已删除的位置
minTopHash = 4 // 最小有效的 tophash
)
// 查找时:
for i := 0; i < bucketSize; i++ {
if tophash[i] == emptyRest {
break // 后面没有元素了
}
if tophash[i] == emptyOne {
continue // 跳过已删除的位置
}
// 检查实际的键
}
10. 内存布局优化:
Go 对 map 的内存布局进行了优化:
- 减少填充:keys 和 values 数组交错存储
- 缓存友好:tophash 数组在前面,便于快速过滤
- 内存对齐:根据类型大小自动对齐
// 实际内存布局(64位系统)
// [8]byte tophash
// [8]KeyType keys
// [8]ValueType values
// 指针 overflow
// 假设 KeyType=int, ValueType=int
// tophash: 8 bytes
// keys: 8 * 8 = 64 bytes
// values: 8 * 8 = 64 bytes
// 总计: 8 + 64 + 64 = 136 bytes
Map 最佳实践
1. 预分配 map 容量:
// 不推荐:频繁扩容
m := make(map[string]int)
for i := 0; i < 1000; i++ {
m[fmt.Sprintf("key%d", i)] = i // 多次扩容
}
// 推荐:预分配容量
m := make(map[string]int, 1000)
for i := 0; i < 1000; i++ {
m[fmt.Sprintf("key%d", i)] = i // 无需扩容
}
2. 检查键是否存在:
// 推荐:使用两个返回值
if value, ok := m["key"]; ok {
fmt.Println("键存在,值为:", value)
} else {
fmt.Println("键不存在")
}
3. 遍历 map 的顺序:
// 注意:map 的遍历顺序是随机的
for key, value := range m {
fmt.Println(key, value)
}
// 如果需要有序遍历,先收集键并排序
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
// 排序键
for _, key := range keys {
fmt.Println(key, m[key])
}
4. 并发安全:
// 不推荐:map 不是并发安全的
var m = make(map[int]int)
go func() {
m[1] = 1 // 可能导致 panic
}()
// 推荐:使用 sync.Map 或加锁
import "sync"
// 方式1:使用 sync.Map(适合读多写少)
var m sync.Map
m.Store(1, 1)
if value, ok := m.Load(1); ok {
fmt.Println(value)
}
// 方式2:使用互斥锁(适合读写均衡)
type SafeMap struct {
mu sync.RWMutex
m map[int]int
}
func (sm *SafeMap) Get(key int) (int, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
v, ok := sm.m[key]
return v, ok
}
func (sm *SafeMap) Set(key, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
5. 删除键:
// 删除键
delete(m, "key")
// 删除不存在的键是安全的,不会 panic
delete(m, "nonexistent")
6. map 作为函数参数:
// map 是引用类型,传递的是指针
func modifyMap(m map[string]int) {
m["new"] = 100 // 会影响原 map
delete(m, "old") // 会影响原 map
}
// 如果 map 可能为 nil,需要检查
func safeSet(m map[string]int, key string, value int) {
if m == nil {
m = make(map[string]int)
}
m[key] = value
}
7. 选择合适的 key 类型:
// 推荐:可比较的类型作为 key
m1 := make(map[string]int) // 字符串
m2 := make(map[int]int) // 整数
m3 := make(map[bool]int) // 布尔值
m4 := make(map[float64]int) // 浮点数(不推荐,精度问题)
// 不推荐:不可比较的类型(slice、map、func)
// m := make(map[[]int]int) // 编译错误
// 如果需要使用复合类型作为 key,确保它是可比较的
type Point struct {
X, Y int
}
m5 := make(map[Point]string)
8. 避免 map 的内存泄漏:
// 清空 map
for key := range m {
delete(m, key)
}
// 或者直接创建新的 map
m = make(map[string]int)
// 注意:如果 map 中存储了大量数据,即使清空后,
// 底层数组可能不会被立即释放
9. 使用 value, ok 模式避免零值混淆:
// 问题:无法区分零值和键不存在
value := m["key"] // 如果 key 不存在,返回零值
// 解决:使用 value, ok 模式
if value, ok := m["key"]; ok {
fmt.Println("键存在,值为:", value)
} else {
fmt.Println("键不存在")
}
10. map 的零值:
// map 的零值是 nil
var m map[string]int
fmt.Println(m == nil) // true
fmt.Println(len(m)) // 0
// 可以向 nil map 读取和写入
value := m["key"] // 返回零值,不 panic
m["new"] = 1 // 自动初始化 map
Go 1.25 新特性
range over integers
Go 1.25 引入了整数范围的迭代,简化了循环写法。
package main
import "fmt"
func main() {
// 传统方式
for i := 0; i < 5; i++ {
fmt.Println("传统:", i)
}
// Go 1.25 新方式:range over integers
for i := range 5 {
fmt.Println("新方式:", i)
}
// 带起始值
for i := range 3 : 7 {
fmt.Println("范围 3-6:", i)
}
}
改进的 math/bits 包
package main
import (
"fmt"
"math/bits"
)
func main() {
// 计算前导零
x := uint(16)
fmt.Println("前导零:", bits.LeadingZeros(x))
// 计算尾随零
fmt.Println("尾随零:", bits.TrailingZeros(x))
// 计算设置位数
fmt.Println("设置位数:", bits.OnesCount(x))
}
新的 slices 包
package main
import (
"cmp"
"fmt"
"slices"
)
func main() {
numbers := []int{5, 2, 8, 1, 9}
// 排序
slices.Sort(numbers)
fmt.Println("排序后:", numbers)
// 二分查找
index, found := slices.BinarySearch(numbers, 8)
fmt.Printf("查找 8: 索引=%d, 找到=%v\n", index, found)
// 检查元素是否存在
fmt.Println("包含 5:", slices.Contains(numbers, 5))
// 比较切片
numbers2 := []int{1, 2, 5, 8, 9}
fmt.Println("切片相等:", slices.Equal(numbers, numbers2))
// 查找最大最小值
fmt.Println("最小值:", slices.Min(numbers))
fmt.Println("最大值:", slices.Max(numbers))
// 反转切片
slices.Reverse(numbers)
fmt.Println("反转后:", numbers)
// 使用自定义比较函数排序
names := []string{"Alice", "Bob", "Charlie"}
slices.SortFunc(names, cmp.Compare)
fmt.Println("排序后的名字:", names)
}
新的 maps 包
package main
import (
"fmt"
"maps"
)
func main() {
m1 := map[string]int{
"apple": 5,
"banana": 3,
"orange": 7,
}
m2 := map[string]int{
"apple": 5,
"banana": 3,
"orange": 7,
}
// 比较 map 是否相等
fmt.Println("map 相等:", maps.Equal(m1, m2))
// 复制 map
m3 := maps.Clone(m1)
m3["grape"] = 10
fmt.Println("原 map:", m1)
fmt.Println("复制的 map:", m3)
// 获取所有键
keys := maps.Keys(m1)
fmt.Println("所有键:", keys)
// 获取所有值
values := maps.Values(m1)
fmt.Println("所有值:", values)
}