受欢迎的常州做网站,有做外贸的平台网站吗,100种画册排版图解,产品策划书模板数组切片make 1. 数组2. 多维数组3. 切片3.1 直接声明新的切片函数构造切片3.3 思考题3.4 切片和数组的异同 4. 切片的复制5. map5.1 遍历map5.2 删除5.3 线程安全的map 6. nil7. new和make 1. 数组
数组是一个由固定长度的特定类型元素组成的序列#xff0c;一个数… 数组切片make 1. 数组2. 多维数组3. 切片3.1 直接声明新的切片函数构造切片3.3 思考题3.4 切片和数组的异同 4. 切片的复制5. map5.1 遍历map5.2 删除5.3 线程安全的map 6. nil7. new和make 1. 数组
数组是一个由固定长度的特定类型元素组成的序列一个数组可以由零个或多个元素组成。
因为数组的长度是固定的所以在Go语言中很少直接使用数组。
Go语言数组的声明
var 数组变量名 [元素数量]Type数组变量名数组声明及使用时的变量名。元素数量数组的元素数量可以是一个表达式但最终通过编译期计算的结果必须是整型数值元素数量不能含有到运行时才能确认大小的数值。Type可以是任意基本类型包括数组本身类型为数组本身时可以实现多维数组。
例子
//默认数组中的值是类型的默认值
var arr [3]int从数组中取值 通过索引下标取值索引从0开始 fmt.Println(arr[0])fmt.Println(arr[1])fmt.Println(arr[2])for range获取 for index,value : range arr{fmt.Printf(索引:%d,值%d \n,index,value)
}给数组赋值: 初始化的时候赋值 var arr [3]int [3]int{1,2,3}
//如果第三个不赋值就是默认值0
var arr [3]int [3]int{1,2}
//可以使用简短声明
arr : [3]int{1,2,3}
//如果不写数据数量而使用...表示数组的长度是根据初始化值的个数来计算
arr : [...]int{1,2,3}通过索引下标赋值 var arr [3]intarr[0] 5arr[1] 6arr[2] 7一定要注意数组是定长的不可更改在编译阶段就决定了 小技巧 如果觉的每次写 [3]int 有点麻烦你可以为 [3]int 定义一个新的类型。 type arr3 [3]int//这样每次用arr3 代替[3]int注意前面学过 定义一个类型后 arr3就是一个新的类型var arr arr3arr[0] 2for index,value : range arr{fmt.Printf(索引:%d,值%d \n,index,value)}如果想要只初始化第三个值怎么写 //2 给索引为2的赋值 所以结果是 0,0,3arr : [3]int{2:3}for index,value : range arr{fmt.Printf(索引:%d,值%d \n,index,value)}数组比较
如果两个数组类型相同包括数组的长度数组中元素的类型的情况下我们可以直接通过较运算符和!来判断两个数组是否相等只有当两个数组的所有元素都是相等的时候数组才是相等的不能比较两个类型不同的数组否则程序将无法完成编译。
a : [2]int{1, 2}
b : [...]int{1, 2}
c : [2]int{1, 3}
fmt.Println(a b, a c, b c) // true false false
d : [3]int{1, 2}
fmt.Println(a d) // 编译错误无法比较 [2]int [3]int2. 多维数组
Go语言中允许使用多维数组因为数组属于值类型所以多维数组的所有维度都会在创建时自动初始化零值多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。
声明多维数组的语法如下所示
//array_name 为数组的名字array_type 为数组的类型size1、size2 等等为数组每一维度的长度。
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}}取值 通过索引下标取值 fmt.Println(array[1][0])循环取值 for index,value : range array{fmt.Printf(索引:%d,值%d \n,index,value)}赋值
// 声明一个 2×2 的二维整型数组
var array [2][2]int
// 设置每个元素的整型值
array[0][0] 10
array[0][1] 20
array[1][0] 30
array[1][1] 40只要类型一致就可以将多维数组互相赋值如下所示多维数组的类型包括每一维度的长度以及存储在元素中数据的类型
// 声明两个二维整型数组 [2]int [2]int
var array1 [2][2]int
var array2 [2][2]int
// 为array2的每个元素赋值
array2[0][0] 10
array2[0][1] 20
array2[1][0] 30
array2[1][1] 40
// 将 array2 的值复制给 array1
array1 array2因为数组中每个元素都是一个值所以可以独立复制某个维度如下所示
// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int array1[1]
// 将数组中指定的整型值复制到新的整型变量里
var value int array1[1][0]3. 切片
切片Slice与数组一样也是可以容纳若干类型相同的元素的容器。
与数组不同的是无法通过切片类型来确定其值的长度。
每个切片值都会将数组作为其底层数据结构。
我们也把这样的数组称为切片的底层数组。
切片slice是对数组的一个连续片段的引用所以切片是一个引用类型。
这个片段可以是整个数组也可以是由起始和终止索引标识的一些项的子集需要注意的是终止索引标识的项不包括在切片内(左闭右开的区间)。
Go语言中切片的内部结构包含地址、大小和容量切片一般用于快速地操作一块数据集合。
从连续内存区域生成切片是常见的操作格式如下
slice [开始位置 : 结束位置]语法说明如下
slice表示目标切片对象开始位置对应目标切片对象的索引结束位置对应目标切片的结束索引。
从数组生成切片代码如下
var a [3]int{1, 2, 3}
//a[1:2] 生成了一个新的切片
fmt.Println(a, a[1:2])从数组或切片生成新的切片拥有如下特性
取出的元素数量为结束位置 - 开始位置取出元素不包含结束位置对应的索引切片最后一个元素使用 slice[len(slice)] 获取当缺省开始位置时表示从连续区域开头到结束位置(a[:2])当缺省结束位置时表示从开始位置到整个连续区域末尾(a[0:])两者同时缺省时与切片本身等效(a[:])两者同时为 0 时等效于空切片一般用于切片复位(a[0:0])。
注意超界会报运行时错误比如数组长度为3则结束位置最大只能为3 切片在指针的基础上增加了大小约束了切片对应的内存区域切片使用中无法对切片内部的地址和大小进行手动调整因此切片比指针更安全、强大。 示例 切片和数组密不可分如果将数组理解为一栋办公楼那么切片就是把不同的连续楼层出租给使用者出租的过程需要选择开始楼层和结束楼层这个过程就会生成切片 var highRiseBuilding [30]int
for i : 0; i 30; i {highRiseBuilding[i] i 1
}
// 区间
fmt.Println(highRiseBuilding[10:15])
// 中间到尾部的所有元素
fmt.Println(highRiseBuilding[20:])
// 开头到中间指定位置的所有元素
fmt.Println(highRiseBuilding[:2])3.1 直接声明新的切片
除了可以从原有的数组或者切片中生成切片外也可以声明一个新的切片每一种类型都可以拥有其切片类型表示多个相同类型元素的连续集合。
切片类型声明格式如下
//name 表示切片的变量名Type 表示切片对应的元素类型。
var name []Type// 声明字符串切片
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)
fmt.Println(numList nil)
fmt.Println(numListEmpty nil)切片是动态结构只能与 nil 判定相等不能互相判定相等。声明新的切片后可以使用 append() 函数向切片中添加元素。 var strList []string// 追加一个元素strList append(strList,我是码神)fmt.Println(strList)函数构造切片
如果需要动态地创建一个切片可以使用 make() 内建函数格式如下
make( []Type, size, cap )Type 是指切片的元素类型size 指的是为这个类型分配多少个元素cap 为预分配的元素数量这个值设定后不影响 size只是能提前分配空间降低多次分配空间造成的性能问题。
a : make([]int, 2)
b : make([]int, 2, 10)
fmt.Println(a, b)
//容量不会影响当前的元素个数因此 a 和 b 取 len 都是 2
//但如果我们给a 追加一个 a的长度就会变为3
fmt.Println(len(a), len(b))使用 make() 函数生成的切片一定发生了内存分配操作但给定开始与结束位置包括切片复位的切片只是将新的切片结构指向已经分配好的内存区域设定开始与结束位置不会发生内存分配操作。 3.3 思考题 var numbers4 [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}myslice : numbers4[4:6]//这打印出来长度为2fmt.Printf(myslice为 %d, 其长度为: %d\n, myslice, len(myslice))myslice myslice[:cap(myslice)]//为什么 myslice 的长度为2却能访问到第四个元素fmt.Printf(myslice的第四个元素为: %d, myslice[3])3.4 切片和数组的异同
同
类型安全性数组和切片都是类型安全的即它们只能存储固定类型的元素。零值数组和切片的零值分别是相应长度的零值数组和空切片。遍历数组和切片都可以使用 for 循环进行遍历。
异 长度和容量 数组的长度是固定的在声明时必须指定长度并且这个长度在数组的生命周期内是不可变的。切片是一个动态数组它的长度是可变的可以随着元素的添加和删除而变化。切片的容量表示在底层数组中可用的最大元素数量但可以通过重新分配来扩展。 声明和初始化 数组的声明和初始化必须指定长度 var arr [5]int // 声明一个长度为5的整数数组
arr[0] 1 // 初始化第一个元素切片的声明不需要指定长度但可以通过字面量、数组切片、或make函数来初始化 var s []int // 声明一个空切片
s []int{1, 2, 3} // 通过字面量初始化
s arr[1:4] // 通过数组切片
s make([]int, 5) // 通过 make 函数初始化一个长度为5的切片内存管理 数组是值类型当数组被赋值或作为参数传递时会创建数组的副本。这意味着对副本的修改不会影响原数组。切片是引用类型当切片被赋值或作为参数传递时会引用底层的数组。因此对切片的修改可能会影响其他引用同一个底层数组的切片。 灵活性 数组由于其固定长度的特性在需要动态改变大小的场景下显得不够灵活。切片由于其动态长度的特性在需要频繁改变大小的场景中更加灵活和高效。
4. 切片的复制
Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中如果加入的两个数组切片不一样大就会按照其中较小的那个数组切片的元素个数进行复制。
copy() 函数的使用格式如下
copy( destSlice, srcSlice []T) int其中 srcSlice 为数据来源切片destSlice 为复制的目标也就是将 srcSlice 复制到 destSlice目标切片必须分配过空间且足够承载复制的元素个数并且来源和目标的类型必须一致copy() 函数的返回值表示实际发生复制的元素个数。
下面的代码展示了使用 copy() 函数将一个切片复制到另一个切片的过程
slice1 : []int{1, 2, 3, 4, 5}
slice2 : []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置如果想要快速用一个切片的部分元素构建出一个新切片可以使用append函数并且这种方法构建的新切片不会与旧切片关联。 newSlicel append([]int{}, slicel[i:j])
//如果是以下这种方法则修改新切片的内容会影响旧切片
newSlicel slicel[i:j]
//更高效的写法不需要再分配一次内存
newSlicel append(make([]int, 0, j-i), slicel[i:j])切片的引用和复制操作对切片元素的影响:
package main
import fmt
func main() {// 设置元素数量为1000const 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])// 复制原始数据从4到6(不包含)copy(copyData, srcData[4:6])for i : 0; i 5; i {fmt.Printf(%d , copyData[i])}
}5. map
map 是一种无序的键值对的集合。
map 最重要的一点是通过 key 来快速检索数据key 类似于索引指向数据的值。
map 是一种集合所以我们可以像迭代数组和切片那样迭代它。不过map 是无序的我们无法决定它的返回顺序这是因为 map 是使用 hash 表来实现的。
map 是引用类型可以使用如下方式声明
//[keytype] 和 valuetype 之间允许有空格。
var mapname map[keytype]valuetype其中
mapname 为 map 的变量名。keytype 为键类型。valuetype 是键对应的值类型。 在声明的时候不需要知道 map 的长度因为 map 是可以动态增长的未初始化的 map 的值是 nil使用函数 len() 可以获取 map 中 键值对的数目。 package main
import fmt
func main() {var mapLit map[string]intvar mapAssigned map[string]intmapLit map[string]int{one: 1, two: 2}mapAssigned mapLit//mapAssigned 是 mapList 的引用对 mapAssigned 的修改也会影响到 mapList 的值。mapAssigned[two] 3fmt.Printf(Map literal at \one\ is: %d\n, mapLit[one])fmt.Printf(Map assigned at \two\ is: %d\n, mapLit[two])fmt.Printf(Map literal at \ten\ is: %d\n, mapLit[ten])
}map的另外一种创建方式
make(map[keytype]valuetype)切记不要使用new创建map否则会得到一个空引用的指针
map 可以根据新增的 key-value 动态的伸缩因此它不存在固定长度或者最大限制但是也可以选择标明 map 的初始容量 capacity格式如下
make(map[keytype]valuetype, cap)例如
map2 : make(map[string]int, 100)当 map 增长到容量上限的时候如果再增加新的 key-valuemap 的大小会自动加 1所以出于性能的考虑对于大的 map 或者会快速扩张的 map即使只是大概知道容量也最好先标明。既然一个 key 只能对应一个 value而 value 又是一个原始类型那么如果一个 key 要对应多个值怎么办
答案是使用切片
例如当我们要处理 unix 机器上的所有进程以父进程pid 为整形作为 key所有的子进程以所有子进程的 pid 组成的切片作为 value。
通过将 value 定义为 []int 类型或者其他类型的切片就可以优雅的解决这个问题示例代码如下所示
mp1 : make(map[int][]int)
mp2 : make(map[int]*[]int)5.1 遍历map
map 的遍历过程使用 for range 循环完成代码如下
scene : make(map[string]int)
scene[cat] 66
scene[dog] 4
scene[pig] 960
for k, v : range scene {fmt.Println(k, v)
}注意map是无序的不要期望 map 在遍历时返回某种期望顺序的结果
5.2 删除
使用 delete() 内建函数从 map 中删除一组键值对delete() 函数的格式如下
delete(map, 键)map 为要删除的 map 实例键为要删除的 map 中键值对的键。
scene : make(map[string]int)
// 准备map数据
scene[cat] 66
scene[dog] 4
scene[pig] 960
delete(scene, dog)
for k, v : range scene {fmt.Println(k, v)
}Go语言中并没有为 map 提供任何清空所有元素的函数、方法清空 map 的唯一办法就是重新 make 一个新的 map不用担心垃圾回收的效率Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
注意map 在并发情况下只读是线程安全的同时读写是线程不安全的。
5.3 线程安全的map
并发情况下读写 map 时会出现问题代码如下
// 创建一个int到int的映射
m : make(map[int]int)
// 开启一段并发代码
go func() {// 不停地对map进行写入for {m[1] 1}
}()
// 开启一段并发代码
go func() {// 不停地对map进行读取for {_ m[1]}
}()
// 无限循环, 让并发程序在后台执行
for {
}运行代码会报错输出如下
fatal error: concurrent map read and map write错误信息显示并发的 map 读和 map 写也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题map 内部会对这种并发操作进行检查并提前发现。
需要并发读写时一般的做法是加锁但这样性能并不高Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Mapsync.Map 和 map 不同不是以语言原生形态提供而是在 sync 包下的特殊结构。
sync.Map 有以下特性
无须初始化直接声明即可。sync.Map 不能使用 map 的方式进行取值和设置等操作而是使用 sync.Map 的方法进行调用Store 表示存储Load 表示获取Delete 表示删除。使用 Range 配合一个回调函数进行遍历操作通过回调函数返回内部遍历出来的值Range 参数中回调函数的返回值在需要继续迭代遍历时返回 true终止迭代遍历时返回 false。
package main
import (fmtsync
)
func main() {//sync.Map 不能使用 make 创建var scene sync.Map// 将键值对保存到sync.Map//sync.Map 将键和值以 interface{} 类型进行保存。scene.Store(greece, 97)scene.Store(london, 100)scene.Store(egypt, 200)// 从sync.Map中根据键取值fmt.Println(scene.Load(london))// 根据键删除对应的键值对scene.Delete(london)// 遍历所有sync.Map中的键值对//遍历需要提供一个匿名函数参数为 k、v类型为 interface{}每次 Range() 在遍历一个元素时都会调用这个匿名函数把结果返回。scene.Range(func(k, v interface{}) bool {fmt.Println(iterate:, k, v)return true})
}sync.Map 为了保证并发安全有一些性能损失因此在非并发情况下使用 map 相比使用 sync.Map 会有更好的性能。
6. nil
在Go语言中布尔类型的零值初始值为 false数值类型的零值为 0字符串类型的零值为空字符串而指针、切片、映射、通道、函数和接口的零值则是 nil。
nil和其他语言的null是不同的。
nil 标识符是不能比较的
package main
import (fmt
)
func main() {//invalid operation: nil nil (operator not defined on nil)fmt.Println(nilnil)
}nil 不是关键字或保留字
nil 并不是Go语言的关键字或者保留字也就是说我们可以定义一个名称为 nil 的变量比如下面这样
//但不提倡这样做
var nil errors.New(my god)nil 没有默认类型
package main
import (fmt
)
func main() {//error :use of untyped nilfmt.Printf(%T, nil)print(nil)
}不同类型 nil 的指针是一样的
package main
import (fmt
)
func main() {var arr []intvar num *intfmt.Printf(%p\n, arr)fmt.Printf(%p, num)
}nil 是 map、slice、pointer、channel、func、interface 的零值
package main
import (fmt
)
func main() {var m map[int]stringvar ptr *intvar c chan intvar sl []intvar f func()var i interface{}fmt.Printf(%##v\n, m)fmt.Printf(%##v\n, ptr)fmt.Printf(%##v\n, c)fmt.Printf(%##v\n, sl)fmt.Printf(%##v\n, f)fmt.Printf(%##v\n, i)
}零值是Go语言中变量在声明之后但是未初始化被赋予的该类型的一个默认值。
不同类型的 nil 值占用的内存大小可能是不一样的
package main
import (fmtunsafe
)
func main() {var p *struct{}fmt.Println( unsafe.Sizeof( p ) ) // 8var s []intfmt.Println( unsafe.Sizeof( s ) ) // 24var m map[int]boolfmt.Println( unsafe.Sizeof( m ) ) // 8var c chan stringfmt.Println( unsafe.Sizeof( c ) ) // 8var f func()fmt.Println( unsafe.Sizeof( f ) ) // 8var i interface{}fmt.Println( unsafe.Sizeof( i ) ) // 16
}具体的大小取决于编译器和架构
7. new和make
make 关键字的主要作用是创建 slice、map 和 Channel 等内置的数据结构而 new 的主要作用是为类型申请一片内存空间并返回指向这片内存的指针。
make 分配空间后会进行初始化new分配的空间被清零new 分配返回的是指针即类型 *Type。make 返回引用即 Typenew 可以分配任意类型的数据