rune类型
Go 语言的字符有以下两种:
uint8
类型,或者叫 byte 型,代表了ASCII码
的一个字符。rune
类型,代表一个UTF-8字符
。
当需要处理中文、日文或者其他复合字符时,则需要用到rune
类型。rune
类型实际是一个int32
。
Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。
// 遍历字符串
func traversalString() {
s := "hello沙河"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}
输出:
104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³)
104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)
因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。
字符串底层是一个byte数组,所以可以和[]byte
类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。
修改字符串
要修改字符串,需要先将其转换成[]rune
或[]byte
,完成后再转换为string
。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
运算符
位运算符对整数在内存中的二进制位进行操作。
运算符 | 描述 |
---|---|
& | 参与运算的两数各对应的二进位相与。 (两位均为1才为1) |
| | 参与运算的两数各对应的二进位相或。 (两位有一个为1就为1) |
^ | 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (两位不一样则为1) |
<< | 左移n位就是乘以2的n次方。 “a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。 |
>> | 右移n位就是除以2的n次方。 “a>>b”是把a的各二进位全部右移b位。 |
switch case
使用switch
语句可方便地对大量的值进行条件判断。
func switchDemo1() {
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("无效的输入!")
}
}
Go语言规定每个switch
只能有一个default
分支。
一个分支可以有多个值,多个case值中间使用英文逗号分隔。
func testSwitch3() {
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
}
分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。例如:
func switchDemo4() {
age := 30
switch {
case age < 25:
fmt.Println("好好学习吧")
case age > 25 && age < 35:
fmt.Println("好好工作吧")
case age > 60:
fmt.Println("好好享受吧")
default:
fmt.Println("活着真好")
}
}
fallthrough
语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。
func switchDemo5() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("...")
}
}
流程控制for循环
goto
使用goto
语句能简化代码:
func gotoDemo2() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
goto breakTag
}
fmt.Printf("%v-%v
", i, j)
}
}
return
// 标签
breakTag:
fmt.Println("结束for循环")
}
Array(数组)
数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。 基本语法:
// 定义一个长度为3元素类型为int的数组a
var a [3]int
数组定义:
var 数组变量名 [元素数量]T
比如:var a [5]int
, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。 [5]int
和[10]int
是不同的类型。
var a [3]int
var b [4]int
a = b //不可以这样做,因为此时a和b是不同的类型
数组的初始化
数组的初始化也有很多方式。
方法一
初始化数组时可以使用初始化列表来设置数组元素的值。
func main() {
var testArray [3]int //数组会初始化为int类型的零值,[0,0,0]
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化[1,2,0]
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
}
方法二
按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
}
方法三
我们还可以使用指定索引值的方式来初始化数组,例如:
func main() {
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
}
数组的遍历
遍历数组a有以下两种方法:
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}
注意: 多维数组只有第一层可以使用...
来让编译器推导数组长度。
数组是值类型
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
- 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
[n]*T
表示指针数组,*[n]T
表示数组指针 。
切片
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
func main() {
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
// fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和nil比较
}
注意:切片只能和nil比较。
对切片再执行切片表达式时(切片再切片),high
的上限边界是切片的容量cap(a)
,而不是长度。如果索引在运行时超出范围,就会发生运行时panic
。
容量
切片从左指针到上界(max)所能包含元素的个数便是切片的容量。
使用make()函数构造切片
我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()
函数,格式如下:
make([]T, size, cap)
其中:
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
,切片s1 := a[:5]
,相应示意图如下。切片s2 := a[3:6]
,相应示意图如下:
判断切片是否为空
要检查切片是否为空,请始终使用len(s) == 0
来判断,而不应该使用s == nil
来判断。
切片的赋值拷贝
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
append()方法为切片添加元素
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}
注意:通过var声明的零值切片可以在append()
函数直接使用,无需初始化。
var s []int
s = append(s, 1, 2, 3)
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
总结:
-
append()
函数将元素追加到切片的最后并返回该切片。 -
切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。
-
append()函数还支持一次性追加多个元素。
切片的扩容策略
可以通过查看$GOROOT/src/runtime/slice.go
源码,其中扩容相关代码如下:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
copy函数
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
copy(destSlice, srcSlice []T)
其中:
- srcSlice: 数据来源切片
- destSlice: 目标切片
从切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
总结一下就是:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
map
map是一种无序的基于key-value
的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
map定义
Go语言中 map
的定义语法如下:
map[KeyType]ValueType
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
map也支持在声明的时候填充元素,例如:
func main() {
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
判断某个键是否存在
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]
使用delete()函数删除键值对
使用delete()
内建函数从map中删除一组键值对,delete()
函数的格式如下:
delete(map, key)
函数
可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识。
注意:可变参数通常要作为函数的最后一个参数。
返回值
多返回值
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()
将所有返回值包裹起来。
举个例子:
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
返回值补充
当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}
局部变量
如果局部变量和全局变量重名,优先访问局部变量。
函数类型与变量
定义函数类型
我们可以使用type
关键字来定义一个函数类型,具体格式如下:
type calculation func(int, int) int
上面语句定义了一个calculation
类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
add和sub都能赋值给calculation类型的变量。
var c calculation
c = add
函数作为返回值
函数也可以作为返回值:
func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}
匿名函数
函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
func(参数)(返回值){
函数体
}
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
匿名函数多用于实现回调函数和闭包。
闭包
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
。
defer语句
Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
举个例子:
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
内置函数介绍
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
panic/recover
Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover
模式来处理错误。 panic
可以在任何地方引发,但recover
只有在defer
调用的函数中有效。 首先来看一个例子:
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
输出:
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
.../code/func/main.go:12
main.main()
.../code/func/main.go:20 +0x98
程序运行期间funcB
中引发了panic
导致程序崩溃,异常退出了。这个时候我们就可以通过recover
将程序恢复回来,继续往后执行。
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
注意:
recover()
必须搭配defer
使用。defer
一定要在可能引发panic
的语句之前定义。
指针
安全指针
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
Go语言中的指针
任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。
比如,“永远不要高估自己”这句话是我的座右铭,我想把它写入程序中,程序一启动这句话是要加载到内存(假设内存地址0x123456),我在程序中把这段话赋值给变量A
,把内存地址赋值给变量B
。这时候变量B
就是一个指针变量。通过变量A
和变量B
都能找到我的座右铭。
Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&
字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int
、*int64
、*string
等。
取变量指针的语法如下:
ptr := &v // v的类型为T
其中:
- v:代表被取地址的变量,类型为
T
- ptr:用于接收地址的变量,ptr的类型就为
*T
,ptr称做T的指针类型。*代表指针。
举个例子:
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p
", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T
", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
我们来看一下b := &a
的图示:
指针取值
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
两个重要的概念:
- 指针地址
&
- 地址的值
*
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
c := *b // 指针取值(根据指针去内存取值)
}
总结:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
new和make
我们先来看一个例子:
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["沙河娜扎"] = 100
fmt.Println(b)
}
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存。
new
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
其中,
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T
", a) // *int
fmt.Printf("%T
", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
make
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["沙河娜扎"] = 100
}
new与make的区别
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
类型别名和自定义类型
自定义类型
在Go语言中有一些基本的数据类型,如string
、整型
、浮点型
、布尔
等数据类型, Go语言中可以使用type
关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将MyInt定义为int类型type MyInt int
通过type
关键字的定义,MyInt
就是一种新的类型,它具有int
的特性。
类型别名
类型别名是Go1.9
版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。
type TypeAlias = Type
我们之前见过的rune
和byte
就是类型别名,他们的定义如下:
type byte = uint8type rune = int32
注意:自定义类型只会在代码中存在,编译完成时并不会有自定义类型。
结构体
Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体(struct),Go语言中通过结构体来实现面向对象。
结构体的定义
使用type
和struct
关键字来定义结构体,具体代码格式如下:
type 类型名 struct { 字段名 字段类型 字段名 字段类型 …}
其中:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var
关键字声明结构体类型。
var 结构体实例 结构体类型
基本实例化
举个例子:
type person struct { name string age int8}func main() { var p1 person p1.name = "沙河娜扎" p1.age = 18}
我们通过.
来访问结构体的字段(成员变量),例如p1.name
和p1.age
等。
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
package main import ( "fmt") func main() { var user struct{Name string; Age int} user.Name = "小王子" user.Age = 18}
创建指针类型结构体
我们还可以通过使用new
关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var p2 = new(person)
取结构体的地址实例化
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作。
p3 := &person{}fmt.Printf("p3=%#v
", p3) //p3=&main.person{name:"", city:"", age:0}p3.name = "七米"p3.age = 30
使用键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
也可以对结构体指针进行键值对初始化,例如:
p6 := &person{ name: "小王子", age: 18,}
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{ "沙河娜扎", 28,}
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
结构体内存布局
结构体占用一块连续的内存。空结构体是不占用空间的。
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
func newPerson(name, city string, age int8) *person { return &person{ name: name, city: city, age: age, }}
调用构造函数
p9 := newPerson("张三", "沙河", 90)
方法和接收者
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体}
其中:
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
//Person 结构体type Person struct { name string age int8}//NewPerson 构造函数func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, }}//Dream Person做梦的方法func (p Person) Dream() { fmt.Printf("%s的梦想是学好Go语言!
", p.name)}func main() { p1 := NewPerson("小王子", 25) p1.Dream()}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this
或者self
。 例如我们为Person
添加一个SetAge
方法,来修改实例变量的年龄。
// 使用指针接收者func (p *Person) SetAge(newAge int8) { p.age = newAge}
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄// 使用值接收者func (p Person) SetAge2(newAge int8) { p.age = newAge}func main() { p1 := NewPerson("小王子", 25) p1.Dream() p1.SetAge2(30) // (*p1).SetAge2(30)}
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
//Address 地址结构体type Address struct { Province string City string}//User 用户结构体type User struct { Name string Address Address}func main() { user1 := User{ Name: "小王子", Address: Address{ Province: "山东", City: "威海", }, }}
嵌套匿名结构体
//Address 地址结构体type Address struct { Province string City string}//User 用户结构体type User struct { Name string Address //匿名结构体}func main() { var user2 User user2.Name = "小王子" user2.Address.Province = "山东" //通过匿名结构体.字段名访问 user2.City = "威海" //直接访问匿名结构体的字段名}
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTimeuser3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//Animal 动物type Animal struct { name string}func (a *Animal) move() { fmt.Printf("%s会动!
", a.name)}//Dog 狗type Dog struct { Feet int8 *Animal //通过嵌套匿名结构体实现继承}func (d *Dog) wang() { fmt.Printf("%s会汪汪汪~
", d.name)}func main() { d1 := &Dog{ Feet: 4, Animal: &Animal{ //注意嵌套的是结构体指针 name: "乐乐", }, } d1.wang() //乐乐会汪汪汪~ d1.move() //乐乐会动!}
结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。
结构体标签(Tag)
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意事项: 为结构体编写Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
补充
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。我们来看下面的例子:
type Person struct { name string age int8 dreams []string}func (p *Person) SetDreams(dreams []string) { p.dreams = dreams}func main() { p1 := Person{name: "小王子", age: 18} data := []string{"吃饭", "睡觉", "打豆豆"} p1.SetDreams(data) // 你真的想要修改 p1.dreams 吗? data[1] = "不睡觉" fmt.Println(p1.dreams) // ?}
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
func (p *Person) SetDreams(dreams []string) { p.dreams = make([]string, len(dreams)) copy(p.dreams, dreams)}
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。
包
匿名导入包
如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下:
import _ "包的路径"
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。
init()初始化函数
init()函数介绍
在Go语言程序执行时导入包语句会自动触发包内部init()
函数的调用。需要注意的是: init()
函数没有参数也没有返回值。 init()
函数在程序运行时自动被调用执行,不能在代码中主动调用它。
包初始化执行的顺序如下图所示:
init()函数执行顺序
Go语言包会从main
包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最先初始化并调用其init()
函数, 如下图示:
接口
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface
是一组method
的集合,是duck-type programming
的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
为什么要使用接口
type Cat struct{}func (c Cat) Say() string { return "喵喵喵" }type Dog struct{}func (d Dog) Say() string { return "汪汪汪" }func main() { c := Cat{} fmt.Println("猫:", c.Say()) d := Dog{} fmt.Println("狗:", d.Say())}
上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?
接口的定义
Go语言提倡面向接口编程。
每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 方法名2( 参数列表2 ) 返回值列表2 …}
其中:
- 接口名:使用
type
将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
Go语言中有对指针类型变量求值的语法糖,dog指针fugui
内部会自动求值*fugui
。
接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
// Sayer 接口type Sayer interface { say()}// Mover 接口type Mover interface { move()}// 接口嵌套type animal interface { Sayer Mover}
嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:
type cat struct { name string}func (c cat) say() { fmt.Println("喵喵喵")}func (c cat) move() { fmt.Println("猫会动")}func main() { var x animal x = cat{name: "花花"} x.move() x.say()}
空接口
空接口的定义
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
func main() { // 定义一个空接口x var x interface{} s := "Hello 沙河" x = s fmt.Printf("type:%T value:%v
", x, x) i := 100 x = i fmt.Printf("type:%T value:%v
", x, x) b := true x = b fmt.Printf("type:%T value:%v
", x, x)}
空接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数func show(a interface{}) { fmt.Printf("type:%T value:%v
", a, a)}
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值 var studentInfo = make(map[string]interface{}) studentInfo["name"] = "沙河娜扎" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo)
类型断言
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
接口值
一个接口的值(简称接口值)是由一个具体类型
和具体类型的值
两部分组成的。这两部分分别称为接口的动态类型
和动态值
。
我们来看一个具体的例子:
var w io.Writerw = os.Stdoutw = new(bytes.Buffer)w = nil
请看下图分解:
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
- x:表示类型为
interface{}
的变量 - T:表示断言
x
可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
如果要断言多次就需要写多个if
判断,这个时候我们可以使用switch
语句来实现:
func justifyType(x interface{}) { switch v := x.(type) { case string: fmt.Printf("x is a string,value is %v
", v) case int: fmt.Printf("x is a int is %v
", v) case bool: fmt.Printf("x is a bool is %v
", v) default: fmt.Println("unsupport type!") }}
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
注意:
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
- 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
- 大量使用反射的代码通常难以理解。
- 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。
并发
Go语言的并发通过goroutine
实现。goroutine
类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine
并发工作。goroutine
是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
Go语言还提供channel
在多个goroutine
间进行通信。goroutine
和channel
是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。
goroutine
Go语言中的goroutine
就是这样一种机制,goroutine
的概念类似于线程,但 goroutine
是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能——goroutine
,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine
去执行这个函数就可以了,就是这么简单粗暴。
使用goroutine
Go语言中使用goroutine
非常简单,只需要在调用函数的时候在前面加上go
关键字,就可以为一个函数创建一个goroutine
。
一个goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数。
func main() { go hello() // 启动另外一个goroutine去执行hello函数 fmt.Println("main goroutine done!") time.Sleep(time.Second)}
执行上面的代码你会发现,这一次先打印main goroutine done!
,然后紧接着打印Hello Goroutine!
。
首先为什么会先打印main goroutine done!
是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine
是继续执行的。
启动多个goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine
。让我们再来一个例子: (这里使用了sync.WaitGroup
来实现goroutine的同步)
var wg sync.WaitGroupfunc hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println("Hello Goroutine!", i)}func main() { for i := 0; i < 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束}
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine
是并发执行的,而goroutine
的调度是随机的。
goroutine与线程
可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine
的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine
的栈不是固定的,他可以按需增大和缩小,goroutine
的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine
也是可以的。
goroutine调度
GPM
是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
G
很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。P
管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。M(machine)
是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
P的个数是通过runtime.GOMAXPROCS
设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
go语言的优势
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine
则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
func a() { for i := 1; i < 10; i++ { fmt.Println("A:", i) }}func b() { for i := 1; i < 10; i++ { fmt.Println("B:", i) }}func main() { runtime.GOMAXPROCS(1) go a() go b() time.Sleep(time.Second)}
两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。
func a() { for i := 1; i < 10; i++ { fmt.Println("A:", i) }}func b() { for i := 1; i < 10; i++ { fmt.Println("B:", i) }}func main() { runtime.GOMAXPROCS(2) go a() go b() time.Sleep(time.Second)}
Go语言中的操作系统线程和goroutine的关系:
- 一个操作系统线程对应用户态多个goroutine。
- go程序可以同时使用多个操作系统线程。
- goroutine和OS线程是多对多的关系,即m:n。