Go 语言中包括以下内置基础类型:
- 布尔型:bool
- 整型: int64 int32 int16 int8 uint8(byte) uint16 uint32 uint64 int uint
- 浮点型:float32 float64
- 复数型:complex64 complex128
- 字符串:string
- 字符型:rune
- 错误型:error
Go 语言中包括以下内置复合类型:
- 数组:array
- 切片:slice (也是引用类型)
- 指针:pointer
- 字典:map(也是引用类型)
- 通道:chan(也是引用类型)
- 结构体:struct
- 接口:interface(也是引用类型)
各个类型的零值:
- 对于数字是 0
- 对于布尔值是 false
- 对于字符串是 ""
- 对于接口和引用类型(slice,指针,map,通道,函数)是
nil
- 对于一个数组或者结构体,零值是其所有成员的零值
整数
Go同时具备有符号数和无符号整数:
- 有符号整数分4种大小,8位,16位,32位,64位:用
int8
,int16
,int32
,int64
,对应的无符号整数是uint8
,uint16
,uint32
,uint64
。 - 此外还有2种类型
int
和uint
,这两种类型大小相等,都为32位或者64位,但不能认为它们一定就是32位或者64位,即使相同的硬件平台,不同的编译器也可能选用不同的大小。 - 最后还有一种无符号整数
uintptr
,其大小和平台有关系,但足以完整存放指针。
注意在go中,整数与整数相除,如果除数为 0 会 panic
zero := 0
a := 1/zero // panic: runtime error: integer divide by zero
浮点数
Go具有2种大小的浮点数 float32
和 float64
,十进制下,float32 的有效数字位数大约是6位, float64 有效数字大约是15位。
math包中给出了浮点值的极限:常量math.MaxFloat32
是float32最大值,常量math.MaxFloat64
是float64最大值。
math包种还有3个特殊的数:
- 正无穷大
- 负无穷大
-
NaN
(not a number)表示数学上无意义的运算结果
- 浮点数的相等性比较
var f float32 = 16777216 fmt.Println(f == f+1) // true
复数
TODO
布尔值
bool型的值只有2种可能:真(true) 或 假(false)。
和 C语言不同,布尔值无法隐式转换成数值(如0或1),反之也不行。
b := 1
if b { // 编译错误
// ...
}
所以如果需要,最好是写成2个函数:
func btoi(b bool) int {
if b {
return 1
}
return 0
}
func itob(i int) bool { return i != 0 }
常量
常量是一种表达式,其可以保证在编译阶段就计算出表达式的值,并不需要等到运行时。
const (
e = 2.71828
pi = 3.1415
)
- 常量生成器
常量的生成可以用使用常量生成器iota
,它创建一系列相关值,而不是逐个显示写出,常量的声明中,iota从0开始取值,逐项加1。type Weekday int const ( Sunday Weekday = itoa // Sunday = 0 Monday Tuesday //..... ) const ( _ = 1 << (10 * iota) KiB // 1 << (10 * 1) = 1024 MiB // 1 << (10 * 2) = 1048576 GiB //...... )
字符串
字符串是不可变的 byte序列,其本身是一个复合结构,头部指向字节数组,没有NULL结尾,默认以UTF-8编码存储的Unicode码点序列。
type stringStruct {
str unsafe.Pointer
len int
}
内置的len
函数返回字符串的字节数(并非文字符号的数目),cap
不支持字符串类型参数。
下标访问操作s[i]
则取得第 i 个字符,但要注意:字符串的第i
个字节并不一定就是第i
个字符,因为非ASCII字符的UTF-8码点需要两个字节或多个字节。
package main
import "fmt"
func main () {
s := "长江"
fmt.Println(len(s)) // 6
fmt.Println(s) // 长江
fmt.Println(s[0], s[1]) // 233 149
}
因为字符串不可变,所以字符串内部数据不允许修改。
s[0] = 'L' // 编译错误
不可变,意味着两个字符串可以安全地共用一段底层内存,字符串s及其子串可以安全地共用数据。
这种特性使得复制任何长度字符串的开销都很低廉,类似,子串生成操作的开销也很低廉,因为这2种情况都没有分配新的内存。
子串生成操作s[i:j]
产生一个新字符串,内容取自原字符串[i, j)
的字节
-
修改字符串
如果要想修改字符串,则必须将其转换为可变类型([]rune
和[]byte
),待完成转换之后,再转换回来,转换过程,都将发生一次内存拷贝b := []byte{'h','e', 'l', 'l', 'o'} s := string(b) // 字节数组转字符串 s := "hello" b := []byte(s) // 字符串转字节数组
-
字符串的拼接
字符串的拼接时会触发内存分配和拷贝,单行语句拼接多个字符串时只分配一次内存s = s + "a" + "b"
UTF8
-
字符串的遍历
- 按字节遍历
s := "中国" // byte for i := 0; i < len(s); i++ { fmt.Printf("%d: [%c]\t", i, s[i]) } // 输出: 0: [ä] 1: [¸] 2: [] 3: [å] 4: [�] 5: [½]
- 按rune遍历
rune,可以理解为是Unicode字符
这意味着:index 可能是不连续的。// 返回索引和Unicode字符 for index, c := range s { fmt.Printf("%d: [%c]\t", index, c) } // 输出: 0: [中] 3: [国]
- 按字节遍历
-
4个标准包对字符串操作比较重要:
strings
,bytes
,strconv
,Unicode
。-
strings
用于搜索,替换,比较,修整,切分与连接字符串等 -
bytes
用于操作字节slice([]byte
类型) -
strconv
转换布尔值,整数,浮点数为与之对应的字符串的互相转换- 字符串转int
s := "123" var i int var err error i, err = strconv.Atoi(s)
- 字符串转int64
s := "123" var i int64 var err error i, err = strconv.ParseInt(s, 10, 64)
- 字符转uint64
s := "123" var i uint64 var err error i, err = strconv.ParseUint(s, 10, 64)
- 数字转字符串
i := 123 var s string s = strconv.Itoa(i)
- 字符串转int
-
Unicode
判别文字符号值特性函数,如IsDigit
,IsLetter
,IsUpper
,IsLower
,ToUpper
,ToLower
。
-
数组
数组是固定长度且拥有0个或者多个相同数据类型元素序列。
-
初始化
var s [3]int var q [3]int = [3]int{1, 2, 3} // 数组字面量 q := [...]int{1, 2, 3} // 数组字面量 r := [...]int{99: -1} // 100个元素的数组,最后一个元素-1,其余是0
默认情况下,数组的长度是数组类型的一部分,所以
[3]int
和[4]int
是2种不同的数组类型,并且数组的长度必须 是常量表达式。
内置函数len
和cap
返回数组的长度 -
多维数组
- 多维数组的类型包括每一维度的长度以及最终存储在元素中的数据类型
- 在定义多维数组时,仅仅第一维度允许使用
...
- 内置函数
len
和cap
都返回第一维度的长度
-
数组之间可以赋值
在C/C++ 中数组之间不允许赋值,在Go中,同类型的数组是支持相互赋值的arrayA := [...]int {1, 2, 3} arrayB := arrayA // arrayB持有一份拷贝,修改互不影响
-
在函数间传递数组
Go的数组在函数传参是值传递,意味着整个数组,都会被完整复制,所以为了效率,更好的是传递一个数组指针。func foo(ptr *[32]int) { ... }
数组的比较
在长度和类型相同的数组是可以使用==
比较,长度不同或者类型不同的数组不能比较。
slice
slice 表示一个拥有相同类型元素地可变长度的序列,slice通常写作[]T
,它的底层就是一个数组,对数组任意分隔,就可以得到一个切片。
array := [5]string{"a","b","c","d","e"} // array 是数组
slice := array[:] // 等同于原数组的切片
slice := array[2:5] // slice 是切片
slice := make([]int, 3) // 使用make创建一个长度为3的切片
slice := make([]int, 3, 5) // 使用make创建一个长度为3,容量为5的切片
-
slice有三个属性:指针,长度,容量
array := [5]int{1, 2, 3, 4, 5} sliceA := array[1:2] // 1, len(sliceA) = 1, cap(sliceA) = 4 sliceB := array[2:4] // 1, len(sliceB = 2, cap(sliceB) = 3
- 指针
指针指向数组的第一个可以从slice种访问的元素,它并不一定是底层数组的的第一个元素。 - 长度
长度是slice种元素个数,它不能超过slice的容量。可以使用len
函数得到长度。 - 容量
从slice的起始元素到底层数组的最后一个元素间元素个数。可以使用cap
函数得到容量。
因为slice包含了指向数组元素的指针,所以将一个slice传递给函数的时候,就可以在函数内部修改底层数组的元素。
- 指针
-
nil 切片 和 空切片
- nil 切片
var slice []int s := []int(nil) // 类型转换
- 空切片
slice := make([]int, 0) slice := []int{}
- nil 切片
-
切片切割
s := []int{1, 2, 3, 4} s1 := s[1:3] // s1 = s[2, 3] 即范围 [1:3) 左闭右开的范围。
-
slice无法比较
和数组不同的是,slice无法比较,因此不能用==
比较2个slice是否拥有相同的元素。
对于[]byte
可用标准库的bytes.Equal
,但是对于其他类型,则必须自己函数实现。唯一允许的比较操作是和
nil
做比较,值为nil
的slice没有对应的底层数组。var s []int // len(s) == 0, s == nil s = nil // len(s) == 0, s == nil s = []int(nil) // 类型转换,len(s) == 0, s = []int{} // len(s) == 0, s != nil
-
切片迭代
- 使用 range
a := []int{1, 2, 4, 8} for idx, num := range a { fmt.Println(idx, num) } for idx := range a { fmt.Println(idx) }
- 使用传统的 for 循环
a := []int{1, 2, 4, 8} for i := 0; i < len(a); i++ { fmt.Println(a[i]) }
- 使用 range
-
slice 追加元素
内置函数append
用来将元素追加到slice后面,对 空切片 和 nil切片 都可以正常使用append
函数- slice 追加元素
slice := []string{"hello", "world"} str := "go" slice = append(slice, str) // slice: [hello world go]
- slice 追加另一个slice
slice := []string{"hello", "world"} sliceA := []string{"learn", "slice"} slice = append(slice, sliceA...) // slice: [hello world go learn slice]
使用
append
向 slice 追加元素时,如果slice空间不足,则会触发slice扩容,扩容实际上是重新分配一块更大的内存,将原slice的数据拷贝进新slice,然后再返回新slice,扩容后再将数据追加进去。func main() { var x, y []int for i := 0; i < 20; i++ { y = append(x, i) fmt.Printf("%d\tlen=%d\tcap=%d\tslice=%v\n", i, len(y), cap(y), y) x = y } }
-
扩容策略
- 如果原slice的容量小于1024,则新slice容量将扩大位原来的2倍
- 如果原slice的容量大于或等于1024,则新slice容量将扩大位原来的1.25倍
看得出来,上面的扩容策略是为了在避免频繁的扩容和避免浪费空间之间的平衡。
- slice 追加元素
-
slice 拷贝
可以使用内置的copy
将源切片的数据逐个拷贝到目的切片指向的数组中,注意:拷贝过程中不会发生扩容,拷贝的数量取决于两个切片长度的最小值。b2 := make([]byte, len(b)) copy(b2, b)
-
slice 插入元素
- 开头插入
需要注意的是,在切片开头插入元素,一般都会导致内存的重新分配和已有元素的全部复制一次。var a = []int{1, 2, 3} a = append([]int{0}, a...) // 在开头添加一个元素
- 中间插入
var a = []int{1, 2, 3} a = append(a, 0) // 扩充空间 copy(a[i+1:], a[i]) // a[i:] 向后移动一个元素 a[i] = 10 // 设置新的元素
- 开头插入
-
slice 删除元素
- 开头或者结尾删除
var a = []int{1, 2, 3} a = a[1:] // 开头删除 a = a[:len(a)-1] // 结尾删除
- 中间删除
var a = []int{1, 2, 3, 4, 5, 6} a = a[:2 + copy(a[2:], a[2+1:])] // 删除第2个元素, 公式: a[: i + copy(a[i:], a[i+1:])] fmt.Println("a: ", a) // a: [1 2 4 5 6] var b = []int{1, 2, 3, 4, 5, 6} b = b[:1 + copy(b[1:], b[1+3:])] // 删除[1, 1+3)元素, 公式: b[: i + copy(b[i:], b[i+N:])] fmt.Println("b: ", b) // a: [1 2 4 5 6]
- 开头或者结尾删除
-
slice 的排序和查找
还有slice的其他操作可参考:go slice 操作常用技巧
map
在Go语言种,map是散列表的引用,map的类型是map[K]V
,其中K和V是字典的键和值对应的数据类型,键的类型K,必须是可以通过操作符==
来进行比较的数据类型。
-
创建
// 使用字面量的方式创建 ages := map[string]int{ "alice": 34, "charlie": 34, } ages := make(map[string]int)
内置函数
len
返回当前键值对数量,cap
不支持 map 类型-
遍历
for name, age : range ages { fmt.Printf("%s\t%d\n", name, age) }
map中的元素的迭代顺序是不固定的,每次遍历都可能得到不同的元素的顺序。
func main() { m := map[int]int{1: 1, 2: 2, 3: 3, 4: 4, 5:5, 6:6, 7:7} print(m) time.Sleep(time.Second * 5) fmt.Println("----------------") print(m) } func print(m map[int]int) { for k, v := range m { fmt.Println(k, v) } }
如果需要按照某种顺序来遍历map中的元素,那么可以将 key 放入 slice 中排序得到。
-
查找
通过下标操作访问map中的元素,会有2种返回值类型:age := ages["bob"]
这种将会返回value,如果key不存在,则value会是零值。
再或者额外返回一个bool值,用来表示该key是否存在
age, exist := ages["bob"]; if exist { //.... }
返回的value 是一份拷贝,对其修改并不会改变 map 中对应的 value。
-
添加
age := ages["bob"]
注意:map的零值是
nil
,表示没有引用任何散列表,向零值map设置元素会导致panic。 -
修改
修改和添加类似,也是用下标访问key直接修改value,但需要注意的是,当key不存在时,会直接创建key,value 则为零值age := ages["bob"] // bob 不存在,将会直接创建
-
删除
使用内置函数delete
,来从字典中根据键值移除元素,健不存在时,不会报错。delete(ages, "alice")
-
快速清空map元素
m = make(map[T1]T2) // 直接make一个新的map,原来的map由gc回收。
-
map 无法比较
和slice一样,map无法比较,唯一合法地就是和nil
比较。var a map[string]int // a is nil len(a) == 0 a = nil // a is nil len(a) == 0 c := make(map[string]int) // c is not nil len(c) == 0 d := map[string]int{} // c is not nil len(c) == 0
如果我们想要比较2个map,则可以采用如下实现:
func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { // 这里不能直接写为:xv != y[k] return false } } return true } equal(map[string]{"A": 0}, map[string]int{"B": 42})
-
map 的并发安全性
map 不是并发安全的,所以多协程下读写需要加锁。 这里提一个比较少见的但容易犯错的场景:var m = map[string]int{} var lock sync.Mutex func write() { for i := 0; i != 1000000; i++ { time.Sleep(time.Microsecond * 100) lock.Lock() m[tool.GenerateRandomString(5)] = tool.GenerateRandom(10, 100) lock.Unlock() } } func read() { for i := 0; i != 1000000; i++ { time.Sleep(time.Microsecond * 200) fmt.Println(fmt.Sprintf("%+v", m)) // +v 会遍历map,这里会造成map的并发读写,进而panic } } func main() { go write() go read() select {} }
Set
go 中自带的标准库只有map
,而没有set
,那么我们可以利用 map
和 struct{}
size 为0 来简单封装实现一个 set
package main
import "fmt"
type Set map[string]struct{}
func NewSet() Set {
s := make(map[string]struct{})
return Set(s)
}
func NewSetWithInitSize(size int) Set {
s := make(map[string]struct{}, size)
return Set(s)
}
func (s Set) Has(key string) bool {
_, exist := s[key]
return exist
}
func (s Set) Add(key string) {
s[key] = struct{}{}
}
func (s Set) Delete(key string) {
delete(s, key)
}
func main() {
s := NewSet()
s.Add("hello")
s.Add("world")
s.Add("go")
exist := s.Has("hello")
if exist {
fmt.Println("exist")
}
exist = s.Has("world")
if exist {
fmt.Println("exist")
}
s.Delete("go")
exist = s.Has("go")
if exist {
fmt.Println("exist")
} else {
fmt.Println("not exist")
}
}
更加完备的set,可以参考开源:golang-set
指针
获取一个变量的指针非常容易,使用取地址符 &
就可以
name := "hello world"
nameP := &name
指针的操作
在 Go 语言中指针的操作无非是两种:一种是获取指针指向的值,一种是修改指针指向的值。-
什么时候使用指针?
- 不要对 map、slice、channel 这类引用类型使用指针;
- 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
- 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
- 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
Go语言中的 new 和 make 的区别
TODO