王清欢Randy 王清欢Randy
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
  • Golang基础

    • 数据类型

      • 变量与常量
      • 基础数据类型之值类型
      • 基础数据类型之引用类型
        • 01 Slice 切片
          • 1.1 创建切片
          • 1.2 操作切片
          • 1.2.1 切片追加
          • 1.2.2 拷贝切片
          • 1.2.3 遍历切片
          • 1.2.4 切片删除
          • 1.3 切片扩容
        • 02 Map 键值对
          • 2.1 创建 Map
          • 2.2 操作 Map
          • 2.2.1 Map 取值
          • 2.2.2 遍历 Map
          • 2.2.3 Map 删除
          • 2.3 Map 实现原理
        • 03 Channel 通道
        • 参考资料
    • 流程控制

      • 条件判断
      • 循环控制
    • 函数

      • 函数基础
      • 匿名函数与闭包
      • 延迟调用
  • Golang学习笔记
  • Golang基础
  • 数据类型
王清欢
2023-03-24
目录

基础数据类型之引用类型

# 基础数据类型 引用类型

引用类型的在内存栈空间中存储的是保存变量名和指向堆空间中的变量地址,地址指向的堆空间中保存着实际的值。在赋值或拷贝变量时,栈空间中保存的地址也被拷贝指向相同的堆空间中保存的值,所以在修改其中一个变量的值时,其他的变量会一起被修改。

类型 默认值 说明
slice nil 引用类型
map nil 引用类型
channel nil 引用类型

# 01 Slice 切片

切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型,类似于 C/C++ 中的数组类型,而Golang 中的数组是值类型 (opens new window),赋值和函数传参操作都会复制整个数组数据。

相较于 Golang 数组拷贝过程中的巨大内存开销,采用切片的方式进行赋值或者传参不需要使用额外的内存并且比使用数组更有效率。

切片的实现原理类似 C++ STL 中的 vector,但是切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

type slice struct {
    array unsafe.Pointer // 保存指向堆空间的地址
    len   int            // 包含元素的个数(实际被使用容量)长度
    cap   int            // 切片的实际容量(使用部分+未使用部分)通常大于等于len
}

# 1.1 创建切片

声明切片的方式与数组声明类似,但是不在[]中指明长度,数组是固定长度的,而切片的长度可变的。

// 数组声明
var arr1 [3]int
var arr2 [...]int{1,2,3}
arr3 := [3]int{1,2,3} // 短变量声明
// 切片声明
var s1 []int // 空切片
s1 := []int{1,2,3} // 短变量声明

make函数初始化切片:Golang 的 make 内置函数用于分配内存空间,返回引用类型本身。make 函数有三个入参分别是:数据类型(*_type),长度(len)和容量(cap),如果容量被省略则与长度同值。

// 声明形式
var slice []type = make([]type, len)
slice  := make([]type, len)
slice  := make([]type, len, cap)

// example:
func SliceCreate() {
    var s1 []int = make([]int, 3, 5)
    fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    var s2 []int = make([]int, 3)
    fmt.Println("s2:", s2, " len / cap:", len(s2), "/", cap(s2))
}

/* output:
s1: [0 0 0]  len / cap: 3 / 5
s2: [0 0 0]  len / cap: 3 / 3
*/

空切片初始化:nil切片表示该切片结构体的指针指向nil,表示切片不存在,常用于函数异常返回值。空切片的指针指向具体地址但该地址没有存放任何元素,空切片一般会用来表示一个空的集合。

func SliceCreate() {
    var s3 []int // nil切片
    if s3 == nil {
        fmt.Println("s3 is empty")
    }
    s3 = []int{1, 2, 3}
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
    // 空切片
    s4 := make([]int, 0)
    fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    s4 = []int{}
    fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    s4 = []int{1, 2, 3, 4}
    fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
}

/* output:
s3 is empty
s3: [1 2 3]  len / cap: 3 / 3
s4: []  len / cap: 0 / 0
s4: []  len / cap: 0 / 0
s4: [1 2 3 4]  len / cap: 4 / 4
*/

使用字面量从数组中切片:使用切片引用数组连续的全部或部分数据,使用字面量(索引号)获取数组的部分数据,字面量操作含义如下表所示

操作 含义
arr[n] 索引号为n的单个元素
arr[:] 从索引位置0到len(arr)-1中所获得的切片即数组的所有元素
arr[low:] 从索引位置low到len(arr)-1中所获得的切片,长度为len(arr)-low,容量为len(arr)
arr[:high] 从索引位置0到high-1中所获得的切片,长度为high,容量为len(arr)
arr[low:high] 从索引位置low到high-1中所获得的切片,长度为high-low,容量为len(arr)
arr[low:high:max] 从索引位置low到high-1中所获得的切片,长度为high-low,容量为max-low
func SliceCreate() {
    var arr = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
    s5 := arr[1:5:6]
    fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
    s6 := arr[1:5]
    fmt.Println("s6:", s6, " len / cap:", len(s6), "/", cap(s6))
}

/* output:
s5: [2 3 4 5]  len / cap: 4 / 5
s6: [2 3 4 5]  len / cap: 4 / 9
*/ 

# 1.2 操作切片

# 1.2.1 切片追加

append()内置函数添加元素:切片使用append()内置函数向该切片末尾追加元素,并返回新的切片。

func SliceAppend() {
    s1 := make([]int, 1)
    fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    s2 := append(s1, 1)
    fmt.Println("s2:", s2, " len / cap:", len(s2), "/", cap(s2))
    fmt.Printf("pos s1: %p; pos s2: %p\n", &s1, &s2)
}

/* output:
s1: [0]  len / cap: 1 / 1
s2: [0 1]  len / cap: 2 / 2
pos s1: 0xc0000040d8; pos s2: 0xc000004108
*/

向切片中追加多个元素:

  1. 切片追加多个元素,可以通过多次调用append(),也可以添加多个入参
func SliceAppend() {
    s3 := []int{1, 2, 3}
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
    s3 = append(s3, 4)
    s3 = append(s3, 5)
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
    s3 = append(s3, 6, 7, 8)
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
}

/* output:
s3: [1 2 3]  len / cap: 3 / 3
s3: [1 2 3 4 5]  len / cap: 5 / 6
s3: [1 2 3 4 5 6 7 8]  len / cap: 8 / 12
*/
  1. 使用append()将切片作为追加元素,使用...运算符将切片值拆分成单个追加元素
func SliceAppend() {
    s4 := []int{1, 2, 3}
    fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    s5 := []int{4, 5}
    fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
    s6 := append(s4, s5...)
    fmt.Println("s6:", s6, " len / cap:", len(s6), "/", cap(s6))
}

/* output:
s4: [1 2 3]  len / cap: 3 / 3
s5: [4 5]  len / cap: 2 / 2
s6: [1 2 3 4 5]  len / cap: 5 / 6
*/

# 1.2.2 拷贝切片

copy()内置函数拷贝切片:使用 copy 内置函数拷贝切片时,是将切片的数据拷贝到另外新开辟的内存空间中;copy 内置函数的参数和返回值为copy( dest Slice, src Slice []T) int,其中第一个参数为拷贝的目标切片,第二个参数是拷贝的对象即数据源,返回值表示的是根据两个切片长度len的较小值实际成功拷贝的元素个数。

func SliceCopy() {
    s1 := make([]int, 3, 5)
    fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    s2 := []int{1, 2}
    fmt.Println("s2:", s2, " len / cap:", len(s2), "/", cap(s2))
    copy(s1, s2)
    fmt.Println("copy s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    fmt.Println("copy s2:", s2, " len / cap:", len(s2), "/", cap(s2))
    s3 := []int{3}
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
    s1 = append(s1, s3...)
    fmt.Println("append s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    copy(s3, s2)
    fmt.Println("copy s3:", s3, " len / cap:", len(s3), "/", cap(s3))
    fmt.Println("copy s2:", s2, " len / cap:", len(s2), "/", cap(s2))
}

/* output:
s1: [0 0 0]  len / cap: 3 / 5
s2: [1 2]  len / cap: 2 / 2
copy s1: [1 2 0]  len / cap: 3 / 5
copy s2: [1 2]  len / cap: 2 / 2
s3: [3]  len / cap: 1 / 1
append s1: [1 2 0 3]  len / cap: 4 / 5
copy s3: [1]  len / cap: 1 / 1
copy s2: [1 2]  len / cap: 2 / 2
*/

=赋值运算符浅拷贝:Golang 中有了 Array 数组还提出 Slice 切片的一个重要动机就是当数组保存数据规模过大时,避免全部重新复制一遍数组元素,而使用指向存储实际数据空间的指针高效利用内存。使用=拷贝切片时,两者引用同一个内存空间,当修改其中一个时,两者同时被修改。

func SliceCopy() {
    s4 := []int{1, 2, 3}
    s5 := []int{}
    s6 := make([]int, 3)
    fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
    fmt.Println("s6:", s6, " len / cap:", len(s6), "/", cap(s6))
    s5 = s4
    copy(s6, s4)
    fmt.Println("copy s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    fmt.Println("copy s5:", s5, " len / cap:", len(s5), "/", cap(s5))
    fmt.Println("copy s6:", s6, " len / cap:", len(s6), "/", cap(s6))
    s4[0] = 9
    fmt.Println("modify s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    fmt.Println("modify s5:", s5, " len / cap:", len(s5), "/", cap(s5))
    fmt.Println("modify s6:", s6, " len / cap:", len(s6), "/", cap(s6))
}

/* output:
s4: [1 2 3]  len / cap: 3 / 3
s5: []  len / cap: 0 / 0
s6: [0 0 0]  len / cap: 3 / 3
copy s4: [1 2 3]  len / cap: 3 / 3
copy s5: [1 2 3]  len / cap: 3 / 3
copy s6: [1 2 3]  len / cap: 3 / 3
modify s4: [9 2 3]  len / cap: 3 / 3
modify s5: [9 2 3]  len / cap: 3 / 3
modify s6: [1 2 3]  len / cap: 3 / 3
*/

# 1.2.3 遍历切片

for i/for range遍历切片:和遍历数组一样可以使用标准索引法遍历切片,使用len()内置函数获取数组长度,然后使用[]中括号运算符获取索引元素;也可以使用for range这种更加便捷的遍历方式遍历引用。

func SliceTraversal() {
    s1 := []int{6, 5, 4, 3, 2, 1}
    for i := 0; i < len(s1); i++ {
        fmt.Printf("index: %d, value: %d\n", i, s1[i])
    }
    fmt.Println("--------------")
    for index, value := range s1 {
        fmt.Printf("index: %d, value: %d\n", index, value)
    }
}

/* output:
index: 0, value: 6
index: 1, value: 5
index: 2, value: 4
index: 3, value: 3
index: 4, value: 2
index: 5, value: 1
--------------
index: 0, value: 6
index: 1, value: 5
index: 2, value: 4
index: 3, value: 3
index: 4, value: 2
index: 5, value: 1
*/

# 1.2.4 切片删除

Golang 中切片元素的删除过程并没有提供任何的语法糖或者方法封装,删除元素需要以被删除元素为分界点,将前后两个部分的内存重新连接起来。但这种方法在切片数据规模较大时非常低效。

func SliceDelete() {
    s1 := []int{0, 1, 2, 3, 4, 5}
    index := 3
    fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    fmt.Println("before: ", s1[:index], "after: ", s1[index+1:])
    s1 = append(s1[:index], s1[index+1:]...)
    fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
}

/* output:
s1: [0 1 2 3 4 5]  len / cap: 6 / 6
before:  [0 1 2] after:  [4 5]
s1: [0 1 2 4 5]  len / cap: 5 / 6
*/

# 1.3 切片扩容

前面介绍到 Golang 切片的实现原理类似 C++ STL 中的 vector,切片中也有类似于 vector 中动态扩容的智能动作。

当切片使用append()内置函数追加元素时,如果当前切片容量cap被使用完时,就需要重新开辟一块新的内存空间,然后把原数据拷贝到该新空间中,并把指向原地址空间的切片指针重定向到新空间,最后释放掉原存储数据的空间。

Go 中切片扩容的策略:按照 2 倍或者 1.5 倍扩大原切片的容量cap (注意不是长度len)。

  • 如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量,即每次增加原来容量的一倍

  • 一旦元素个数超过 1024 个元素,那么增长因子就变成 1.5 ,即每次增加原来容量的四分之一

func SliceExpend() {
    s1 := make([]int, 3, 5)
    fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    s1 = append(s1, 4, 5, 6)
    fmt.Println("s1:", s1, " len / cap:", len(s1), "/", cap(s1))
    s2 := make([]int, 1023, 1024)
    fmt.Println("ori s2:", " len / cap:", len(s2), "/", cap(s2))
    s2 = append(s2, 1024, 1025)
    fmt.Println("exp s2:", " len / cap:", len(s2), "/", cap(s2))
}

/* output:
s1: [0 0 0]  len / cap: 3 / 5
s1: [0 0 0 4 5 6]  len / cap: 6 / 10
ori s2:  len / cap: 1023 / 1024
exp s2:  len / cap: 1025 / 1536
*/

不触发切片自动扩容的情况:Golang 切片扩容机制中要注意如果切片创建是通过字面量对 Array 数组的截取,要注意明确第三个参数 cap 值,当 cap 并不等于指向数组的总容量时且切片长度小于容量时,不会触发自动扩容,导致切片指针指向的就是原 Array 数组,当数组元素发生改变时也会影响切片。

所以用字面量创建切片的时候,cap 的值一定要明确,避免共享原数组导致的 bug。

func SliceExpend() {
    arr := [5]int{1, 2, 3, 4, 5}
    s3 := arr[:3]
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
    s3 = append(s3, 4)
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
    arr[0] = 9
    fmt.Println("s3:", s3, " len / cap:", len(s3), "/", cap(s3))
}

/* output:
s3: [1 2 3]  len / cap: 3 / 5
s3: [1 2 3 4]  len / cap: 4 / 5
s3: [9 2 3 4]  len / cap: 4 / 5 
*/

切片扩容导致的索引失效:当切片作为函数参数时,如果在函数内部发生了扩容,这时再修改切片中的值不会生效,因为修改发生在新开辟的内存空间中,对原先的数据没有任何影响。

func SliceExpend() {
    s4 := make([]int, 3, 4)
    fmt.Println("s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    appendToExpend(s4)
    fmt.Println("append s4:", s4, " len / cap:", len(s4), "/", cap(s4))
    s5 := []int{1, 2, 3}
    fmt.Println("s5:", s5, " len / cap:", len(s5), "/", cap(s5))
    appendToExpend(s5)
    fmt.Println("append s5:", s5, " len / cap:", len(s5), "/", cap(s5))
}

func appendToExpend(s []int) {
    s = append(s, 4)
    s[0] = 9
}

/* output:
s4: [0 0 0]  len / cap: 3 / 4
append s4: [9 0 0]  len / cap: 3 / 4
s5: [1 2 3]  len / cap: 3 / 3
append s5: [1 2 3]  len / cap: 3 / 3
*/

# 02 Map 键值对

键值对(Map)是 Golang 中提供映射关系容器,底层使用哈希表(散列表)实现。

不同于 C++ 标准模板库 STL 关联容器 (opens new window) 提供了基于红黑树实现的有序键值对集合 Map 和基于哈希表实现的无序键值对集合 unordered_map 两种关联容器,Goalng 中提供了无序的 Map。

键值对 Map 就是通过 键(Key)获取值(Value)的一种蕴含映射关系的数据结构,其底层最为简单的存储方式就是线性表,用Hash函数计算存储位置即索引,然后将数据值存入该位置。

# 2.1 创建 Map

不同于切片使用不带长度的[]元素类型的方式声明,Golang为键值对保留了关键字map用于声明键值对集合 Map,其声明格式为var mapName map[KeyType]ValueType,其中 mapName是键值对集合的名称,KeyType为键类型;ValueType是键对应的值类型。

Map和切片一样,也是引用类型,声明之后未进行初始化时指向nil,所以在使用Map时声明后需要初始化,可以使用make内置函数初始化,也可以不是用make函数在声明是就填充元素完成初始化。

func MapCreate() {
    var m1 map[string]int
    if m1 == nil {
        fmt.Println("m1 is empty!")
    }
    var m2 = make(map[string]int)
    m2["one"] = 1
    m2["two"] = 2
    fmt.Println("m2: ", m2, " len: ", len(m2))
    m3 := map[string]int{
        "one":   1,
        "two":   2,
        "three": 3,
    }
    fmt.Println("m3: ", m3, " len: ", len(m3))
}

/* output:
m1 is empty!
m2:  map[one:1 two:2]  len:  2
m3:  map[one:1 three:3 two:2]  len:  3
*/

# 2.2 操作 Map

# 2.2.1 Map 取值

map 可以直接根据键进行取值mapName[key],如果查询的键值对不存也不会报错,返回结果是空值。

func MapGet() {
    m1 := map[string]int{
        "one": 1,
        "two": 2,
    }
    fmt.Println("exist: ", m1["one"], " don't exist: ", m1["three"])
}

/* output:
exist:  1  don't exist:  0
*/

取值判断:Golang 中也提供了判断某个键是否存在的机制,使用比较特殊的写法value, ok := mapName[key],在默认获取键值的基础上,多取了一个变量ok,可以判断键是否存在于map中。

func MapGet() {
    m1 := map[string]int{
        "one": 1,
        "two": 2,
    }
    fmt.Println("m1: ", m1, " len: ", len(m1))
    val, ok := m1["one"]
    if ok {
        fmt.Println("exist: ", val)
    } else {
        fmt.Println("don't exist")
    }
}

/* output:
m1:  map[one:1 two:2]  len:  2
exist:  1
*/

# 2.2.2 遍历 Map

一般使用  for range 遍历 Map,遍历map时的元素顺序与添加键值对的顺序无关。

func MapTraversal() {
    m1 := map[string]int{
        "one":   1,
        "two":   2,
        "three": 3,
    }
    fmt.Println("m1: ", m1, " len: ", len(m1))
    for k, v := range m1 {
        fmt.Println("key: ", k, " val: ", v)
    }
}

/* output:
m1:  map[one:1 three:3 two:2]  len:  3
key:  one  val:  1
key:  two  val:  2
key:  three  val:  3
*/

如果仅遍历Map的键或者值,在for range参数中省略或者用匿名遍历忽略其中一个即可。

func MapTraversal() {
    m1 := map[string]int{
        "one":   1,
        "two":   2,
        "three": 3,
    }
    fmt.Println("m1: ", m1, " len: ", len(m1))
    // 只取键
    for k := range m1 {
        fmt.Println("key: ", k)
    }
    // 只取
    for _, v := range m1 {
        fmt.Println(" val: ", v)
    }
}


/* output:
m1:  map[one:1 three:3 two:2]  len:  3
key:  one  val:  1
key:  two  val:  2
key:  three  val:  3
key:  one
key:  two
key:  three
 val:  1
 val:  2
 val:  3
*/

按指定顺序遍历Map:由于 Golang 中 Map 底层实现是 Hash 表,所以通常遍历结果是无序的,如果要按照指定顺序遍历 Map,则需要额外进行排序等操作实现。

func MapTraversal() {
    m2 := map[string]int{
        "stu-2": 99,
        "stu-4": 79,
        "stu-3": 93,
        "stu-1": 63,
    }
    keys := make([]string, 0, 4)
    for k := range m2 {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    fmt.Println("sort print:")
    for _, key := range keys {
        fmt.Println(key, m2[key])
    }
}

/* output:
sort print:
stu-1 63
stu-2 99
stu-3 93
stu-4 79
*/

# 2.2.3 Map 删除

Map 中如果需要删除某个键值对可以直接使用内置函数delete()进行操作,使用格式为delete(mapName,key),其中 mapName 是要删除的 map 实例名称,key 则为要删除的 map 键值对中的键。

func MapDelete() {
    m1 := map[string]int{
        "one": 1,
        "xyz": 100,
        "two": 2,
    }
    fmt.Println("m1: ", m1, " len: ", len(m1))
    delete(m1, "xyz")
    fmt.Println("m1: ", m1, " len: ", len(m1))
}

/* output:
m1:  map[one:1 two:2 xyz:100]  len:  3
m1:  map[one:1 two:2]  len:  2
*/

# 2.3 Map 实现原理

Golang Map实现原理 - 简书 (opens new window)

# 03 Channel 通道

待并发编程更新.......

# 参考资料

切片Slice · Go语言中文文档 (opens new window)

Go slice切片详解和实战 - 掘金 (opens new window)

golang基础-深入理解 slice - 掘金 (opens new window)

Map · Go语言中文文档 (opens new window)

Go map详解和实战 - 掘金 (opens new window)

Golang Map实现原理 - 简书 (opens new window)

golang 系列:channel 全面解析 (opens new window)

上次更新: 2023/11/19, 12:55:48
基础数据类型之值类型
条件判断

← 基础数据类型之值类型 条件判断→

Theme by Vdoing | Copyright © 2023-2024 Wang Qinghuan | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式