Skip to content

数据类型 - Golang基础面试题

本章包含Golang数据类型系统的核心面试题和知识点,涵盖基本类型、复合类型和类型转换。

📋 重点面试题

1:Go的基本数据类型有哪些?

Go语言的基本数据类型可以分为以下几类:

1. 布尔类型

go
var isActive bool = true
var isCompleted bool = false
var isDefault bool  // 零值为false

fmt.Printf("isActive: %t\n", isActive)     // true
fmt.Printf("isDefault: %t\n", isDefault)   // false

2. 数值类型

整数类型:

go
// 有符号整数
var i8 int8 = 127        // -128 到 127
var i16 int16 = 32767    // -32768 到 32767  
var i32 int32 = 2147483647
var i64 int64 = 9223372036854775807

// 无符号整数
var ui8 uint8 = 255      // 0 到 255
var ui16 uint16 = 65535  // 0 到 65535
var ui32 uint32 = 4294967295
var ui64 uint64 = 18446744073709551615

// 特殊整数类型
var i int = 42           // 平台相关,32位或64位
var ui uint = 42         // 平台相关,32位或64位
var uiptr uintptr = 0    // 存储指针的无符号整数

fmt.Printf("int size: %d bytes\n", unsafe.Sizeof(i))     // 8 bytes (64位系统)
fmt.Printf("uintptr size: %d bytes\n", unsafe.Sizeof(uiptr)) // 8 bytes (64位系统)

浮点数类型:

go
var f32 float32 = 3.14159
var f64 float64 = 3.141592653589793

// 复数类型
var c64 complex64 = 1 + 2i
var c128 complex128 = 1 + 2i

fmt.Printf("float32 precision: %f\n", f32)  // 精度有限
fmt.Printf("float64 precision: %f\n", f64)  // 双精度
fmt.Printf("complex128: %v\n", c128)         // (1+2i)

3. 字符串和字符类型

字符串实际上是一个 byte 切片

go
// 字符串类型
var s string = "Hello, 世界"
var empty string         // 零值为""

// 字符类型(rune是int32的别名)
var r rune = ''         // Unicode码点
var b byte = 'A'         // byte是uint8的别名

fmt.Printf("string: %s\n", s)
fmt.Printf("rune: %c (%d)\n", r, r)    // 中 (20013)
fmt.Printf("byte: %c (%d)\n", b, b)    // A (65)

类型大小对比

go
func printTypeSizes() {
    fmt.Println("=== 类型大小对比 ===")
    fmt.Printf("bool:       %d bytes\n", unsafe.Sizeof(bool(true)))
    fmt.Printf("int8:       %d bytes\n", unsafe.Sizeof(int8(0)))
    fmt.Printf("int16:      %d bytes\n", unsafe.Sizeof(int16(0)))
    fmt.Printf("int32:      %d bytes\n", unsafe.Sizeof(int32(0)))
    fmt.Printf("int64:      %d bytes\n", unsafe.Sizeof(int64(0)))
    fmt.Printf("float32:    %d bytes\n", unsafe.Sizeof(float32(0)))
    fmt.Printf("float64:    %d bytes\n", unsafe.Sizeof(float64(0)))
    fmt.Printf("complex64:  %d bytes\n", unsafe.Sizeof(complex64(0)))
    fmt.Printf("complex128: %d bytes\n", unsafe.Sizeof(complex128(0)))
    fmt.Printf("string:     %d bytes\n", unsafe.Sizeof(string("")))
    fmt.Printf("rune:       %d bytes\n", unsafe.Sizeof(rune(0)))
    fmt.Printf("byte:       %d bytes\n", unsafe.Sizeof(byte(0)))
}

2:rune和byte的区别是什么?

1. 基本定义

go
// byte是uint8的别名,用于表示ASCII字符或字节
// 把汉字赋值给byte类型的数据会直接报错。
type byte = uint8

// rune是int32的别名,用于表示Unicode码点
// Go语言中采用的是统一的UTF-8编码,
// 英文字母在底层占1个字节,特殊字符和中文汉字则占用1~4个字节,刚好和rune的大小匹配
type rune = int32

2. 实际使用对比

go
package main

import (
    "fmt"
    "unicode/utf8"
)

func demonstrateRuneVsByte() {
    text := "Hello世界"
    
    fmt.Printf("字符串: %s\n", text) 
    //字符串: Hello世界
    fmt.Printf("字符串长度(字节): %d\n", len(text))                    
    // 字符串长度(字节): 11  
    fmt.Printf("字符串长度(rune): %d\n", utf8.RuneCountInString(text)) 
    // 字符串长度(rune): 7
    
    fmt.Println("\n=== 使用byte遍历 ===")
    for i := 0; i < len(text); i++ {
        fmt.Printf("索引 %d: %c (%d)\n", i, text[i], text[i])
    }
    // === 使用byte遍历 ===
    // 索引 0: H (72)
    // 索引 1: e (101)  
    // 索引 2: l (108)
    // 索引 3: l (108)
    // 索引 4: o (111)
    // 索引 5: ä (228)  // 中文字符的第一个字节
    // 索引 6: ¸ (184)  // 中文字符的第二个字节
    // 索引 7:  (150)   // 中文字符的第三个字节
    // 索引 8: ç (231)  // "界"字符的第一个字节
    // 索引 9:  (149)   // "界"字符的第二个字节  
    // 索引 10:  (140)  // "界"字符的第三个字节
    
    fmt.Println("\n=== 使用rune遍历 ===")  
    for i, r := range text {
        fmt.Printf("索引 %d: %c (%d)\n", i, r, r)
    }
    // === 使用rune遍历 ===
    // 索引 0: H (72)
    // 索引 1: e (101)
    // 索引 2: l (108)
    // 索引 3: l (108)
    // 索引 4: o (111)
    // 索引 5: 世 (19990)
    // 索引 8: 界 (30028)
}

3. 字符串操作最佳实践

go
func stringOperationBestPractices() {
    text := "Go语言编程"
    
    // 错误方式:直接索引可能破坏UTF-8编码
    fmt.Printf("第二个字符: %c\n", text[2])  // 输出乱码è
    
    // 正确方式1:转换为rune slice
    runes := []rune(text)
    fmt.Printf("第二个字符: %c\n", runes[2])  // 语
    
    // 正确方式2:使用range遍历
    count := 0
    for _, r := range text {
        count++
        if count == 2 {
            fmt.Printf("第二个字符: %c\n", r)  // 语
            break
        }
    }
    
    // 字符串截取
    fmt.Printf("前3个字符: %s\n", string(runes[:3]))  // Go语
    
    // 统计字符数量
    fmt.Printf("字符数量: %d\n", len(runes))  // 5
    fmt.Printf("字节数量: %d\n", len(text))   // 13 (英文1字节,中文3字节)
}

3:Go的复合数据类型有哪些?

1. 数组(Array)

固定大小,不能使用append,但能用arr1[start:end]得到切片,之后可以进行append 无法在运行时改变 数组作为参数传递进行值传递,复制整个数组的内容,副本内的数值改变不会影响原数据 不能将一个slice类型和array类型相互交换 切片的Data字段就是对底层数组的引用(两者是上下级关系) 数组的类型是[2]int,[3]int这种,根据长度不同有不同的类型,切片只有不定常[]int类型

go
// 固定长度的数组
var arr1 [5]int                    // [0 0 0 0 0],通过var不能指定值,只有默认值
arr2 := [3]string{"a", "b", "c"}   // [a b c]
arr3 := [...]int{1, 2, 3, 4}       // 长度自动推断为4

// 数组是值类型
func arrayValueType() {
    original := [3]int{1, 2, 3}
    copy := original  // 复制整个数组
    copy[0] = 100
    
    fmt.Printf("原数组: %v\n", original)  // [1 2 3]
    fmt.Printf("复制数组: %v\n", copy)    // [100 2 3]
}

2. 切片(Slice)

go
// 动态数组
var slice1 []int                    // nil slice
slice2 := []string{"a", "b", "c"}   // 字面量创建
slice3 := make([]int, 5, 10)        // 长度5,容量10

func sliceOperations() {
    numbers := []int{1, 2, 3}
    
    // 追加元素
    numbers = append(numbers, 4, 5)
    fmt.Printf("追加后: %v\n", numbers)  // [1 2 3 4 5]
    
    // 切片操作
    sub := numbers[1:4]  // [2 3 4]
    fmt.Printf("子切片: %v\n", sub)
    
    // 容量和长度
    fmt.Printf("长度: %d, 容量: %d\n", len(numbers), cap(numbers))
    
    // 切片是引用类型
    sub[0] = 100
    fmt.Printf("修改子切片后原切片: %v\n", numbers)  // [1 100 3 4 5]
}

3. 映射(Map)

go
// 键值对集合
var map1 map[string]int              // nil map
map2 := make(map[string]int)         // 空map
map3 := map[string]int{              // 字面量创建
    "apple":  5,
    "banana": 3,
}

func mapOperations() {
    fruits := make(map[string]int)
    
    // 添加元素
    fruits["apple"] = 5
    fruits["banana"] = 3
    
    // 读取元素
    count, exists := fruits["apple"]
    if exists {
        fmt.Printf("苹果数量: %d\n", count)
    }
    
    // 删除元素
    delete(fruits, "banana")
    
    // 遍历map
    for fruit, count := range fruits {
        fmt.Printf("%s: %d\n", fruit, count)
    }
}

4. 结构体(Struct)

go
// 自定义数据类型
type Person struct {
    Name    string
    Age     int
    Email   string
    private int  // 私有字段
}

func structOperations() {
    // 创建结构体
    p1 := Person{
        Name:  "Alice",
        Age:   30,
        Email: "alice@example.com",
    }
    
    // 匿名结构体
    point := struct {
        X, Y int
    }{10, 20}
    
    // 结构体指针
    p2 := &Person{Name: "Bob", Age: 25}
    
    fmt.Printf("p1: %+v\n", p1)
    fmt.Printf("point: %+v\n", point)
    fmt.Printf("p2: %+v\n", *p2)
}

5. 指针(Pointer)

go
func pointerOperations() {
    x := 42
    p := &x  // 获取x的地址
    
    fmt.Printf("x的值: %d\n", x)        // 42
    fmt.Printf("p的值: %p\n", p)        // 地址
    fmt.Printf("*p的值: %d\n", *p)      // 42 (解引用)
    
    *p = 100  // 通过指针修改x的值
    fmt.Printf("修改后x的值: %d\n", x)   // 100
    
    // 指针的零值是nil
    var nilPtr *int
    if nilPtr == nil {
        fmt.Println("指针为nil")
    }
}

6. 接口(Interface)

go
// 定义接口
type Writer interface {
    Write([]byte) (int, error)
}

type FileWriter struct {
    filename string
}

func (fw FileWriter) Write(data []byte) (int, error) {
    fmt.Printf("写入文件 %s: %s\n", fw.filename, string(data))
    return len(data), nil
}

func interfaceOperations() {
    var w Writer = FileWriter{"test.txt"}
    w.Write([]byte("Hello, World!"))
    
    // 空接口可以存储任何类型
    var empty interface{}
    empty = 42
    empty = "hello"
    empty = []int{1, 2, 3}
}

7. 通道(Channel)

对于一个nil通道的读写会直接阻塞线程,出现问题

go
func channelOperations() {
    // 创建通道
    ch := make(chan int, 2)  // 缓冲通道,容量2
    
    // 发送数据
    ch <- 1
    ch <- 2
    
    // 接收数据
    val1 := <-ch
    val2 := <-ch
    
    fmt.Printf("接收到: %d, %d\n", val1, val2)
    
    // 关闭通道
    close(ch)
    
    // 检查通道是否关闭
    if val, ok := <-ch; !ok {
        fmt.Println("通道已关闭")
    }
}
操作nil slicenil mapnil chan
len()返回 0返回 0不可用(编译错误)
range正常(0 次迭代)正常(0 次迭代)永久阻塞(无元素可取)
读取元素panic(索引越界)返回零值,false永久阻塞
写入元素panic(索引越界)panic(写入 nil map)永久阻塞
append正常工作不适用不适用
close不适用不适用panic(关闭 nil chan)

5:深入理解Slice、Map、Channel、Struct的底层结构

1. 🔍 Slice 底层结构

Slice 的内部表示

一个 slice 变量(例如 var s []int)在内存中是一个 sliceHeader 结构体。这个结构体包含三个字段:

代码实现
go
// Go 运行时中 slice 的底层结构定义
// 位于 runtime/slice.go
type sliceHeader struct {
    array unsafe.Pointer  // 指向底层数组的指针,8 字节的指针
    len   int             // 当前长度,8 字节的整数
    cap   int             // 容量,8个字节的整数
}// (在 64 位系统上,总共 24 字节)

// 示例:理解 slice 的内存布局
func demonstrateSliceStructure() {
    s := make([]int, 3, 5)
    
    // slice 在内存中的表示:
    // +--------+-----+-----+
    // | array  | len | cap |
    // +--------+-----+-----+
    // | 0x...  |  3  |  5  |
    // +--------+-----+-----+
    //    |
    //    v
    // 底层数组: [0, 0, 0, _, _]
    //           ^--------^  ^--^
    //           len=3       预留空间
    
    fmt.Printf("Slice大小: %d bytes\n", unsafe.Sizeof(s))  // 24 bytes (64位系统)
    // 结构:8 bytes (指针) + 8 bytes (int) + 8 bytes (int) = 24 bytes
}
Slice 的扩容机制

当len大于cap时,会进行扩容

  1. 如果新容量 > 2倍旧容量,直接使用新容量(append一个很大的值)
  2. 如果旧容量 < 256,新容量 = 2倍旧容量
  3. 如果旧容量 >= 256,新容量 = 旧容量 + (旧容量 + 3*256) / 4 目的:逐渐降低增长率,避免大slice浪费太多内存 容量会被调整到与元素大小和系统内存分配粒度(通常为 8 字节或 16 字节)对齐的边界,保证可用
扩容机制详解
go
func main() {
	one := []int{1}
	two := []int{2, 3}
	slice := make([]int, 0)
	fmt.Println(cap(slice))
	slice = append(slice, one...) 
	fmt.Println(cap(slice)) // 打印为1,新容量为1
	slice = append(slice, two...)
	fmt.Println(cap(slice)) // 打印为3,新长度为2+1,大于两倍旧容量,使用新长度作为容量
	slice = append(slice, two...)
	fmt.Println(cap(slice)) // 打印为6,新容量为小于两倍旧容量,使用两倍旧容量
}

// 扩容规则(Go 1.18+):
// 1. 如果新容量 > 2倍旧容量,直接使用新容量
// 2. 如果旧容量 < 256,新容量 = 2倍旧容量
// 3. 如果旧容量 >= 256,新容量 = 旧容量 + (旧容量 + 3*256) / 4
//    目的:逐渐降低增长率,避免大slice浪费太多内存

func sliceGrowthRule(old int, cap int) int {
    if cap > old*2 {
        return cap
    }
    
    if old < 256 {
        return old * 2
    }
    
    newcap := old
    for {
        newcap += (newcap + 3*256) / 4
        if newcap >= cap {
            return newcap
        }
    }
}

// 完整拷贝避免共享
func demonstrateSliceCopy() {
    original := []int{1, 2, 3, 4, 5}
    
    // 方法1:使用 copy
    copied1 := make([]int, len(original))
    copy(copied1, original)
    
    // 方法2:使用 append
    copied2 := append([]int{}, original...)
    
    // 修改copied不会影响original
    copied1[0] = 100
    copied2[1] = 200
    
    fmt.Printf("original: %v\n", original)  // [1, 2, 3, 4, 5]
    fmt.Printf("copied1: %v\n", copied1)    // [100, 2, 3, 4, 5]
    fmt.Printf("copied2: %v\n", copied2)    // [1, 200, 3, 4, 5]
}
🎯 Slice 性能优化技巧
go
// ❌ 低效:频繁扩容
func badSliceUsage() []int {
    var s []int
    for i := 0; i < 10000; i++ {
        s = append(s, i)  // 可能触发多次扩容和内存拷贝
    }
    return s
}

// ✅ 高效:预分配容量
func goodSliceUsage() []int {
    s := make([]int, 0, 10000)  // 预分配容量,避免扩容
    for i := 0; i < 10000; i++ {
        s = append(s, i)
    }
    return s
}

// ⚠️ 注意:Slice 截取导致的内存泄漏
func sliceMemoryLeak() {
    // 问题:bigSlice 的底层数组不会被释放
    bigSlice := make([]byte, 1024*1024*100)  // 100MB
    smallSlice := bigSlice[:10]               // 只用10个字节
    
    // ✅ 解决方案:完整拷贝
    smallSliceCopy := make([]byte, 10)
    copy(smallSliceCopy, bigSlice[:10])
    // 现在 bigSlice 可以被 GC 回收了
}
陷阱

数组是值传递

  • Data部分因为是指针,所以在函数中更改,在外层会看见
  • len和cap部分因为是值,所以在函数中更改,在外层不会看见
  • 在函数中操作切片
    • 如果不超过容量cap值,函数内部的修改外部是能看见的
    • 如果超过cap,会重新获取Data指针,内外的切片不是同一个地址,内部的修改外部看不见
  • 但是函数内部的扩容后的len会变大,这个因为len是值,里面的修改外面看不到,所以外面想要获得同样的len只能通过切片方式获取,否则会panic
go
func doSlice(slice[]int) {
	slice = append(slice, 1)
	fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8 1]
	fmt.Println(len(slice)) // 打印为10
	fmt.Println(cap(slice)) // 打印为10
	fmt.Println(&slice[0]) // 打印为0x1000000000
	slice = append(slice, 1)
	fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8 1]
	fmt.Println(len(slice)) // 打印为11
	fmt.Println(cap(slice)) // 打印为20,扩容后返回了新的容量
	fmt.Println(&slice[0]) // 打印为0x1000000020,扩容后返回了新的Data指针
	slice[9] = 10
	fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8 10]
}

func main() {
	slice := make([]int, 9,10)
	for i := 0; i < 9; i++ {
		slice[i] = i
	}
	doSlice(slice)
	fmt.Println(slice) // 打印为[0 1 2 3 4 5 6 7 8]
	fmt.Println(len(slice), cap(slice)) // 打印为9 10
	fmt.Println(&slice[0]) // 打印为0x1000000000
	fmt.Println(slice[0:10]) // 打印为[0 1 2 3 4 5 6 7 8 1],看不到扩容后的修改
	fmt.Println(slice[10]) // panic,超出长度
}

2. 🗂️ Map 底层结构

Map 的内部表示
代码实现
go
// Go 运行时中 map 的底层结构定义
// 位于 runtime/map.go

// hmap 是 map 的头部结构
type hmap struct {
    count     int      // map 中的键值对数量
    flags     uint8    // 标志位(是否在写入等)
    B         uint8    // bucket 数量的对数:2^B 个 bucket
    noverflow uint16   // 溢出 bucket 的大概数量
    hash0     uint32   // hash 种子
    
    buckets    unsafe.Pointer  // 指向 2^B 个 bucket 的数组
    oldbuckets unsafe.Pointer  // 扩容时指向旧的 bucket 数组
    nevacuate  uintptr          // 扩容进度
    
    extra *mapextra  // 可选字段
}

// bmap 是 map 的 bucket 结构
type bmap struct {
    // tophash 数组存储每个 key 的 hash 值的高8位
    // 用于快速判断 key 是否在 bucket 中
    tophash [8]uint8
    
    // 实际上还有以下字段(编译器动态生成):
    // keys    [8]keytype    // 8个key
    // values  [8]valuetype  // 8个value
    // overflow *bmap        // 溢出bucket指针
}

// Map 的内存布局示意
func demonstrateMapStructure() {
    m := make(map[string]int)
    
    // hmap 大小:48 bytes (64位系统)
    // 包含:
    // - count: 8 bytes
    // - flags: 1 byte
    // - B: 1 byte
    // - noverflow: 2 bytes
    // - hash0: 4 bytes
    // - buckets: 8 bytes (指针)
    // - oldbuckets: 8 bytes (指针)
    // - nevacuate: 8 bytes
    // - extra: 8 bytes (指针)
    
    fmt.Printf("Map header大小: %d bytes\n", unsafe.Sizeof(m))  // 8 bytes (只是指针)
    
    // 每个 bucket 可以存储 8 个键值对
    // 如果超过8个,会创建溢出 bucket
}
Map 的扩容机制
点击查看扩容机制详解
go
package main

import "fmt"

func demonstrateMapGrowth() {
    // Map 的两种扩容情况:
    
    // 1. 负载因子过大(元素过多)
    //    触发条件:count / (2^B * 8) > 6.5
    //    扩容方式:翻倍扩容(2^B -> 2^(B+1))
    
    // 2. 溢出 bucket 过多(但元素不多)
    //    触发条件:noverflow >= 2^B
    //    扩容方式:等量扩容(重新排列,减少溢出bucket)
    
    m := make(map[int]int)
    fmt.Println("=== Map 扩容演示 ===")
    
    // 插入大量数据观察扩容
    for i := 0; i < 100; i++ {
        m[i] = i
        if i%10 == 0 {
            fmt.Printf("插入 %d 个元素\n", i+1)
        }
    }
}

// 渐进式扩容:不会一次性迁移所有数据
func demonstrateIncrementalRehash() {
    // Go 的 map 扩容是渐进式的:
    // 1. 分配新的 bucket 数组
    // 2. 每次操作时迁移1-2个旧bucket
    // 3. 查找时会同时查新旧bucket
    
    // 优点:
    // - 避免扩容时的长时间停顿
    // - 分摊扩容成本到每次操作
    
    // 缺点:
    // - 扩容期间内存占用翻倍
    // - 查找需要查两个bucket数组
}
🎯 Map 性能优化技巧
go
// ❌ 低效:未预分配容量
func badMapUsage() map[int]int {
    m := make(map[int]int)
    for i := 0; i < 10000; i++ {
        m[i] = i  // 可能触发多次扩容
    }
    return m
}

// ✅ 高效:预分配容量
func goodMapUsage() map[int]int {
    m := make(map[int]int, 10000)  // 预分配容量
    for i := 0; i < 10000; i++ {
        m[i] = i
    }
    return m
}

// ⚠️ 注意:Map 不是并发安全的
func unsafeConcurrentMap() {
    m := make(map[int]int)
    
    // ❌ 并发写入会 panic
    go func() {
        for i := 0; i < 1000; i++ {
            m[i] = i
        }
    }()
    
    go func() {
        for i := 0; i < 1000; i++ {
            m[i] = i
        }
    }()
    // panic: concurrent map writes
}

// ✅ 使用 sync.Map 或加锁
import "sync"

func safeConcurrentMap() {
    var m sync.Map
    
    var wg sync.WaitGroup
    wg.Add(2)
    
    go func() {
        defer wg.Done()
        for i := 0; i < 1000; i++ {
            m.Store(i, i)
        }
    }()
    
    go func() {
        defer wg.Done()
        for i := 0; i < 1000; i++ {
            m.Store(i, i)
        }
    }()
    
    wg.Wait()
}

3. 📡 Channel 底层结构

Channel 的内部表示
点击查看完整代码实现
go
// Go 运行时中 channel 的底层结构定义
// 位于 runtime/chan.go

type hchan struct {
    qcount   uint           // 队列中的元素个数
    dataqsiz uint           // 环形队列的大小
    buf      unsafe.Pointer // 指向环形队列的指针
    elemsize uint16         // 元素大小
    closed   uint32         // 是否关闭
    elemtype *_type         // 元素类型
    sendx    uint           // 发送索引
    recvx    uint           // 接收索引
    
    recvq    waitq          // 等待接收的 goroutine 队列
    sendq    waitq          // 等待发送的 goroutine 队列
    
    lock mutex              // 互斥锁
}

// waitq 是等待队列
type waitq struct {
    first *sudog
    last  *sudog
}

// Channel 的内存布局
func demonstrateChannelStructure() {
    ch := make(chan int, 3)
    
    // hchan 结构大小:96 bytes (64位系统)
    // 包含:
    // - 环形缓冲区(buf)
    // - 发送/接收索引(sendx/recvx)
    // - 等待队列(recvq/sendq)
    // - 互斥锁(lock)
    
    fmt.Printf("Channel大小: %d bytes\n", unsafe.Sizeof(ch))  // 8 bytes (只是指针)
}
Channel 的工作原理
点击查看工作原理详解
go
package main

import (
    "fmt"
    "time"
)

// 1. 无缓冲 Channel:发送和接收必须配对
func demonstrateUnbufferedChannel() {
    ch := make(chan int)
    
    // 发送操作会阻塞,直到有接收者
    go func() {
        fmt.Println("发送前")
        ch <- 42
        fmt.Println("发送后")
    }()
    
    time.Sleep(100 * time.Millisecond)
    
    fmt.Println("接收前")
    val := <-ch
    fmt.Println("接收后:", val)
}

// 2. 缓冲 Channel:有容量限制
func demonstrateBufferedChannel() {
    ch := make(chan int, 2)
    
    // 前2个发送不会阻塞
    ch <- 1
    ch <- 2
    
    fmt.Println("已发送2个元素")
    
    // 第3个发送会阻塞
    go func() {
        fmt.Println("发送第3个元素...")
        ch <- 3
        fmt.Println("第3个元素已发送")
    }()
    
    time.Sleep(100 * time.Millisecond)
    
    // 接收一个元素,释放空间
    fmt.Println("接收:", <-ch)
    time.Sleep(100 * time.Millisecond)
}

// 3. Channel 的发送和接收流程
func demonstrateChannelFlow() {
    /*
    发送流程:
    1. 加锁
    2. 检查是否有等待接收的 goroutine
       - 有:直接传递数据,唤醒接收者
       - 无:检查缓冲区
    3. 缓冲区未满:将数据放入缓冲区
    4. 缓冲区已满:将当前 goroutine 加入发送队列,阻塞
    5. 解锁
    
    接收流程:
    1. 加锁
    2. 检查是否有等待发送的 goroutine
       - 有:直接接收数据,唤醒发送者
       - 无:检查缓冲区
    3. 缓冲区有数据:从缓冲区取数据
    4. 缓冲区为空:将当前 goroutine 加入接收队列,阻塞
    5. 解锁
    */
}

// 4. Channel 的关闭
func demonstrateChannelClose() {
    ch := make(chan int, 3)
    
    // 发送数据
    ch <- 1
    ch <- 2
    ch <- 3
    
    // 关闭 channel
    close(ch)
    
    // 可以继续接收已缓冲的数据
    fmt.Println(<-ch)  // 1
    fmt.Println(<-ch)  // 2
    fmt.Println(<-ch)  // 3
    
    // 接收零值
    val, ok := <-ch
    fmt.Printf("val=%d, ok=%t\n", val, ok)  // val=0, ok=false
    
    // ❌ 不能向已关闭的 channel 发送数据
    // ch <- 4  // panic: send on closed channel
}
🎯 Channel 使用最佳实践
go
// 1. 使用 select 处理多个 channel
func demonstrateSelect() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    quit := make(chan bool)
    
    go func() {
        for {
            select {
            case val := <-ch1:
                fmt.Println("从 ch1 接收:", val)
            case val := <-ch2:
                fmt.Println("从 ch2 接收:", val)
            case <-quit:
                fmt.Println("退出")
                return
            }
        }
    }()
}

// 2. 使用 channel 实现超时控制
func demonstrateTimeout() {
    ch := make(chan int)
    
    select {
    case val := <-ch:
        fmt.Println("接收到:", val)
    case <-time.After(1 * time.Second):
        fmt.Println("超时")
    }
}

// 3. 使用 channel 实现信号量
func demonstrateSemaphore() {
    // 限制并发数为3
    sem := make(chan struct{}, 3)
    
    for i := 0; i < 10; i++ {
        sem <- struct{}{}  // 获取信号量
        
        go func(id int) {
            defer func() { <-sem }()  // 释放信号量
            
            fmt.Printf("Goroutine %d 开始工作\n", id)
            time.Sleep(1 * time.Second)
            fmt.Printf("Goroutine %d 完成工作\n", id)
        }(i)
    }
}

// ⚠️ 避免 goroutine 泄漏
func avoidGoroutineLeak() {
    // ❌ 错误:goroutine 会永久阻塞
    ch := make(chan int)
    go func() {
        val := <-ch  // 永远等待
        fmt.Println(val)
    }()
    // ch 没有发送者,goroutine 泄漏
    
    // ✅ 正确:使用 context 或 done channel
    done := make(chan struct{})
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-done:
            return
        }
    }()
    
    // 清理
    close(done)
}

4. 🏗️ Struct 底层结构

Struct 的内存对齐
点击查看完整代码实现
go
package main

import (
    "fmt"
    "unsafe"
)

// 1. 内存对齐规则
func demonstrateMemoryAlignment() {
    // 不同类型的对齐要求:
    // - bool, int8, uint8:  1 byte
    // - int16, uint16:      2 bytes
    // - int32, uint32:      4 bytes
    // - int64, uint64:      8 bytes
    // - float32:            4 bytes
    // - float64:            8 bytes
    // - pointer:            8 bytes (64位系统)
    
    type BadStruct struct {
        a bool    // 1 byte + 7 bytes padding
        b int64   // 8 bytes
        c bool    // 1 byte + 7 bytes padding
        d int64   // 8 bytes
    }
    
    type GoodStruct struct {
        b int64   // 8 bytes
        d int64   // 8 bytes
        a bool    // 1 byte
        c bool    // 1 byte + 6 bytes padding
    }
    
    fmt.Printf("BadStruct 大小: %d bytes\n", unsafe.Sizeof(BadStruct{}))   // 32 bytes
    fmt.Printf("GoodStruct 大小: %d bytes\n", unsafe.Sizeof(GoodStruct{})) // 24 bytes
    
    // 优化建议:按照大小降序排列字段
}

// 2. 零值结构体
func demonstrateZeroSizeStruct() {
    type Empty struct{}
    
    e := Empty{}
    fmt.Printf("Empty 大小: %d bytes\n", unsafe.Sizeof(e))  // 0 bytes
    
    // 空结构体的应用场景:
    // 1. 作为 map 的 value,实现 set
    set := make(map[string]struct{})
    set["key1"] = struct{}{}
    set["key2"] = struct{}{}
    
    // 2. 作为 channel 的信号
    done := make(chan struct{})
    go func() {
        // do something
        done <- struct{}{}
    }()
}

// 3. 结构体嵌入(Embedding)
func demonstrateStructEmbedding() {
    type Person struct {
        Name string
        Age  int
    }
    
    type Employee struct {
        Person        // 嵌入 Person
        Company string
    }
    
    emp := Employee{
        Person:  Person{Name: "Alice", Age: 30},
        Company: "Tech Corp",
    }
    
    // 可以直接访问嵌入类型的字段
    fmt.Printf("Name: %s\n", emp.Name)  // 等价于 emp.Person.Name
    fmt.Printf("Age: %d\n", emp.Age)
    
    // 内存布局:
    // Employee 包含 Person 的所有字段 + Company 字段
    // +------+-----+---------+
    // | Name | Age | Company |
    // +------+-----+---------+
}

// 4. 结构体标签(Tags)
func demonstrateStructTags() {
    type User struct {
        Name  string `json:"name" db:"user_name"`
        Email string `json:"email" db:"email_address"`
        Age   int    `json:"age,omitempty" db:"age"`
    }
    
    u := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    
    // 使用反射读取标签
    t := reflect.TypeOf(u)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("字段: %s, JSON标签: %s, DB标签: %s\n",
            field.Name,
            field.Tag.Get("json"),
            field.Tag.Get("db"))
    }
}

// 5. 结构体比较
func demonstrateStructComparison() {
    type Point struct {
        X, Y int
    }
    
    p1 := Point{1, 2}
    p2 := Point{1, 2}
    p3 := Point{2, 3}
    
    // ✅ 可比较:所有字段都是可比较类型
    fmt.Println(p1 == p2)  // true
    fmt.Println(p1 == p3)  // false
    
    type BadPoint struct {
        X, Y  int
        Data  []int  // slice 不可比较
    }
    
    // ❌ 不可比较:包含 slice
    // bp1 := BadPoint{1, 2, []int{1, 2}}
    // bp2 := BadPoint{1, 2, []int{1, 2}}
    // fmt.Println(bp1 == bp2)  // 编译错误
}
🎯 Struct 性能优化技巧
go
// 1. 字段对齐优化
type OptimizedStruct struct {
    // 按照大小降序排列
    ptr1   *int      // 8 bytes
    ptr2   *int      // 8 bytes
    int64Val int64   // 8 bytes
    int32Val int32   // 4 bytes
    int16Val int16   // 2 bytes
    int8Val  int8    // 1 byte
    boolVal  bool    // 1 byte
    // 总大小:32 bytes(无padding)
}

// 2. 使用指针避免大结构体拷贝
type LargeStruct struct {
    data [1024]byte
}

// ❌ 低效:值传递,拷贝整个结构体
func processValue(ls LargeStruct) {
    // 拷贝了 1024 bytes
}

// ✅ 高效:指针传递,只拷贝 8 bytes
func processPointer(ls *LargeStruct) {
    // 只拷贝指针
}

// 3. 使用空结构体节省内存
type Config struct {
    Enabled     bool
    EnableDebug struct{}  // 0 bytes,用作标记
}

📊 性能对比总结

操作SliceMapChannelStruct
内存占用24 bytes (header)8 bytes (指针)8 bytes (指针)字段总和+padding
访问速度O(1)O(1)平均需加锁O(1)
扩容成本中等高(渐进式)不可变不可变
并发安全
底层结构数组哈希表环形队列+锁连续内存

🎯 面试高频问题

Q1: 为什么 slice 传递给函数后,在函数内修改会影响原slice?

go
// 因为 slice 是引用类型,传递的是 slice header(包含指针)
// header 是值拷贝,但指向的底层数组是同一个
func modifySlice(s []int) {
    s[0] = 100  // ✅ 修改底层数组,影响原slice
}

func appendSlice(s []int) {
    s = append(s, 4)  // ❌ 可能重新分配,不影响原slice
}

Q2: 为什么 map 必须用 make 初始化?

go
// 因为 map 需要初始化复杂的哈希表结构:
// - 分配 bucket 数组
// - 初始化 hash 种子
// - 设置初始容量
// new(map) 只返回 nil 指针,无法使用

Q3: Channel 关闭后还能接收数据吗?

go
// 可以!已缓冲的数据仍然可以接收
// 但接收完后会返回零值和 false
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)

fmt.Println(<-ch)  // 1
fmt.Println(<-ch)  // 2
val, ok := <-ch    // val=0, ok=false

Q4: 如何减少 struct 的内存占用?

go
// 1. 字段按大小降序排列(减少 padding)
// 2. 使用小类型(int8 vs int64)
// 3. 使用位字段(bitfield)
// 4. 使用空结构体做标记

6:类型转换和类型断言

详细解答

1. 基本类型转换

go
func basicTypeConversion() {
    // 数值类型转换
    var i int = 42
    var f float64 = float64(i)  // int转float64
    var u uint = uint(i)        // int转uint
    
    // 可能丢失精度的转换
    var big float64 = 1.7976931348623157e+308
    var small float32 = float32(big)  // +Inf (溢出)
    
    // 字符串和数值转换需要使用strconv包
    import "strconv"
    
    str := strconv.Itoa(i)           // int转string
    num, err := strconv.Atoi("123")  // string转int
    if err != nil {
        fmt.Printf("转换失败: %v\n", err)
    }
    
    fmt.Printf("i=%d, f=%f, u=%d\n", i, f, u)
    fmt.Printf("big=%g, small=%g\n", big, small)
    fmt.Printf("str=%s, num=%d\n", str, num)
}

2. 类型断言

go
func typeAssertion() {
    var i interface{} = "hello"
    
    // 类型断言(可能panic)
    s := i.(string)
    fmt.Printf("断言成功: %s\n", s)
    
    // 安全的类型断言
    if s, ok := i.(string); ok {
        fmt.Printf("安全断言成功: %s\n", s)
    }
    
    // 断言失败的情况
    if n, ok := i.(int); ok {
        fmt.Printf("这不会执行: %d\n", n)
    } else {
        fmt.Println("断言失败: i不是int类型")
    }
    
    // 危险操作:会panic
    // n := i.(int)  // panic: interface conversion: interface {} is string, not int
}

3. 类型开关

go
func typeSwitch(x interface{}) {
    switch v := x.(type) {
    case nil:
        fmt.Println("x是nil")
    case bool:
        fmt.Printf("x是bool类型,值为%t\n", v)
    case int:
        fmt.Printf("x是int类型,值为%d\n", v)
    case string:
        fmt.Printf("x是string类型,值为%s\n", v)
    case []int:
        fmt.Printf("x是[]int类型,值为%v\n", v)
    default:
        fmt.Printf("x是未知类型%T,值为%v\n", v, v)
    }
}

func demonstrateTypeSwitch() {
    typeSwitch(nil)
    typeSwitch(true)
    typeSwitch(42)
    typeSwitch("hello")
    typeSwitch([]int{1, 2, 3})
    typeSwitch(3.14)
}

4. 复杂类型转换示例

go
import (
    "encoding/json"
    "fmt"
)

func complexTypeConversion() {
    // 结构体转JSON
    type User struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }
    
    user := User{Name: "Alice", Age: 30}
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Printf("JSON序列化失败: %v\n", err)
        return
    }
    
    fmt.Printf("JSON数据: %s\n", jsonData)
    
    // JSON转结构体
    var user2 User
    err = json.Unmarshal(jsonData, &user2)
    if err != nil {
        fmt.Printf("JSON反序列化失败: %v\n", err)
        return
    }
    
    fmt.Printf("反序列化结果: %+v\n", user2)
    
    // interface{}转具体类型
    var data interface{} = map[string]interface{}{
        "name": "Bob",
        "age":  float64(25),  // JSON数字默认是float64
    }
    
    if m, ok := data.(map[string]interface{}); ok {
        name := m["name"].(string)
        age := int(m["age"].(float64))
        fmt.Printf("提取的数据: name=%s, age=%d\n", name, age)
    }
}

🎯 核心知识点总结

基本类型要点

  1. 整数类型: 有符号(int8-int64)和无符号(uint8-uint64),注意溢出
  2. 浮点类型: float32和float64,注意精度问题
  3. 字符类型: byte(ASCII)和rune(Unicode)的区别
  4. 布尔类型: 只有true和false,零值为false

复合类型要点

  1. 数组: 固定长度,值类型,长度是类型的一部分
  2. 切片: 动态数组,引用类型,底层指向数组
  3. 映射: 键值对,引用类型,无序
  4. 结构体: 自定义类型,值类型,支持方法

内存分配要点(重要!)

  1. new(T): 为任何类型分配零值内存,返回*T指针
    • 适用于所有类型
    • 返回的引用类型(slice/map/channel)是nil,不可直接使用
    • 类比:分配"毛坯房",给你地址但房子是空的
  2. make(T): 只用于slice/map/channel,返回初始化的T本身
    • 完整初始化内部数据结构
    • 返回的值立即可用
    • 类比:盖房子并装修好,可以直接入住
  3. 最佳实践:
    • slice/map/channel用make
    • 结构体用&T{}
    • 基本类型按需选择
  4. C语言类比:
    • new ≈ calloc() + 返回指针
    • make ≈ 复杂的多步初始化过程

类型转换要点

  1. 显式转换: Go不支持隐式类型转换
  2. 安全检查: 使用comma ok惯用法进行安全断言
  3. 类型开关: 处理多种类型的优雅方式
  4. 性能考虑: 频繁的类型转换可能影响性能

内存布局要点

  1. 值类型: 数组、结构体直接存储数据
  2. 引用类型: 切片、映射、通道存储引用
  3. 指针类型: 存储内存地址,支持间接访问
  4. 接口类型: 存储类型信息和数据指针

🔍 进阶学习建议

  1. 深入理解内存模型: 掌握值类型和引用类型的区别
  2. 性能优化: 了解不同类型操作的性能特征
  3. 并发安全: 理解哪些类型是并发安全的
  4. 最佳实践: 学习Go社区的类型使用惯例

正在精进