跳转至

容器

数组

Go语言中的数组(Array)与传统编程语言中的数组有所不同, 主要特点如下:

1. 基本概念:

  • 数组是同一种数据类型固定长度的序列.
  • 数组的长度在定义时必须是常量, 并且是数组类型的组成部分. 一旦定义, 长度不能改变.
  • 例如, var a [5]intvar a [10]int 是两种不同的数组类型, 因为它们的长度不同.

2. 声明与初始化:

  • 声明: var 变量名 [长度]类型, 如 var a [5]int.
  • 初始化:
    • 完全初始化: var arr1 = [5]int{1, 2, 3, 4, 5}
    • 部分初始化: var arr0 [5]int = [5]int{1, 2, 3} (未初始化的元素默认为零值)
    • 自动推断长度: var arr2 = [...]int{1, 2, 3, 4, 5, 6} (使用 ... 让编译器根据初始化值推断数组长度)
    • 指定索引初始化: var str = [5]string{3: "hello world", 4: "tom"} (只初始化特定索引的元素)
    • 结构体数组初始化: 可以初始化包含结构体的数组, 如 d := [...]struct { name string; age uint8 }{ {"user1", 10}, {"user2", 20} }

3. 访问:

  • 数组元素通过下标访问, 下标从 0 开始, 最后一个元素的下标是 len-1.
  • 如果下标超出数组合法范围, 会触发 panic (访问越界).
  • 可以使用 for 循环和 for range 循环遍历数组.

4. 值类型:

  • Go 语言中的数组是值类型. 这意味着当数组进行赋值或作为函数参数传递时, 会复制整个数组, 而不是传递指针. 因此, 对副本的修改不会影响原始数组.
  • 为了避免性能问题(特别是对于大型数组), 通常建议使用切片(slice)数组指针 *[n]T 来传递数组.

5. 比较操作符:

  • 数组支持 ==!= 操作符, 因为它们的内存总是被初始化过的.

6. 指针数组与数组指针:

  • 指针数组 [n]*T: 数组的元素是指针.
  • 数组指针 *[n]T: 指向数组的指针.

7. 内置函数:

  • len(a): 返回数组的长度(元素数量).
  • cap(a): 对于数组, cap 也返回数组的长度.

8. 多维数组:

  • Go 语言也支持多维数组, 声明和初始化方式类似一维数组.
  • 例如: var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}.
  • 遍历多维数组可以使用嵌套的 for range 循环.

切片

Go 语言中的切片(Slice)是对数组的引用, 因此它是引用类型, 但其自身是结构体, 在传递时是值拷贝.

以下是关于 Go 切片的一些关键点:

  • 可变长度: 切片的长度是可变的, 可以看作是一个可变的数组.
  • 长度与容量:
    • len() 函数返回切片中可用元素的数量, 读写操作不能超过此限制.
    • cap() 函数返回切片的最大扩展容量, 不能超出其底层数组的限制.
    • 切片的长度始终小于或等于容量: 0 <= len(slice) <= cap(array).
  • nil 切片: 如果一个切片为 nil, 那么它的 lencap 都等于 0.
  • 创建方式:
    • 声明切片: var s1 []int
    • 短声明: s2 := []int{}
    • 使用 make() 函数:
      • var s3 []int = make([]int, 0)
      • slice := make([]type, len)
      • slice := make([]type, len, cap)
    • 初始化赋值: s5 := []int{1, 2, 3}
    • 从数组切片: arr := [5]int{1, 2, 3, 4, 5}; s6 = arr[1:4] (前闭后开区间)
  • 切片初始化(全局和局部): 可以通过 arr[start:end], arr[:end], arr[start:], arr[:] 等方式从数组中创建切片.
  • 内存布局: 切片操作的实际目标是底层数组, 需要注意索引号的差异.
  • append 函数:
    • 用于向切片尾部添加数据, 并返回新的切片对象.
    • 如果追加数据后超出原切片的容量 (cap) 限制, Go 会重新分配一个更大的底层数组, 并将原有数据复制过去(通常以2倍容量重新分配).
    • 为了减少内存分配和数据复制开销, 在大批量添加数据时, 建议一次性分配足够大的空间, 或初始化足够长的 len 属性, 然后通过索引操作.
    • 及时释放不再使用的切片对象, 避免持有过期数组导致垃圾回收无法进行.
  • cap 重新分配规律: 当切片容量不足时, 通常会以2倍的方式进行扩容.
  • copy 函数:
    • 在两个切片之间复制数据, 复制长度以较小的切片长度为准.
    • 两个切片可以指向同一个底层数组, 允许元素区间重叠.
    • 建议及时将所需数据复制到较小的切片中, 以便释放超大号底层数组的内存.
  • 切片遍历: 使用 for index, value := range slice 结构进行遍历.
  • 切片调整大小(resize): 可以通过重新切片来调整切片的大小, 例如 b := a[1:2]c := b[0:3].
  • 字符串与切片:
    • Go 语言中字符串的底层是 byte 数组, 因此可以进行切片操作.
    • 字符串本身是不可变的. 要修改字符串中的字符, 需要先将其转换为 []byte (英文字符串) 或 []rune (包含中文字符的字符串), 修改后再转换回 string.
  • 三索引切片 (slice[x:y:z]):
    • slice[x:y:z] 表示切片内容为 [x:y], 切片长度为 y-x, 切片容量为 z-x.
    • 例如 slice[:6:8] 表示切片内容从索引 0 到 6(不包含6), 长度为 6, 容量为 8.
  • 数组或切片转字符串: 可以使用 strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1) 的方式将数组或切片转换为逗号分隔的字符串.

指针

包括以下几个方面:

  1. 指针的概念:

    • Go 语言中的指针与 C/C++ 不同, 是"安全指针", 不能进行偏移和运算.
    • 理解指针需要掌握三个概念: 指针地址, 指针类型和指针取值.
  2. Go 语言中的指针:

    • Go 语言函数传参是值拷贝, 如果想修改变量, 可以使用指向该变量地址的指针变量.
    • 使用指针传递数据可以避免拷贝数据.
    • Go 语言中的指针操作主要使用 & (取地址) 和 * (根据地址取值) 两个符号.
  3. 指针地址和指针类型:

    • 每个变量在运行时都有一个内存地址.
    • & 操作符用于获取变量的地址.
    • 值类型 (int, float, bool, string, array, struct) 都有对应的指针类型, 例如 *int, *string.
    • 通过 ptr := &v 语法可以获取变量 v 的地址, ptr 的类型为 *T (T 的指针类型).
  4. 指针取值:

    • 对普通变量使用 & 获取指针后, 可以使用 * 操作符对指针进行取值, 获取指针指向的原变量的值.
    • &* 是一对互补的操作符.
    • 总结了变量, 指针地址, 指针变量, 取地址, 取值的相互关系和特性.
    • 通过 modify1modify2 函数的例子, 说明了值传递和指针传递的区别.
  5. 空指针:

    • 当指针未分配任何变量时, 其值为 nil.
    • 提供了判断空指针的示例代码.
  6. new 和 make:

    • 在 Go 语言中, 引用类型变量在使用前需要声明并分配内存空间.
    • newmake 是 Go 语言内置的两个用于分配内存的函数.
  7. new 函数:

    • new(Type) 函数接受一个类型作为参数, 返回一个指向该类型内存地址的指针.
    • new 函数返回的指针所指向的值是该类型的零值.
    • 示例代码展示了 new 函数的使用, 以及如何对指针进行初始化和赋值.
  8. make 函数:

    • make 函数用于为 slice, mapchan 这三种引用类型创建内存.
    • make 函数返回的是这三种类型本身, 而不是它们的指针类型.
    • make 函数是不可替代的, 在使用 slice, mapchannel 时必须使用 make 进行初始化.
    • 示例代码展示了 make 函数如何初始化 map 并进行赋值.
  9. new 与 make 的区别:

    • 两者都用于内存分配.
    • make 只用于 slice, mapchannel 的初始化, 返回的是引用类型本身.
    • new 用于类型的内存分配, 返回的是指向类型零值的指针.

map

在 Go 语言中, map 是一种无序的键值对数据结构, 类似于其他语言中的字典或哈希表. 它是一个引用类型, 这意味着在声明 map 变量后, 必须使用 make() 函数进行初始化才能使用.

以下是关于 Go 语言 map 的详细解释:

1. map 的定义

map 的定义语法如下:

map[KeyType]ValueType
  • KeyType: 表示键的类型, 可以是任何可比较的类型(如整型, 浮点型, 字符串, 数组, 结构体等).
  • ValueType: 表示键对应的值的类型, 可以是任何类型.

2. map 的初始化

map 类型的变量默认初始值为 nil, 需要使用 make() 函数来分配内存并初始化. 语法为:

make(map[KeyType]ValueType, [cap])
  • cap: 表示 map 的容量. 这是一个可选参数, 但建议在初始化 map 时指定一个合适的容量, 以优化性能.

示例:

scoreMap := make(map[string]int, 8) // 初始化一个键为string, 值为int, 容量为8的map

map 也支持在声明时直接填充元素:

userInfo := map[string]string{
    "username": "pprof.cn",
    "password": "123456",
}

3. map 的基本使用

  • 添加/修改元素:

    scoreMap["张三"] = 90 // 添加或修改键为"张三"的元素, 值为90
    
  • 获取元素:

    fmt.Println(scoreMap["小明"]) // 获取键为"小明"的值
    
  • 判断键是否存在:

    Go 语言提供了一种特殊的语法来判断 map 中键是否存在:

    value, ok := map[key]
    
    • value: 如果键存在, value 为对应的值; 如果键不存在, value 为值类型的零值.
    • ok: 一个布尔值, 如果键存在则为 true, 否则为 false.

    示例:

    v, ok := scoreMap["张三"]
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("查无此人")
    }
    

4. map 的遍历

使用 for range 遍历 map:

for k, v := range scoreMap {
    fmt.Println(k, v) // 遍历键和值
}

for k := range scoreMap {
    fmt.Println(k) // 只遍历键
}

**注意: ** 遍历 map 时的元素顺序与添加键值对的顺序无关, 因为 map 是无序的.

5. 删除键值对

使用 delete() 内建函数从 map 中删除一组键值对:

delete(map, key)
  • map: 要删除键值对的 map.
  • key: 要删除的键值对的键.

示例:

delete(scoreMap, "小明") // 删除键为"小明"的键值对

6. 按照指定顺序遍历 map

由于 map 是无序的, 如果需要按照特定顺序遍历 map, 可以先将 map 的所有键取出放入一个切片中, 然后对切片进行排序, 最后按照排序后的键遍历 map.

示例:

// ... (生成 scoreMap 的代码)

var keys = make([]string, 0, 200)
for key := range scoreMap {
    keys = append(keys, key)
}
sort.Strings(keys) // 对切片进行排序

for _, key := range keys {
    fmt.Println(key, scoreMap[key]) // 按照排序后的键遍历 map
}

7. 元素为 map 类型的切片

可以创建一个切片, 其元素类型为 map. 在使用前需要对切片中的每个 map 元素进行初始化.

var mapSlice = make([]map[string]string, 3) // 创建一个包含3个map的切片
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "王五"
// ...

8. 值为切片类型的 map

可以创建一个 map, 其值类型为切片.

var sliceMap = make(map[string][]string, 3) // 创建一个值为切片的map
key := "中国"
value, ok := sliceMap[key]
if !ok {
    value = make([]string, 0, 2) // 如果键不存在, 初始化一个切片
}
value = append(value, "北京", "上海")
sliceMap[key] = value

结构体

Go语言中的结构体(struct)是一种自定义数据类型, 它允许你将多个不同类型的数据组合成一个单一的复合类型. 这与面向对象编程中的"类"概念相似, 但Go语言本身不直接支持"类"的继承等面向对象特性, 而是通过结构体的内嵌和接口来实现更高的扩展性和灵活性.

以下是该文档中关于Go语言结构体的核心概念:

1. 类型别名和自定义类型

  • 自定义类型: 使用 type 关键字可以定义一个全新的类型, 它基于已有的基本类型(如 int, string 等)或通过 struct 定义.
    type MyInt int // MyInt 是一个全新的类型, 具有 int 的特性
    
  • 类型别名: Go 1.9 引入的新功能, 使用 type TypeAlias = Type 定义. 类型别名只是原有类型的一个别名, 本质上两者是同一个类型. 例如 byte = uint8rune = int32.
  • 区别: 自定义类型会创建一个新的类型, 而类型别名只是给现有类型起一个新名字. 编译后, 类型别名不会作为独立类型存在.

2. 结构体的定义

  • 使用 typestruct 关键字定义结构体:
    type TypeName struct {
        FieldName1 FieldType1
        FieldName2 FieldType2
        // ...
    }
    
    • TypeName: 结构体的名称, 在一个包内必须唯一.
    • FieldName: 结构体字段的名称, 在一个结构体中必须唯一.
    • FieldType: 结构体字段的具体类型.
  • 示例:
    type Person struct {
        name string
        city string
        age  int8
    }
    
    相同类型的字段也可以写在一行:
    type Person1 struct {
        name, city string
        age        int8
    }
    
  • 结构体是一种聚合型数据类型, 用于描述一组值(如一个人的姓名, 年龄, 城市).

3. 结构体的实例化

  • 结构体只有在实例化后才会分配内存并能使用其字段.
  • 基本实例化:
    var p1 Person
    p1.name = "pprof.cn"
    p1.city = "北京"
    p1.age = 18
    
    通过 . 访问结构体的字段.
  • 匿名结构体: 用于定义临时数据结构, 不需要单独命名.
    var user struct{ Name string; Age int }
    user.Name = "pprof.cn"
    user.Age = 18
    
  • 创建指针类型结构体: 使用 new 关键字或 & 操作符获取结构体的地址.
    var p2 = new(Person) // p2 是一个 *Person 类型的指针
    p3 := &Person{}      // p3 也是一个 *Person 类型的指针
    
    Go语言支持直接通过结构体指针使用 . 访问成员 (如 p2.name), 这是一种语法糖, 底层会自动解引用.

4. 结构体初始化

  • 默认零值初始化:
    var p4 Person // p4 的字段会被初始化为其类型的零值(string 为 "", int8 为 0)
    
  • 使用键值对初始化:
    p5 := Person{
        name: "pprof.cn",
        city: "北京",
        age:  18,
    }
    // 也可以对结构体指针进行键值对初始化
    p6 := &Person{
        name: "pprof.cn",
        city: "北京",
        age:  18,
    }
    // 可以只初始化部分字段, 未初始化的字段会是零值
    p7 := &Person{
        city: "北京",
    }
    
  • 使用值的列表初始化 (简写):
    p8 := &Person{
        "pprof.cn",
        "北京",
        18,
    }
    
    • 必须初始化所有字段.
    • 初始值顺序必须与字段声明顺序一致.
    • 不能与键值对初始化混用.

5. 结构体内存布局

  • 结构体字段在内存中是连续存储的.

6. 构造函数

  • Go语言没有内置的构造函数, 但可以自定义函数来返回结构体实例或指针. 通常返回结构体指针以避免大数据结构的值拷贝开销.
    func newPerson(name, city string, age int8) *Person {
        return &Person{
            name: name,
            city: city,
            age:  age,
        }
    }
    

7. 方法和接收者

  • 方法 (Method): 作用于特定类型变量的函数, 这个特定类型变量称为接收者 (Receiver).
  • 定义格式:
    func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
        // 函数体
    }
    
    • 接收者变量名通常使用接收者类型名的第一个小写字母.
    • 接收者类型可以是值类型或指针类型.
  • 指针类型的接收者:
    • 可以修改接收者本身的成员变量.
    • 类似于其他语言中的 thisself.
      func (p *Person) SetAge(newAge int8) {
          p.age = newAge // 修改 p 的 age 字段
      }
      
  • 值类型的接收者:
    • 方法内部对接收者成员的修改只作用于副本, 不会影响原始变量.
      func (p Person) SetAge2(newAge int8) {
          p.age = newAge // 只修改了副本的 age 字段
      }
      
  • 何时使用指针类型接收者:
    1. 需要修改接收者中的值.
    2. 接收者是较大的对象, 值拷贝开销大.
    3. 为保持一致性, 如果某个方法使用指针接收者, 其他方法也应如此.
  • 任意类型添加方法: 除了结构体, 任何自定义类型都可以拥有方法.
    type MyInt int
    func (m MyInt) SayHello() {
        fmt.Println("Hello, 我是一个int. ")
    }
    
    • 注意: 不能给其他包的类型定义方法.

8. 结构体的匿名字段

  • 结构体成员字段可以只有类型而没有字段名.
    type Person struct {
        string // 匿名字段, 字段名默认为 string
        int    // 匿名字段, 字段名默认为 int
    }
    
    匿名字段默认使用类型名作为字段名, 因此一个结构体中同种类型的匿名字段只能有一个.

9. 嵌套结构体

  • 一个结构体可以包含另一个结构体或结构体指针作为其字段.
    type Address struct {
        Province string
        City     string
    }
    type User struct {
        Name    string
        Gender  string
        Address Address // 嵌套 Address 结构体
    }
    
  • 嵌套匿名结构体: 可以直接访问匿名结构体的字段.
    type User struct {
        Name    string
        Gender  string
        Address // 嵌套匿名 Address 结构体
    }
    // 访问方式: user2.Address.Province 或 user2.City (直接访问匿名结构体的字段)
    
    当访问结构体成员时, 会优先在当前结构体中查找, 找不到再去匿名结构体中查找.
  • 字段名冲突: 当嵌套结构体有相同字段名时, 需要明确指定内嵌结构体的字段以避免歧义.
    user3.Address.CreateTime = "2000"
    user3.Email.CreateTime = "2000"
    
  • 结构体的"继承": Go语言通过嵌套匿名结构体实现类似其他语言的继承特性.
    type Animal struct {
        name string
    }
    func (a *Animal) move() { /* ... */ }
    
    type Dog struct {
        Feet    int8
        *Animal // 嵌套匿名结构体指针实现"继承"
    }
    func (d *Dog) wang() { /* ... */ }
    
    这样 Dog 类型就拥有了 Animalmove 方法.
  • 内存管理和共享::

    • 当嵌套的是值类型结构体时 (Address Address), 每次创建外部结构体 (Person) 的实例, 都会为内部结构体 (Address) 拷贝一份新的数据.
    • 当嵌套的是指针类型结构体时 (Address *Address), 外部结构体 (PersonWithAddressPtr) 只存储一个指向内部结构体 (Address) 的内存地址. 这意味着多个外部结构体实例可以共享同一个内部结构体实例.

10. 结构体字段的可见性

  • 字段名首字母大写表示可公开访问(导出).
  • 字段名首字母小写表示私有(仅在定义当前结构体的包中可访问).

11. 结构体与 JSON 序列化

  • Go语言的 json 包可以将结构体与 JSON 格式的字符串进行相互转换.
  • json.Marshal: 结构体 -> JSON 字符串
  • json.Unmarshal: JSON 字符串 -> 结构体

12. 结构体标签 (Tag)

  • Tag 是结构体的元信息, 通过反射机制在运行时读取.
  • 定义格式: `key1:"value1" key2:"value2"`
  • 键值对之间用空格分隔, 键与值用冒号分隔, 值用双引号括起来.
  • 常用于 JSON 序列化时指定字段的键名, 例如:
    type Student struct {
        ID     int    `json:"id"` // JSON 序列化时使用 "id" 作为键
        Gender string // 默认使用字段名作为键
        name   string // 私有字段, 不会被 json 包访问
    }