卡码笔记-最强八股文
首页
计算机基础
C++
Java
Go
面经
笔记广场 (opens new window)
代码随想录 (opens new window)
首页
计算机基础
C++
Java
Go
面经
笔记广场 (opens new window)
代码随想录 (opens new window)
  • Go八股

    • Go基础
    • Go并发
    • Go web
    • Go 垃圾回收

# Go语言基础

# 和Java对比介绍⼀下Go语⾔的特点和优势(考点:对编程语言的理解)【简单】

总的来说,Go语言在性能、并发处理、部署和开发效率上都有其独特的优势,尤其适合网络服务和云计算领域

  • 语法简洁:Go的语法非常简洁,没有类和继承等概念,代码易于读写和维护
  • 编译型语言:Go是编译型语言,编译成机器码直接运行,但编译速度非常快
  • 高性能:Go语言的执行速度非常接近C和C++,比Java更快
  • 并发支持:Go语言的并发模型是基于goroutine和channel,使得并发编程变得简单高效,而Java的多线程模型相对来说更复杂一些
  • 内存管理:Go拥有自己的垃圾回收机制,简化了内存管理
  • 部署简单:Go程序编译后生成单一的可执行文件,部署非常简单
  • 标准库丰富:Go拥有高质量的标准库,涵盖网络、加密、数据结构等方面
  • 工具链:Go有一套强大的工具链,如用于格式化代码的gofmt、用于性能分析的pprof
  • 静态类型:Go是静态类型语言,有助于在编译时捕捉错误
  • 跨平台编译:Go支持跨平台编译,可以很方便地为不同操作系统构建应用程序

# go包管理的方式有哪些?(考点:包管理)【简单】

# 简要回答

Go语言的包管理主要经历了几个阶段,最开始是GOPATH的方式,现在主要用Go Modules。

# 详细回答

1.GOPATH:这是早期Go语言的包管理方式。每个项目都需要放在GOPATH的下面,Go会从GOPATH的src目录寻找所有的包。

2.vendor:早期第三方推出的一个Go语言的包管理方式。允许将项目依赖的外部包直接下载到项目的vendor目录中。

3.GO MODULES:官方从1.11版本开始引入,成了官方推荐的包管理方式。不再依赖GOPATH,可以直接在任何地方创建项目,通过go.mod文件来管理依赖。

# Go支持重载吗?如何在Go中实现一个方法的“重载”?(考点:方法重载)【中等】

# 详细回答

Go 不支持函数/方法的重载,你不能在同一个作用域中定义多个函数名相同但参数不同的函数 会报编译错误:“(function name) redeclared in this block”。

虽然语法上不支持重载,但可以通过以下方式模拟:

  1. 使用接口+类型断言
func Add(a, b interface{}) interface{} {
    switch aVal := a.(type) {
    case int:
        if bVal, ok := b.(int); ok {
            return aVal + bVal
        }
    case float64:
        if bVal, ok := b.(float64); ok {
            return aVal + bVal
        }
    }
    return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 使用范型(1.18版本后)
func Add[T int | float64](a, b T) T {
    return a + b
}

fmt.Println(Add(1, 2))       // 3
fmt.Println(Add(1.1, 2.2))   // 3.3
1
2
3
4
5
6

泛型是 Go 实现“重载”的最佳选择,因为它是类型安全的,且有编译时检查

3.使用组合+接口。

不同的方法封装在不同的嵌套结构中,外部选择性调用这些方法

// 定义包含不同方法的结构体
type StringPrinter struct{}
func (p StringPrinter) Print(s string) {
    fmt.Println("String:", s)
}

type IntPrinter struct{}
func (p IntPrinter) Print(n int) {
    fmt.Println("Int:", n)
}


// **组合成一个“统一接口”
type Printer struct {
    StringPrinter
    IntPrinter
}


//
func main() {
    p := Printer{}
    p.Print("hello") // 调用 StringPrinter.Print
    p.Print(42)      // 调用 IntPrinter.Print
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

结合方法2和方法3:

type Printable interface {
    PrintAny(v any)
}

type Printer struct {
    StringPrinter
    IntPrinter
}

func (p Printer) PrintAny(v any) {
    switch val := v.(type) {
    case string:
        p.Print(val)
    case int:
        p.Print(val)
    default:
        fmt.Println("Unsupported type")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 利用函数传递 |特性|用法| |:-😐:-😐 |函数是一等公民|可以将函数作为参数传递| |接口 interface{}|可以接收任意类型参数| |类型断言 / type switch|可以在运行时判断传入值的类型|
// 定义重载实现函数
func printInt(i int) {
    fmt.Println("int:", i)
}

func printString(s string) {
    fmt.Println("string:", s)
}

func printFloat(f float64) {
    fmt.Println("float:", f)
}
// 定义统一调度函数
func Print(value interface{}) {
    switch v := value.(type) {
    case int:
        printInt(v)
    case string:
        printString(v)
    case float64:
        printFloat(v)
    default:
        fmt.Println("unsupported type")
    }
}


// 或者像下面这样
func Dispatch(handler interface{}) {
    switch h := handler.(type) {
    case func(int):
        h(100)
    case func(string):
        h("Go")
    case func(float64):
        h(3.14)
    default:
        fmt.Println("unsupported handler")
    }
}

Dispatch(func(i int) {
    fmt.Println("handle int:", i)
})

Dispatch(func(s string) {
    fmt.Println("handle string:", s)
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

重写(overriding)在 OOP 领域中是指子类重写父类的方法,在 go 中称为方法的覆盖(当一个嵌套结构体(被组合的 struct)和外部结构体拥有相同方法名时,外部的方法会覆盖嵌套结构体的方法。)

# 扩展回答

1.什么是类型安全? 程序中的变量只能用于其所属类型允许的操作,不允许发生不合理的类型转换或操作。

好处 说明
防止错误 避免对类型使用不合法的操作(如把字符串当成数字)
提高可读性 变量类型明确,代码更清晰
增强 IDE 智能提示 自动补全和类型跳转依赖类型信息
更好的性能优化 编译器可以做更激进的优化(例如内联、分配优化等)
  1. 什么是编译时检查? 编译器在代码编译阶段就会检查语法、类型、常量表达式、未使用变量等错误。

3.静态类型与编译时检查 静态类型语言中,变量的类型在编译时就确定,不能随意更改。动态类型语言中,变量的类型在运行时才决定,变量可以赋不同类型的值。

  • 如果语言是静态类型的,通常就支持编译时类型检查;
  • 如果语言是动态类型的,通常类型检查只能在运行时进行

# Go语言中如何实现继承?(考点:面向对象编程)【中等】

Go 语言中并没有传统的继承机制(如 Java 的 extends 或 C++ 的基类继承),而是通过**组合(Composition)**实现类似继承的功能。这种方式符合 Go 的设计哲学:优先使用组合,而非继承。

以下是实现继承的方式及相关说明:


# 1. 嵌套结构体实现“继承”

在 Go 中,结构体可以将另一个结构体嵌套为自己的字段,从而实现类似继承的行为。嵌套结构体中的字段和方法会被提升到外部结构体中,可以直接访问和调用。

示例:

package main

import "fmt"

// 父结构体
type Animal struct {
    Name string
}

func (a Animal) Speak() {
    fmt.Println(a.Name, "is making a sound")
}

// 子结构体
type Dog struct {
    Animal // 嵌套 Animal,相当于继承
    Breed  string
}

func main() {
    dog := Dog{
        Animal: Animal{Name: "Buddy"},
        Breed:  "Golden Retriever",
    }
    dog.Speak() // 调用嵌套结构体的方法
    fmt.Println(dog.Name, "is a", dog.Breed)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

输出:

Buddy is making a sound
Buddy is a Golden Retriever
1
2

特点:

  • 通过嵌套结构体实现了方法和字段的复用。

  • Dog 结构体直接“继承”了 Animal 的字段 Name 和方法 Speak。


# 2. 方法重写

子结构体可以定义与父结构体相同的方法,从而覆盖嵌套结构体的方法,实现类似方法重写的功能。

示例:

func (d Dog) Speak() {
    fmt.Println(d.Name, "is barking")
}

func main() {
    dog := Dog{
        Animal: Animal{Name: "Buddy"},
        Breed:  "Golden Retriever",
    }
    dog.Speak() // 调用 Dog 的 Speak 方法,而非 Animal 的
}
1
2
3
4
5
6
7
8
9
10
11

输出:

Buddy is barking
1

特点:

  • Dog 的 Speak 方法覆盖了 Animal 的 Speak 方法。

  • 如果需要调用被覆盖的方法,可以显式调用嵌套结构体的方法,例如 dog.Animal.Speak()。


# 3. 接口与组合的结合

Go 的接口配合组合机制,可以实现类似继承的多态功能。

示例:

type Speaker interface {
    Speak()
}

type Animal struct {
    Name string
}

func (a Animal) Speak() {
    fmt.Println(a.Name, "is making a sound")
}

type Dog struct {
    Animal
}

func (d Dog) Speak() {
    fmt.Println(d.Name, "is barking")
}

func makeSound(s Speaker) {
    s.Speak()
}

func main() {
    a := Animal{Name: "Generic Animal"}
    d := Dog{Animal: Animal{Name: "Buddy"}}

    makeSound(a) // 调用 Animal 的 Speak
    makeSound(d) // 调用 Dog 的 Speak
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

输出:

Generic Animal is making a sound
Buddy is barking
1
2

特点:

  • 通过接口定义行为(如 Speak 方法)。

  • 子结构体通过组合和接口实现多态行为。


# 4. 匿名组合(匿名字段)与字段提升

当一个结构体嵌套另一个结构体时,如果嵌套的是匿名字段,那么嵌套结构体的字段和方法会被“提升”为外部结构体的字段和方法。

示例:

type Address struct {
    City, State string
}

type Person struct {
    Name    string
    Address // 匿名字段
}

func main() {
    p := Person{
        Name:    "Alice",
        Address: Address{City: "San Francisco", State: "CA"},
    }
    fmt.Println(p.Name, "lives in", p.City, p.State) // Address 的字段被提升
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

输出:

Alice lives in San Francisco CA
1

特点:

  • Person 结构体直接访问 Address 的字段 City 和 State,表现得像继承。

# 5. 区别于传统继承

虽然 Go 的组合机制和传统继承类似,但它并不支持:

  • 访问控制:没有 protected 关键字,所有嵌套字段和方法的访问权限取决于其首字母是否大写。

  • 强制的父子关系:嵌套结构体是组合关系,而不是严格的父子继承关系。

  • 多级继承:嵌套的组合机制更简单,不涉及复杂的继承层级。


# 6. 使用场景

Go 的组合机制更倾向于灵活复用,通常会在以下场景中使用:

  1. 复用代码:通过嵌套结构体共享字段和方法。

  2. 实现多态:通过接口和组合模拟继承行为。

  3. 解耦设计:避免传统继承带来的强耦合问题。


# 总结

  • Go 不支持传统的继承,但可以通过 结构体嵌套 和 接口 实现类似的功能。

  • 组合机制更加灵活,减少了传统继承中的复杂性和层级耦合。

  • Go 的设计哲学是通过组合和接口实现代码复用,而不是依赖复杂的继承体系。

# Go语言中如何实现多态?(考点:面向对象编程)【中等】

在 Go 语言中,多态主要通过接口(interface)实现。接口定义了一组行为(方法),不同的类型通过实现这些方法来满足接口,从而达到多态的效果。Go 的接口采用隐式实现机制,即只要一个类型实现了接口中的所有方法,就认为该类型实现了该接口。

以下是如何在 Go 中实现多态的详细说明和示例:


# 1. 什么是多态?

多态是面向对象编程的一个核心特性,指同一接口可以在不同类型上表现出不同的行为。

特点:

  • 提高代码的扩展性和复用性。

  • 同一操作在不同对象上的具体实现不同。

在 Go 中,多态通过接口和类型实现接口来实现。


# 2. 基于接口的多态实现

# 示例:使用接口定义行为
package main

import "fmt"

// 定义接口
type Shape interface {
    Area() float64 // 定义一个行为:计算面积
}

// 定义圆形
type Circle struct {
    Radius float64
}

// Circle 实现 Shape 接口
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

// 定义矩形
type Rectangle struct {
    Width, Height float64
}

// Rectangle 实现 Shape 接口
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 使用多态
func printArea(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
}

func main() {
    c := Circle{Radius: 5}
    r := Rectangle{Width: 4, Height: 6}

    printArea(c) // 调用 Circle 的 Area 方法
    printArea(r) // 调用 Rectangle 的 Area 方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

输出:

Area: 78.50
Area: 24.00
1
2

分析:

  • Shape 是接口,定义了 Area 方法。

  • Circle 和 Rectangle 分别实现了 Shape 接口。

  • 函数 printArea 可以接受任何实现了 Shape 接口的类型,实现多态。


# 3. 动态多态:接口变量存储不同类型

接口变量可以动态存储实现该接口的任何类型。

示例:

package main

import "fmt"

// 定义接口
type Speaker interface {
    Speak()
}

// 定义两种类型
type Dog struct{}
type Cat struct{}

// Dog 实现 Speak 方法
func (d Dog) Speak() {
    fmt.Println("Woof!")
}

// Cat 实现 Speak 方法
func (c Cat) Speak() {
    fmt.Println("Meow!")
}

func main() {
    var s Speaker

    s = Dog{}
    s.Speak() // 输出: Woof!

    s = Cat{}
    s.Speak() // 输出: Meow!
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

分析:

  • 接口变量 s 动态存储了不同的类型 Dog 和 Cat。

  • 调用 s.Speak() 时,执行的是对应类型的方法。


# 4. 类型断言与类型选择

Go 提供了类型断言和**类型选择(type switch)**来识别接口变量中存储的具体类型。

# 类型断言
func main() {
    var s Speaker = Dog{}

    // 类型断言
    d, ok := s.(Dog)
    if ok {
        fmt.Println("This is a Dog")
        d.Speak()
    } else {
        fmt.Println("Not a Dog")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
# 类型选择(type switch)
func describe(s Speaker) {
    switch v := s.(type) {
    case Dog:
        fmt.Println("This is a Dog")
        v.Speak()
    case Cat:
        fmt.Println("This is a Cat")
        v.Speak()
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    describe(Dog{}) // 输出: This is a Dog
    describe(Cat{}) // 输出: This is a Cat
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 5. 利用空接口实现泛型化的多态

Go 中的空接口 interface{} 可以接受任何类型,但需要配合类型断言或反射来使用。

示例:

func printValue(value interface{}) {
    switch v := value.(type) {
    case int:
        fmt.Println("Integer:", v)
    case string:
        fmt.Println("String:", v)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    printValue(42)         // 输出: Integer: 42
    printValue("hello")    // 输出: String: hello
    printValue(3.14)       // 输出: Unknown type
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 6. 使用 Go 1.18 的泛型增强多态

在 Go 1.18 及以上版本中,可以通过泛型实现更强大的多态支持。

示例:

package main

import "fmt"

// 泛型函数
func printValue[T any](value T) {
    fmt.Printf("Value: %v\n", value)
}

func main() {
    printValue(42)         // 输出: Value: 42
    printValue("hello")    // 输出: Value: hello
    printValue(3.14)       // 输出: Value: 3.14
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 7. Go 中多态的优势

  1. 简洁性:通过接口定义行为,使代码更清晰。

  2. 松耦合:接口和实现分离,降低代码耦合。

  3. 灵活性:接口变量可动态存储不同类型,实现运行时多态。


# 总结

  • 接口是实现多态的核心:通过接口定义行为,不同类型实现接口来表现不同的行为。

  • 接口变量实现动态多态:接口变量可以存储不同类型的值,并调用其对应的方法。

  • 类型断言和类型选择:在需要时,可以识别接口变量的具体类型。

  • 泛型增强多态(Go 1.18+):进一步提高了代码的灵活性和复用性。

这种接口驱动的多态使 Go 语言的代码更简洁、灵活、易维护。

# Go语言中切片和数组的区别是什么?(考点:切片与数组)【简单】

在 Go 语言中,切片(Slice)和数组(Array)是两种常用的数据结构,它们有一些相似之处,但在底层实现和使用方式上有明显的区别。以下是它们的主要区别:


# 1. 定义和长度

  • 数组:

    • 数组是一个长度固定的序列,长度是类型的一部分。
    • 一旦声明,数组的长度不能更改。 示例:
    var arr [3]int // 声明一个长度为3的整型数组
    arr[0] = 1
    fmt.Println(arr) // 输出: [1 0 0]
    
    1
    2
    3
  • 切片:

    • 切片是一个长度可变的序列,引用的是一个底层数组的一部分。
    • 切片本身不存储数据,它是对底层数组的一个视图。 示例:
    var slice []int // 声明一个切片
    slice = append(slice, 1) // 动态添加元素
    fmt.Println(slice)       // 输出: [1]
    
    1
    2
    3

# 2. 长度和容量

  • 数组:

    • 数组的长度是固定的,使用时必须指定长度。
    • 无论数组是否填满,其长度总是等于定义时的大小。 示例:
    arr := [5]int{1, 2, 3}
    fmt.Println(len(arr)) // 输出: 5
    
    1
    2
  • 切片:

    • 切片有两个动态属性:长度(len)和容量(cap)。
    • 长度是切片中当前可用元素的个数。
    • 容量是切片从开始位置到底层数组末尾的元素总数。 示例:
    slice := make([]int, 3, 5) // 创建长度为3,容量为5的切片
    fmt.Println(len(slice))   // 输出: 3
    fmt.Println(cap(slice))   // 输出: 5
    
    1
    2
    3

# 3. 是否可以动态扩容

  • 数组:

    • 数组的长度固定,不能动态扩展或缩减。
  • 切片:

    • 切片是基于数组构建的,可以动态扩展。当切片扩容时,会创建一个更大的底层数组并复制原数据。 示例:
    slice := []int{1, 2, 3}
    slice = append(slice, 4, 5)
    fmt.Println(slice) // 输出: [1 2 3 4 5]
    
    1
    2
    3

# 4. 值传递与引用传递

  • 数组:

    • 数组是值类型,传递数组时会复制整个数组。 示例:
    func modifyArray(a [3]int) {
        a[0] = 99
    }
    arr := [3]int{1, 2, 3}
    modifyArray(arr)
    fmt.Println(arr) // 输出: [1 2 3] (未修改)
    
    1
    2
    3
    4
    5
    6
  • 切片:

    • 切片是引用类型,传递切片时只是引用底层数组的地址。 示例:
    func modifySlice(s []int) {
        s[0] = 99
    }
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println(slice) // 输出: [99 2 3] (已修改)
    
    1
    2
    3
    4
    5
    6

# 5. 初始化方式

  • 数组:

    • 数组需要指定长度,或通过字面量初始化。 示例:
    arr1 := [3]int{1, 2, 3}   // 定长数组
    arr2 := [...]int{4, 5, 6} // 使用省略号自动推断长度
    
    1
    2
  • 切片:

    • 切片可以通过 make、字面量或从数组/已有切片生成。 示例:
    slice1 := make([]int, 3)        // 长度为3,初始值为0的切片
    slice2 := []int{1, 2, 3}        // 字面量初始化
    slice3 := slice2[1:3]           // 从已有切片中生成
    
    1
    2
    3

# 6. 底层结构

  • 数组:

    • 数组是一个连续的内存块,直接存储数据。
  • 切片:

    • 切片是一个引用类型,底层由一个结构体表示,包含:
      • 指向底层数组的指针。
      • 切片的长度。
      • 切片的容量。

# 7. 性能和场景

  • 数组:

    • 性能较高,因为数组的大小是固定的,编译器可以直接优化内存分配。
    • 常用于需要固定大小的场景,例如矩阵或缓冲区。
  • 切片:

    • 更灵活,适用于动态数据处理。
    • 适合需要频繁增删数据的场景。

# 8. 示例对比

数组示例:

arr := [3]int{1, 2, 3}
fmt.Println(arr[0]) // 访问元素
arr[0] = 10         // 修改元素
fmt.Println(arr)    // 输出: [10 2 3]
1
2
3
4

切片示例:

slice := []int{1, 2, 3}
slice = append(slice, 4) // 动态扩展
fmt.Println(slice)       // 输出: [1 2 3 4]
1
2
3

# 总结

特性 数组 切片
长度是否固定 固定 可变
是否支持动态扩展 否 是
是否是值类型 是 否(引用类型)
底层结构 直接存储数据 底层引用数组
使用场景 数据大小固定,性能优先 动态数据处理,灵活性优先

切片更常用于实际开发,因为它灵活且功能强大,而数组则更多用在需要固定大小的场景中。

# slice 底层数据结构是什么?有什么特性?(考点:切片的底层)【中等】

# 简要回答

slice底层数据结构包含三部分:

  • 指向底层数组的指针
  • 切片长度
  • 切片容量
type slice struct {
    ptr *ElementType  // 指向底层数组中某个元素的指针
    len int           // 当前切片的长度(可访问的元素个数)
    cap int           // 当前切片的容量(从 ptr 开始到底层数组末尾的容量)
}
1
2
3
4
5

slice的特性:

  • 动态增长:如果超出容量可以自动扩容
  • 共享底层数组:slice操作往往不复置底层数组,而是共享底层数组
  • 视图:可以基于已有的slice创建新的slice,两者会共享底层数组
  • 连续分配内存:slice本身是不连续的,但是底层数组是连续的,这使得遍历slice很快

# Map、Slice作为参数传递会遇到什么问题?(考点:Map、Slice的使用)【中等】

Map

  • 引用传递:Map是引用类型,Map作为参数传递给函数时,函数内对Map的任何修改都会影响原始Map
  • 并发问题:Map不是并发安全的

Slice

  • Slice也是引用类型,任何对Slice的修改都会影响原始Slice
  • 但如果在函数内部进行扩容操作,会分配新的底层数组,但原始Slice不会引用新的数组

# slice是如何扩容的?(考点:扩容机制)【中等】

在 Go 语言中,切片的扩容是一个自动的过程,当向切片中追加元素(例如通过 append 函数)导致切片长度超过当前容量时,Go 会为切片分配一个新的、容量更大的底层数组,将原数组的数据复制到新的数组中,并更新切片的指针和容量信息。


# 1. 扩容机制

切片的扩容机制遵循以下原则:

  1. 容量不足时触发扩容:

    • 如果 append 操作导致切片的长度超过当前容量(cap),则触发扩容。
  2. 扩容倍数:

    • 当切片的容量较小时(容量小于 1024),新容量通常是当前容量的两倍。
    • 当容量较大时,扩容的增量约为当前容量的 25%。
    • 扩容策略可能因具体的 Go 版本而有所不同。
  3. 数据迁移:

    • 扩容时,Go 会为切片分配一个新的底层数组,将旧数组的数据复制到新数组中。
    • 扩容后的切片会指向新的数组,而旧数组则会被垃圾回收(如果没有其他引用)。

# 2. 扩容规则示例

示例代码:

package main

import "fmt"

func main() {
    slice := make([]int, 0, 2) // 创建一个长度为0、容量为2的切片
    fmt.Printf("Initial: len=%d, cap=%d\n", len(slice), cap(slice))

    for i := 1; i <= 10; i++ {
        slice = append(slice, i)
        fmt.Printf("After append %d: len=%d, cap=%d\n", i, len(slice), cap(slice))
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

输出:

Initial: len=0, cap=2
After append 1: len=1, cap=2
After append 2: len=2, cap=2
After append 3: len=3, cap=4
After append 4: len=4, cap=4
After append 5: len=5, cap=8
After append 6: len=6, cap=8
After append 7: len=7, cap=8
After append 8: len=8, cap=8
After append 9: len=9, cap=16
After append 10: len=10, cap=16
1
2
3
4
5
6
7
8
9
10
11

说明:

  1. 切片初始化时容量为 2。

  2. 第 3 次 append 时,容量不足,触发扩容,容量从 2 增加到 4(倍增)。

  3. 第 9 次 append 时,再次触发扩容,容量从 8 增加到 16(倍增)。


# 3. 扩容的底层实现

切片扩容的核心代码在 Go 的运行时库中(runtime/slice.go)。简化后的扩容逻辑如下:

  1. 判断新容量:

    • 如果当前容量小于 1024,则新容量为原容量的两倍。
    • 如果当前容量大于等于 1024,则新容量增加当前容量的约 25%。
  2. 分配新数组并拷贝数据:

    • 根据计算的容量分配新的底层数组。
    • 使用 copy 函数将原数组的数据复制到新数组中。
  3. 返回新的切片:

    • 返回指向新数组的切片,同时更新切片的长度和容量。

# 4. 为什么扩容时采用倍增策略?

  1. 避免频繁的内存分配:

    • 每次扩容都需要分配新数组并拷贝数据。如果每次只增加固定大小,会导致频繁的内存分配和数据迁移,性能较低。
  2. 保证性能和内存利用率的平衡:

    • 倍增策略在性能和内存使用之间取得了较好的平衡。尽管倍增可能导致一些未使用的内存(容量比实际长度大),但可以显著减少扩容操作的次数。

# 5. 特殊情况下的扩容行为

# 自定义扩容容量

可以通过提前分配足够的容量来避免频繁扩容。

示例:

slice := make([]int, 0, 100) // 提前分配容量为100的切片
1
# 多维切片扩容
  • 如果是多维切片,扩容逻辑同样适用。

  • 注意:每个维度的切片都是独立的,扩容只针对当前维度的切片。


# 6. 扩容的注意事项

  1. 扩容会导致底层数组重新分配:

    • 如果扩容发生,切片的底层数组会被重新分配,原切片和新切片底层数组不再共享。
  2. 可能的性能开销:

    • 扩容涉及到内存分配和数据拷贝。如果数据量较大,频繁扩容可能会影响性能。
  3. 切片扩容和 Goroutine 并发安全问题:

    • 切片本身是线程安全的,但扩容可能导致数据竞争,尤其是当多个 Goroutine 同时访问切片时,需要使用锁或其他并发控制机制。

# 7. 总结

  • 切片扩容是 Go 语言提供的动态内存管理机制,简化了开发者手动管理数组大小的复杂性。

  • 扩容遵循 倍增策略,容量较小时每次加倍,容量较大时以增量扩容,保证性能和内存利用率的平衡。

  • 开发中需要注意扩容的性能开销以及引用特性带来的潜在问题,可以通过预分配容量来优化切片的使用效率。

# Go语言中struct和class有什么区别?(考点:语法基础)【简单】

总的来说,Go的struct更简单,更灵活,通过组合和接口实现代码的复用和多态

  • 没有类继承:在Go中,struct不支持类之间的继承关系
  • 组合:在Go中,struct可以嵌入到另一个struct中,从而复用字段和方法
  • 方法直接绑定:在Go中,方法是绑定到类型的,可以直接给struct或者其他类型定义方法(方法是绑定类型的,函数则与类型无关)
  • 字段可见性:在Go中,struct中的字段默认是公开的,如果字段名首字母小写,则只能在同一个包内访问,如果字段首字母大写,则可以在其他包中访问
  • 接口实现:在Go中,struct可以实现接口但不需要显式声明,只要struct中实现了接口的所有方法,就实现了该接口
  • 构造函数:在Go中,struct没有构造函数

# 讲讲Go的错误处理机制。(考点:错误处理)【中等】

Go 语言的错误处理机制以简洁和明确为核心,避免了传统语言中常见的复杂异常机制,强调通过显式返回错误值进行错误处理。以下是 Go 错误处理的核心概念、方法和特点:


# 1. Go 的错误处理机制特点

  1. 显式错误返回

    • Go 没有像 Java 或 Python 那样的异常机制,不使用 try-catch。
    • 通过函数的多返回值机制,直接返回错误值。
  2. error 接口

    • Go 提供了一个内置的 error 接口,用于描述错误:
      type error interface {
          Error() string
      }
      
      1
      2
      3
    • 函数或方法可以通过返回实现了 error 接口的对象来指示是否发生错误。
  3. 鼓励显式检查

    • 开发者需要显式检查函数返回的错误并处理,代码更具可读性和可控性。

# 2. 错误处理的基本用法

# (1) 使用内置 errors.New 生成错误
  • errors.New 用于简单创建错误对象。

  • 常见用法是返回一个 error,让调用者检查。

import (
    "errors"
    "fmt"
)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err) // 输出: Error: division by zero
        return
    }
    fmt.Println("Result:", result)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# (2) 使用 fmt.Errorf 格式化错误
  • 可以用 fmt.Errorf 添加更多上下文信息。
import (
    "fmt"
)

func openFile(filename string) error {
    return fmt.Errorf("failed to open file %s: %w", filename, errors.New("file not found"))
}

func main() {
    err := openFile("example.txt")
    if err != nil {
        fmt.Println(err)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# (3) 返回 nil 表示无错误
  • Go 中 nil 是 error 的零值,表示没有错误。
func isPositive(number int) error {
    if number < 0 {
        return errors.New("number is negative")
    }
    return nil
}
1
2
3
4
5
6

# 3. 自定义错误类型

# (1) 创建自定义错误类型
  • Go 允许开发者定义自己的错误类型,提供更细粒度的错误信息。
type DivideError struct {
    Dividend int
    Divisor  int
}

func (e *DivideError) Error() string {
    return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivideError{Dividend: a, Divisor: b}
    }
    return a / b, nil
}

func main() {
    _, err := divide(10, 0)
    if err != nil {
        fmt.Println(err) // 输出: cannot divide 10 by 0
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# (2) 使用 errors.Is 和 errors.As 检查特定错误
  • errors.Is:检查错误是否等于某个特定错误。

  • errors.As:判断错误类型,并提取为特定的错误类型。

import (
    "errors"
    "fmt"
)

var ErrDivideByZero = errors.New("division by zero")

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }
    return a / b, nil
}

func main() {
    _, err := divide(10, 0)
    if errors.Is(err, ErrDivideByZero) {
        fmt.Println("Cannot divide by zero!") // 输出: Cannot divide by zero!
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 4. panic 和 recover

# (1) panic
  • 表示程序遇到了无法恢复的严重错误,程序会中止运行。

  • 用于不可预期的异常情况,如数组越界、空指针等。

func main() {
    panic("something went wrong")
    fmt.Println("This will not be printed")
}
1
2
3
4
# (2) recover
  • 用于捕获 panic 并使程序恢复运行。

  • 一般结合 defer 使用,写在可能导致 panic 的函数中。

func safeDivide(a, b int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    if b == 0 {
        panic("division by zero")
    }
    fmt.Println("Result:", a/b)
}

func main() {
    safeDivide(10, 0)
    fmt.Println("Program continues...")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# (3) 不建议滥用 panic
  • panic 适用于程序运行时的不可恢复错误。

  • 普通错误处理应优先使用 error 返回值。


# 5. 错误处理的最佳实践

  1. 优先使用显式错误检查

    • 避免滥用 panic 和 recover。
    • 在大多数情况下,通过返回 error 处理错误更优雅和可控。
  2. 添加上下文信息

    • 使用 fmt.Errorf 或自定义错误类型,为错误提供更多上下文。
  3. 明确错误边界

    • 在接口、模块或服务之间,定义清晰的错误边界,避免错误泄漏到外部。
  4. 统一错误管理

    • 定义公共错误变量或类型,便于在程序中复用和统一处理。
  5. 日志与监控

    • 在生产环境中记录错误日志,便于分析和调试。

# 6. 示例:综合错误处理

import (
    "errors"
    "fmt"
)

var ErrInvalidInput = errors.New("invalid input")

func calculate(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("%w: divisor cannot be zero", ErrInvalidInput)
    }
    return a / b, nil
}

func main() {
    result, err := calculate(10, 0)
    if err != nil {
        if errors.Is(err, ErrInvalidInput) {
            fmt.Println("Input error:", err)
        } else {
            fmt.Println("Unexpected error:", err)
        }
        return
    }
    fmt.Println("Result:", result)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

总结:

  • Go 的错误处理机制简单直接,通过显式返回值避免隐藏的异常逻辑。

  • 尽管 panic/recover 提供了异常处理能力,但不应滥用,应优先采用显式错误返回。

  • error 接口结合自定义错误类型和上下文信息,使错误处理更灵活和强大。

Last Updated: 3/19/2026, 4:49:23 PM

Go并发 →

评论

验证登录状态...

侧边栏
夜间
卡码简历
代码随想录
卡码投递表🔥
2026群
添加客服微信 PS:通过微信后,请发送姓名-学校-年级-2026实习/校招
支持卡码笔记
鼓励/支持/赞赏Carl
1. 如果感觉本站对你很有帮助,也可以请Carl喝杯奶茶,金额大小不重要,心意已经收下
2. 希望大家都能梦想成真,有好的前程,加油💪