温馨提示:这篇文章已超过413天没有更新,请注意相关的内容是否还可用!
摘要:本篇文章是关于Go语言的超全详解,面向初学者,从入门级别开始讲解。文章涵盖了Go语言的基本概念、语法、数据类型、函数、控制流、数组、切片、映射等基础知识,以及Go语言的并发编程和性能优化等方面的内容。文章旨在帮助读者快速掌握Go语言的核心概念和编程技巧,为后续的进阶学习打下坚实的基础。
文章目录
- 1. Go语言的出现
- 2. go版本的hello world
- 3. 数据类型
- 3.0 定义变量
- 3.0.1 如果变量没有初始化
- 3.0.2 如果变量没有指定类型
- 3.0.3 :=符号
- 3.0.4 多变量声明
- 3.0.5 匿名变量
- 3.0.6 变量作用域
- 3.1 基本类型
- 3.2 指针
- 3.2.1 指针声明和初始化
- 3.2.2 空指针
- 3.3 数组
- 3.3.1 声明数组
- 3.3.2 初始化数组
- 3.3.3 go中的数组名意义
- 3.3.4 数组指针
- 3.4 结构体
- 3.4.1 声明结构体
- 3.4.2 访问结构体成员
- 3.4.3 结构体指针
- 3.5 字符串
- 3.5.1 字符串定义和初始化
- 3.5.2 字符串UTF8编码
- 3.5.3 字符串的强制类型转换
- 3.6 slice
- 3.6.1 slice定义
- 3.6.2 添加元素
- 3.6.3 删除元素
- 3.7 函数
- 3.7.1 函数分类
- 3.7.2 函数声明和定义
- 3.7.3 函数传参
- 3.7.4 函数返回值
- 3.7.5 递归调用
- 3.8 方法
- 3.9 接口
- 3.9.1 什么是接口
- 3.9.2 结构体类型
- 3.9.3 具体类型向接口类型赋值
- 3.9.4 获取接口类型数据的具体类型信息
- 3.10 channel
- 3.10.1 相关结构体定义
- 3.10.2 阻塞式读写channel操作
- 3.10.3 非阻塞式读写channel操作
- 3.11 map
- 3.11.1 插入数据
- 3.11.2 删除数据
- 3.11.3 查找数据
- 3.11.4 扩容
- 4. 常用语句及关键字
- 4.1 条件语句
- 4.2 循环语句
- 4.2.1 循环处理语句
- 4.2.1 循环控制语句
- 4.3 关键字
1. Go语言的出现
在具体学习go语言的基础语法之前,我们来了解一下go语言出现的时机及其特点。
Go语言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三个大牛于2007年开始设计发明,他们最终的目标是设计一种适应网络和多核时代的C语言。所以Go语言很多时候被描述为“类C语言”,或者是“21世纪的C语言”,当然从各种角度看,Go语言确实是从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等诸多编程思想。但是Go语言更是对C语言最彻底的一次扬弃,它舍弃了C语言中灵活但是危险的指针运算,还重新设计了C语言中部分不太合理运算符的优先级,并在很多细微的地方都做了必要的打磨和改变。
2. go版本的hello world
在这一部分我们只是使用“hello world”的程序来向大家介绍一下go语言的所编写的程序的基本组成。
package main import "fmt" func main() { // 终端输出hello world fmt.Println("Hello world!") }
和C语言相似,go语言的基本组成有:
- 包声明,编写源文件时,必须在非注释的第一行指明这个文件属于哪个包,如package main。
- 引入包,其实就是告诉Go 编译器这个程序需要使用的包,如import "fmt"其实就是引入了fmt包。
- 函数,和c语言相同,即是一个可以实现某一个功能的函数体,每一个可执行程序中必须拥有一个main函数。
- 变量,Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。
- 语句/表达式,在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
- 注释,和c语言中的注释方式相同,可以在任何地方使用以 // 开头的单行注释。以 /* 开头,并以 */ 结尾来进行多行注释,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
需要注意的是:标识符是用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母和数字、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。
- 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
- 标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected)。
3. 数据类型
在 Go 编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。具体分类如下:
类型 详解 布尔型 布尔型的值只可以是常量 true 或者 false。 数字类型 整型 int 和浮点型 float。Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 字符串类型 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 派生类型 (a) 指针类型(Pointer)(b) 数组类型© 结构化类型(struct)(d) Channel 类型(e) 函数类型(f) 切片类型(g) 接口类型(interface)(h) Map 类型 3.0 定义变量
声明变量的一般形式是使用 var 关键字,具体格式为:var identifier typename。如下的代码中我们定义了一个类型为int的变量。
package main import "fmt" func main() { var a int = 27 fmt.Println(a); }
3.0.1 如果变量没有初始化
在go语言中定义了一个变量,指定变量类型,如果没有初始化,则变量默认为零值。零值就是变量没有做初始化时系统默认设置的值。
类型 零值 数值类型 0 布尔类型 false 字符串 “”(空字符串) 3.0.2 如果变量没有指定类型
在go语言中如果没有指定变量类型,可以通过变量的初始值来判断变量类型。如下代码
package main import "fmt" func main() { var d = true fmt.Println(d) }
3.0.3 :=符号
当我们定义一个变量后又使用该符号初始化变量,就会产生编译错误,因为该符号其实是一个声明语句。
使用格式:typename := value
也就是说intVal := 1相等于:
var intVal int intVal =1
3.0.4 多变量声明
可以同时声明多个类型相同的变量(非全局变量),如下图所示:
var x, y int var c, d int = 1, 2 g, h := 123, "hello"
关于全局变量的声明如下:
var ( vname1 v_type1 vname2 v_type2 )
具体举例如下:
var ( a int b bool )
3.0.5 匿名变量
匿名变量的特点是一个下画线_,这本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。
使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。
示例代码如下:
func GetData() (int, int) { return 10, 20 } func main(){ a, _ := GetData() _, b := GetData() fmt.Println(a, b) }
需要注意的是匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
3.0.6 变量作用域
作用域指的是已声明的标识符所表示的常量、类型、函数或者包在源代码中的作用范围,在此我们主要看一下go中变量的作用域,根据变量定义位置的不同,可以分为一下三个类型:
- 函数内定义的变量为局部变量,这种局部变量的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。这种变量在存在于函数被调用时,销毁于函数调用结束后。
- 函数外定义的变量为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,甚至可以使用import引入外部包来使用。全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。
- 函数定义中的变量成为形式参数,定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。形式参数会作为函数的局部变量来使用。
3.1 基本类型
类型 描述 uint8 / uint16 / uint32 / uint64 无符号 8 / 16 / 32 / 64位整型 int8 / int16 / int32 / int64 有符号8 / 16 / 32 / 64位整型 float32 / float64 IEEE-754 32 / 64 位浮点型数 complex64 / complex128 32 / 64 位实数和虚数 byte 类似 uint8 rune 类似 int32 uintptr 无符号整型,用于存放一个指针 以上就是go语言基本的数据类型,有了数据类型,我们就可以使用这些类型来定义变量,Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。
3.2 指针
与C相同,Go语言让程序员决定何时使用指针。变量其实是一种使用方便的占位符,用于引用计算机内存地址。Go 语言中的的取地址符是&,放到一个变量前使用就会返回相应变量的内存地址。
指针变量其实就是用于存放某一个对象的内存地址。
3.2.1 指针声明和初始化
和基础类型数据相同,在使用指针变量之前我们首先需要申明指针,声明格式如下:var var_name *var-type,其中的var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。
代码举例如下:
var ip *int /* 指向整型*/ var fp *float32 /* 指向浮点型 */
指针的初始化就是取出相对应的变量地址对指针进行赋值,具体如下:
var a int= 20 /* 声明实际变量 */ var ip *int /* 声明指针变量 */ ip = &a /* 指针变量的存储地址 */
3.2.2 空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil,也称为空指针。它概念上和其它语言的null、NULL一样,都指代零值或空值。
3.3 数组
和c语言相同,Go语言也提供了数组类型的数据结构,数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。
3.3.1 声明数组
Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
var variable_name [SIZE] variable_type
以上就可以定一个一维数组,我们举例代码如下:
var balance [10] float32
3.3.2 初始化数组
数组的初始化方式有不止一种方式,我们列举如下:
- 直接进行初始化:var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
- 通过字面量在声明数组的同时快速初始化数组:balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
- 数组长度不确定,编译器通过元素个数自行推断数组长度,在[ ]中填入...,举例如下:var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}和balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
- 数组长度确定,指定下标进行部分初始化:balanced := [5]float32(1:2.0, 3:7.0)
注意:
- 初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小。
3.3.3 go中的数组名意义
在c语言中我们知道数组名在本质上是数组中第一个元素的地址,而在go语言中,数组名仅仅表示整个数组,是一个完整的值,一个数组变量即是表示整个数组。
所以在go中一个数组变量被赋值或者被传递的时候实际上就会复制整个数组。如果数组比较大的话,这种复制往往会占有很大的开销。所以为了避免这种开销,往往需要传递一个指向数组的指针,这个数组指针并不是数组。关于数组指针具体在指针的部分深入的了解。
3.3.4 数组指针
通过数组和指针的知识我们就可以定义一个数组指针,代码如下:
var a = [...]int{1, 2, 3} // a 是一个数组 var b = &a // b 是指向数组的指针
数组指针除了可以防止数组作为参数传递的时候浪费空间,还可以利用其和for range来遍历数组,具体代码如下:
for i, v := range b { // 通过数组指针迭代数组的元素 fmt.Println(i, v) }
具体关于go语言的循环语句我们在后文中再进行详细介绍。
3.4 结构体
通过上述数组的学习,我们就可以直接定义多个同类型的变量,但这往往也是一种限制,只能存储同一种类型的数据,而我们在结构体中就可以定义多个不同的数据类型。
3.4.1 声明结构体
在声明结构体之前我们首先需要定义一个结构体类型,这需要使用type和struct,type用于设定结构体的名称,struct用于定义一个新的数据类型。具体结构如下:
type struct_variable_type struct { member definition member definition ... member definition }
定义好了结构体类型,我们就可以使用该结构体声明这样一个结构体变量,语法如下:
variable_name := structure_variable_type {value1, value2...valuen} variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
3.4.2 访问结构体成员
如果要访问结构体成员,需要使用点号 . 操作符,格式为:结构体变量名.成员名。举例代码如下:
package main import "fmt" type Books struct { title string author string } func main() { var book1 Books Book1.title = "Go 语言入门" Book1.author = "mars.hao" }
3.4.3 结构体指针
关于结构体指针的定义和申明同样可以套用前文中讲到的指针的相关定义,从而使用一个指针变量存放一个结构体变量的地址。
定义一个结构体变量的语法:var struct_pointer *Books。
这种指针变量的初始化和上文指针部分的初始化方式相同struct_pointer = &Book1,但是和c语言中有所不同,使用结构体指针访问结构体成员仍然使用.操作符。格式如下:struct_pointer.title
3.5 字符串
一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。
3.5.1 字符串定义和初始化
Go语言字符串的底层结构在reflect.StringHeader中定义,具体如下:
type StringHeader struct { Data uintptr Len int }
也就是说字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。
字符串其实是一个结构体,因此字符串的赋值操作也就是reflect.StringHeader结构体的复制过程,并不会涉及底层字节数组的复制,所以我们也可以将字符串数组看作一个结构体数组。
字符串和数组类似,内置的len函数返回字符串的长度。
3.5.2 字符串UTF8编码
根据Go语言规范,Go语言的源文件都是采用UTF8编码。因此,Go源文件中出现的字符串面值常量一般也是UTF8编码的(对于转义字符,则没有这个限制)。提到Go字符串时,我们一般都会假设字符串对应的是一个合法的UTF8编码的字符序列。
Go语言的字符串中可以存放任意的二进制字节序列,而且即使是UTF8字符序列也可能会遇到坏的编码。如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符‘\uFFFD’,这个字符在不同的软件中的显示效果可能不太一样,在印刷中这个符号通常是一个黑色六角形或钻石形状,里面包含一个白色的问号‘�’。
下面的字符串中,我们故意损坏了第一字符的第二和第三字节,因此第一字符将会打印为“�”,第二和第三字节则被忽略;后面的“abc”依然可以正常解码打印(错误编码不会向后扩散是UTF8编码的优秀特性之一)。代码如下:
fmt.Println("\xe4\x00\x00\xe7\x95\x8cabc") // �界abc
不过在for range迭代这个含有损坏的UTF8字符串时,第一字符的第二和第三字节依然会被单独迭代到,不过此时迭代的值是损坏后的0:
// 0 65533 // \uFFFD, 对应 � // 1 0 // 空字符 // 2 0 // 空字符 // 3 30028 // 界 // 6 97 // a // 7 98 // b // 8 99 // c
3.5.3 字符串的强制类型转换
在上文中我们知道源代码往往会采用UTF8编码,如果不想解码UTF8字符串,想直接遍历原始的字节码:
- 可以将字符串强制转为[]byte字节序列后再行遍历(这里的转换一般不会产生运行时开销):
- 采用传统的下标方式遍历字符串的字节数组
除此以外,字符串相关的强制类型转换主要涉及到[]byte和[]rune两种类型。每个转换都可能隐含重新分配内存的代价,最坏的情况下它们的运算时间复杂度都是O(n)。
不过字符串和[]rune的转换要更为特殊一些,因为一般这种强制类型转换要求两个类型的底层内存结构要尽量一致,显然它们底层对应的[]byte和[]int32类型是完全不同的内部布局,因此这种转换可能隐含重新分配内存的操作。
3.6 slice
简单地说,切片就是一种简化版的动态数组。因为动态数组的长度不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,而切片则使用得相当广泛。
切片高效操作的要点是要降低内存分配的次数,尽量保证append操作(在后续的插入和删除操作中都涉及到这个函数)不会超出cap的容量,降低触发内存分配的次数和每次分配内存大小。
3.6.1 slice定义
我们先看看切片的结构定义,reflect.SliceHeader:
type SliceHeader struct { Data uintptr // 指向底层的的数组指针 Len int // 切片长度 Cap int // 切片最大长度 }
和数组一样,内置的len函数返回切片中有效元素的长度,内置的cap函数返回切片容量大小,容量必须大于或等于切片的长度。
切片可以和nil进行比较,只有当切片底层数据指针为空时切片本身为nil,这时候切片的长度和容量信息将是无效的。如果有切片的底层数据指针为空,但是长度和容量不为0的情况,那么说明切片本身已经被损坏了
只要是切片的底层数据指针、长度和容量没有发生变化的话,对切片的遍历、元素的读取和修改都和数组是一样的。在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。
当我们想定义声明一个切片时可以如下:
在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息·(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。
3.6.2 添加元素
append() :内置的泛型函数,可以向切片中增加元素。
- 在切片尾部追加N个元素
var a []int a = append(a, 1) // 追加1个元素 a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式 a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
注意:尾部添加在容量不足的条件下需要重新分配内存,可能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用append函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。
- 在切片开头位置添加元素
var a = []int{1,2,3} a = append([]int{0}, a...) // 在开头位置添加1个元素 a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
注意:在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
- 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个位置插入切片
每个添加操作中的第二个append调用都会创建一个临时切片,并将a[i:]的内容复制到新创建的切片中,然后将临时创建的切片再追加到a[:i]。
- append和copy组合
a = append(a, 0) // 切片扩展1个空间 copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置 a[i] = x // 设置新添加的元素
第三个操作中会创建一个临时对象,我们可以借用copy函数避免这个操作,这种方式操作语句虽然冗长了一点,但是相比前面的方法,可以减少中间创建的临时切片。
3.6.3 删除元素
根据要删除元素的位置有三种情况:
- 从开头位置删除;
- 直接移动数据指针,代码如下:
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将后续数据向前移动,代码如下:
a = []int{1, 2, 3} a = a[:copy(a, a[1:])] // 删除开头1个元素 a = a[:copy(a, a[N:])] // 删除开头N个元素
- 从中间位置删除;
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成:
- append删除操作如下:
a = []int{1, 2, 3, ...} a = append(a[:i], a[i+1], ...) a = append(a[:i], a[i+N:], ...)
- copy删除操作如下:
a = []int{1, 2, 3} a = a[:copy(a[:i], a[i+1:])] // 删除中间1个元素 a = a[: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 函数
为完成某一功能的程序指令(语句)的集合,称为函数。
3.7.1 函数分类
在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。
举例代码如下:
- 具名函数:就和c语言中的普通函数意义相同,具有函数名、返回值以及函数参数的函数。
func Add(a, b int) int { return a+b }
- 匿名函数:指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。
var Add = func(a, b int) int { return a+b }
解释几个名词如下:
- 闭包函数:返回为函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域。
- 一级对象:支持闭包的多数语言都将函数作为第一级对象,就是说函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
- 包:go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的。
3.7.2 函数声明和定义
Go 语言函数定义格式如下:
func fuction_name([parameter list])[return types]{ 函数体 }
解析 func 函数由func开始声明 function_name 函数名称 parameter list 参数列表 return_types 返回类型 函数体 函数定义的代码集合 3.7.3 函数传参
Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。
当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果,我们解释一下解包的含义,代码如下:
func main(){ var a = []int{1, 2, 3} Print(a...) // 解包 Print(a) // 未解包 } func Print(a ...int{}) { fmt.Println(a...) }
以上当传入参数为a...时即是对切片a进行了解包,此时其实相当于直接调用Print(1,2,3)。当传入参数直接为 a时等价于直接调用Print([]int{}{1,2,3})
3.7.4 函数返回值
不仅函数的参数可以有名字,也可以给函数的返回值命名。
举例代码如下:
func Find(m map[int]int, key int)(value int, ok bool) { value,ok = m[key] return }
如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值,举例代码如下:
func mian() { for i := 0 ; i defer func() { println(i) } } } // 该函数最终的输出为: // 3 // 3 // 3 for i := 0; i 2^15时,也即overflow数量超过32768时。
- 如果常规桶数目不大于2^15,那么使用的溢出桶数目超过常规桶就算是多了;
- 如果常规桶数目大于215,那么使用溢出桶数目一旦超过215就算多了。
这样做的目的是把松散的键值对重新排列一次,能够存储的更加紧凑,进而减少溢出桶的使用,以使bucket的使用率更高,进而保证更快的存取。
4. 常用语句及关键字
接下来我们了解一下关于go语言语句的基本内容。
4.1 条件语句
和c语言类似,相关的条件语句如下表所示:
语句 描述 if 语句 if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 if…else 语句 if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 switch 语句 switch 语句用于基于不同条件执行不同动作。 select 语句 select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。 - if语句
语法如下:
if 布尔表达式 { /* 在布尔表达式为 true 时执行 */ }
- if-else语句
if 布尔表达式 { /* 在布尔表达式为 true 时执行 */ } else { /* 在布尔表达式为 false 时执行 */ }
- switch语句
其中的变量v可以是任何类型,val1和val2可以是同类型的任意值,类型不局限为常量或者整数,或者最终结果为相同类型的表达式。
switch v { case val1: ... case val2: ... default: ... }
- select语句
select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。它将会随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
select { case communication clause : statement(s); case communication clause : statement(s); /* 你可以定义任意数量的 case */ default : /* 可选 */ statement(s); }
注意:
- 每个case必须都是一个通信
- 所有channel表达式都会被求值,所有被发送的表达式都会被求值
- 如果任意某一个通信都可以,它就执行,其他就忽略
- 如果有多个case都可以运行,select就会随机挑选一个来执行。
- 如果没有一个case可以被运行:如果有default子句,就执行default子句,select将被阻塞,直到某个通信可以运行,从而避免饥饿问题。
4.2 循环语句
4.2.1 循环处理语句
go中时使用for实现循环的,共有三种形式:
语法 和c语言中的for相同 for init; condition; post {} 和c语言中的while相同 for condition{} 和c语言中的for(;;)相同 for{} 除此以外,for循环还可以直接使用range对slice、map、数组以及字符串等进行迭代循环,格式如下:
for key, value := range oldmap { newmap[key] = value }
4.2.1 循环控制语句
控制语句 详解 break 中断跳出循环或者switch语句 continue 跳过当前循环的剩余语句,然后继续下一轮循环 goto语句 将控制转移到被标记的语句 - break
break主要用于循环语句跳出循环,和c语言中的使用方式是相同的。且在多重循环的时候还可以使用label标出想要break的循环。
实例代码如下:
a := 0 for a fmt.Printf("%d\n", a) a++ if a==2 { break; } } /* output 0 1 2 */ fmt.Printf("i: %d\n", i) for i2 := 11; i2 fmt.Printf("i2: %d\n", i2) continue } } /* output i: 1 i2: 11 i2: 12 i2: 13 i: 2 i2: 11 i2: 12 i2: 13 i: 3 i2: 11 i2: 12 i2: 13 */ // 使用标记 fmt.Println("---- continue label ----") re: for i := 1; i fmt.Printf("i: %d", i) for i2 := 11; i2 fmt.Printf("i2: %d\n", i2) continue re } } /* output i: 1 i2: 11 i: 2 i2: 11 i: 3 i2: 11 */ if a == 2 { a = a+1 goto LOOP } fmt.Printf("%d\n", a) a++ } /* output: 0 1 2 3 4 */
- break
- select语句
- switch语句
- if-else语句
- if语句
什么是增量扩容呢?
如果负载因子>6.5时,进行增量扩容。这时会新建一个桶(bucket),新的bucket长度是原来的2倍,然后旧桶数据搬迁到新桶。每个旧桶的键值对都会分流到两个新桶中
主要是缩短map容器的响应时间。假如我们直接将map用作某个响应实时性要求非常高的web应用存储,如果不采用增量扩容,当map里面存储的元素很多之后,扩容时系统就会卡往,导致较长一段时间内无法响应请求。不过增量扩容本质上还是将总的扩容时间分摊到了每一次哈希操作上面。
什么是等量扩容?它的触发条件是什么?进行等量扩容后的优势是什么?
等量扩容,就是创建和旧桶数目一样多的新桶,然后把原来的键值对迁移到新桶中,重新做一遍类似增量扩容的搬迁动作。
触发条件:负载因子没超标,溢出桶较多。这个较多的评判标准为:
- copy删除操作如下:
- 从中间位置删除;
- 使用copy将后续数据向前移动,代码如下:
- 将后面的数据向开头移动,使用append原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)
还没有评论,来说两句吧...