Golang学习-CH3 Go语言容器

3.1 Go语言数组

数组是一个由固定长度的特定类型元素组成的序列。和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列。

Go语言数组的声明:

var 数组变量名 [元素数量]Type
var a [3]int             // 定义三个整数的数组
var q [3]int = [3]int{1, 2, 3}	//字面初始化
var r [3]int = [3]int{1, 2}	//初始全为0,1 2 0
q := [...]int{1, 2, 3}  //省略号代表数组长度根据初始化值个数计算
fmt.Printf("%T\n", q) // "[3]int"
  • Go中数组可以进行赋值,但是需要是同类型的数组。[3]int 和 [4]int 是两种不同的数组类型
  • 同样也可以进行判断数组是否相等或者不等,==!=。判断的标准是所有元素是否对应相等,前提仍是类型相同
  • 遍历数组
for k,v := range team{
    fmt.Println(k,v)
}

3.2 Go语言多维数组

var array_name [size1][size2]...[sizen] array_type
//示例
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}

3.3 Go语言切片

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型。Go语言中切片的内部结构包含地址、大小和容量。

从数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2])  //左闭右开
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片。

var name []Type  //中括号内为空

实际体感上:从数组中生成切片更像Python中的切片应用,后者的新切片更像C++中vector的作用

实际示例:

// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)		//true
fmt.Println(numList == nil)	//true
fmt.Println(numListEmpty == nil)	//true

切片是动态结构,只能与 nil 判定相等,不能互相判定相等。

使用make函数构造切片

make( []Type, size, cap )	//切片类型,分配元素数,预分配数量(容量)

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

3.4 Go语言append()为切片添加元素

Go语言的内建函数 append() 可以为切片动态添加元素。

不过需要注意的是,在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充

示例:

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

因为 append 函数返回新切片(临时切片)的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片

3.5 Go语言copy():切片复制

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中。copy() 函数的使用格式如下:

copy( destSlice, srcSlice []T) int //返回值为实际发生复制的个数

目标切片必须分配过空间且足够承载复制的元素个数(不会自动扩容),并且来源和目标的类型必须一致(切片),copy() 函数的返回值表示实际发生复制的元素个数。

复制和引用的不同影响:对于引用而言,本质上还是同一块内存空间,动一发而牵全身

func main() {
    // 设置元素数量为1000
    const elementCount = 1000
    // 预分配足够多的元素切片
    srcData := make([]int, elementCount)
    // 将切片赋值
    for i := 0; i < elementCount; i++ {
        srcData[i] = i
    }
    // 引用切片数据
    refData := srcData
    // 预分配足够多的元素切片
    copyData := make([]int, elementCount)
    // 将数据复制到新的切片空间中
    copy(copyData, srcData)
    // 修改原始数据的第一个元素
    srcData[0] = 999
    // 打印引用切片的第一个元素
    fmt.Println(refData[0])
    // 打印复制切片的第一个和最后一个元素
    fmt.Println(copyData[0], copyData[elementCount-1])
}

3.6 Go语言从切片中删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况:从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

从开头位置删除:

  • 直接移动数据指针
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
  • 不移动数组指针,后续数据前移,append原地完成
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
  • 使用copy函数:copy对a产生了影响,再切片
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

从中间位置删除:

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

从尾部删除:

a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

当需要大量进行中间位置删除操作时,可以考虑使用双链表等其他容器。

3.7 Go语言range关键字:循环迭代切片

Go语言有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的每一个元素:

// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每一个元素,并显示其值
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}
  • 实际上range返回的返回的是每个元素的副本,而不是直接返回对该元素的引用
  • 关键字 range 总是会从切片头部开始迭代。如果想对迭代做更多的控制,则可以使用传统的 for 循环
  • range 关键字不仅仅可以用来遍历切片,它还可以用来遍历数组、字符串、map 或者通道等

3.8 Go语言多维切片

示例

//声明一个二维切片
var slice [][]int
//为二维切片赋值
slice = [][]int{{10}, {100, 200}}

// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}

// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)

3.9 Go语言map(映射)

Go语言中 map 是一种特殊的数据结构,一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构,给定 key,就可以迅速找到对应的 value。

值类型与引用类型

map是引用类型

https://www.cnblogs.com/aresxin/p/GO-zhi-lei-xing-yu-yin-yong-lei-xing.html

map定义和使用

var mapname map[keytype]valuetype

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。

func main() {
    var mapLit map[string]int
    var mapAssigned map[string]int
    mapLit = map[string]int{"one": 1, "two": 2}
    mapCreated := make(map[string]float32)
    mapAssigned = mapLit
    mapCreated["key1"] = 4.5
    mapCreated["key2"] = 3.14159
    mapAssigned["two"] = 3
    fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])
    fmt.Printf("Map created at \"key2\" is: %f\n", mapCreated["key2"])
    fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"])
    fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"])
}
  • mapAssigned 是 mapList 的引用,对 mapAssigned 的修改也会影响到 mapLit 的值。
  • 可以使用 make(),但不能使用 new() 来构造 map,如果错误的使用 new() 分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址
  • map容量:map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制。当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
  • 使用切片作为map的值,一个key对应多个value
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

3.10 Go语言遍历map

map 的遍历过程使用 for range 循环完成,遍历时,可以同时获得键和值。

  • 如果只遍历值:使用匿名变量
  • 如果只遍历键:使用匿名遍历或者忽略值
for k, v := range scene {
    fmt.Println(k, v)
}
for _, v := range scene {
} 
for k := range scene {
}

注意:map无序性,遍历输出元素的顺序与填充顺序无关,不能期望 map 在遍历时返回某种期望顺序的结果。如果需要特定顺序的遍历结果,正确的做法是先排序

scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
// 声明一个切片保存map数据
var sceneList []string
// 将map数据遍历复制到切片中
for k := range scene {
    sceneList = append(sceneList, k)
}
// 对切片进行排序
sort.Strings(sceneList)
// 输出
fmt.Println(sceneList)

3.11 Go语言map元素的删除和清空

Go语言提供了一个内置函数 delete(),用于删除容器内的元素。

删除特定的键值对:

scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
delete(scene, "brazil")
for k, v := range scene {
    fmt.Println(k, v)
}

清空map元素:

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

3.12 Go语言map的多键索引——多个数值条件可以同时查询

https://www.tqwba.com/x_d/jishu/22711.html

以struct结构体作为key

// 查询键
type queryKey struct {
    Name string
    Age  int
}
// 创建查询键到数据的映射
var mapper = make(map[queryKey]*Profile)
// 根据原始数据构建查询索引
func buildIndex(list []*Profile) {
    // 遍历所有数据
    for _, profile := range list {
        // 构建查询键
        key := queryKey{
            Name: profile.Name,
            Age:  profile.Age,
        }
        // 保存查询键
        mapper[key] = profile
    }
}

// 根据条件查询数据
func queryData(name string, age int) {
    // 根据查询条件构建查询键
    key := queryKey{name, age}
    // 根据键值查询数据
    result, ok := mapper[key]
    // 找到数据打印出来
    if ok {
        fmt.Println(result)
    } else {
        fmt.Println("no found")
    }
}

3.13 Go语言sync.Map(在并发环境中使用的map)

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
  • sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量
  • sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。
package main
import (
      "fmt"
      "sync"
)
func main() {
    var scene sync.Map
    // 将键值对保存到sync.Map
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)
    // 从sync.Map中根据键取值
    fmt.Println(scene.Load("london"))
    // 根据键删除对应的键值对
    scene.Delete("london")
    // 遍历所有sync.Map中的键值对
    scene.Range(func(k, v interface{}) bool {
        fmt.Println("iterate:", k, v)
        return true
    })
}

3.14 Go语言list(列表)

列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。

在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。

初始化列表

list 的初始化有两种方法:分别是使用 New() 函数和 var 关键字声明,两种方法的初始化效果都是一致的。

  1. 通过 container/list 包的 New() 函数初始化 list
变量名 := list.New()
  1. 通过 var 关键字声明初始化 list
var 变量名 list.List

列表与切片和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,这既带来了便利,也引来一些问题,例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型将会发生宕机

列表中插入元素

双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFrontPushBack

这两个方法都会返回一个 *list.Element 结构,如果在以后的使用中需要删除插入的元素,则只能通过 *list.Element 配合Remove()方法进行删除。

l := list.New()
l.PushBack("fist")
l.PushFront(67)

列表插入元素方法:

方 法 功 能
InsertAfter(v interface {}, mark * Element) * Element 在 mark 点之后插入元素,mark 点由其他插入函数提供
InsertBefore(v interface {}, mark * Element) *Element 在 mark 点之前插入元素,mark 点由其他插入函数提供
PushBackList(other *List) 添加 other 列表元素到尾部
PushFrontList(other *List) 添加 other 列表元素到头部

列表中删除元素

列表插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。

package main
import "container/list"
func main() {
    l := list.New()
    // 尾部添加
    l.PushBack("canon")
    // 头部添加
    l.PushFront(67)
    // 尾部添加后保存元素句柄
    element := l.PushBack("fist")
    // 在fist之后添加high
    l.InsertAfter("high", element)
    // 在fist之前添加noon
    l.InsertBefore("noon", element)
    // 使用
    l.Remove(element)
}

遍历列表

遍历双链表需要配合 Front() 函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的 Next() 函数

l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
for i := l.Front(); i != nil; i = i.Next() {
    fmt.Println(i.Value)
}

3.15 Go语言nil:空值/零值

在Go语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是 nil。

nil 是Go语言中一个预定义好的标识符:

  • nil标识符不能和自身比较
  • nil不是关键字或保留字
  • nil没有类型
func main() {
    fmt.Printf("%T", nil)	//编译报错:use of untyped nil
    print(nil)
}
  • 不同类型的nil不能进行比较,虽然地址都是0x0
func main() {
    var m map[int]string
    var ptr *int
    fmt.Printf(m == ptr)	//编译报错
}
  • nil 是 map、slice、pointer、channel、func、interface 的零值
  • 不同类型的 nil 值占用的内存大小可能是不一样的
package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var p *struct{}
    fmt.Println( unsafe.Sizeof( p ) ) // 8
    var s []int
    fmt.Println( unsafe.Sizeof( s ) ) // 24
    var m map[int]bool
    fmt.Println( unsafe.Sizeof( m ) ) // 8
    var c chan string
    fmt.Println( unsafe.Sizeof( c ) ) // 8
    var f func()
    fmt.Println( unsafe.Sizeof( f ) ) // 8
    var i interface{}
    fmt.Println( unsafe.Sizeof( i ) ) // 16
}

3.16 Go语言make和new关键字的区别及实现原理

new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化。

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

从上面的代码可以看出,new 函数只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值。


make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length, so make([]int, 0, 10) allocates a slice of length 0 and
// capacity 10.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type

Go语言中的 new 和 make 主要区别如下:

  • make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
  • new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  • new 分配的空间被清零。make 分配空间后,会进行初始化;

实现原理

make

在编译期的类型检查阶段,Go语言其实就将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点,这些节点最终也会调用不同的运行时函数来初始化数据结构。

new

http://c.biancheng.net/view/5722.html

posted @ 2021-07-12 16:39  tlamm  阅读(131)  评论(0)    收藏  举报