Go语言学习笔记

环境配置

GOROOT 用于存放标准库
GOPATH 用于存放非标准库

GOPATH目录

路径 说明
src 用于存放源代码,是开发程序的主要目录(Go使用utf-8字符集)
pkg 编译后生成的文件
bin 编译后生成的可执行文

package. main表示可执行程序 其他表示是应用包

包(package)

Go使用package来组织代码,其中main.main()是每个可运行程序的入口

声明

1
package pkgName

上面告诉我们当前文件属于哪个包

新建包

新建应用或代码包时在src目录下新建一个文件夹,文件夹名称一般是代码包名称。

main包

每个可独立运行的Go程序,必定包含一个package main的文件,在这个文件中必定包含一个入口函数main,而这个函数既没有参数也没有返回值。
main包在编译后产生可执行文件,其他包最后都会生成*.a文件并放置在$GOPATH/kpg/xxx中

包文件组织结构

一个目录一个package,等同于namespace
一个目录下所有文件(子目录除外)属于同一个package
20171027150903524169378.png

go 开发工具

go install

编译并安装,这个命令在内部实际分成两步:

  1. 第一步生成结果文件件(可执行文件或.a包)
  2. 第二步把编译好的结果移到$GOPATH/pkg或者$GOPATH/bin
    1
    2
    3
    4
    5
    6
    7
    go install <package>
    -i 同时安装依赖包
    安装操作会先执行构建,然后还会进行链接操作,并且把结果文件搬运到指定目录。
    如果安装的是库源码文件,那么结果文件会被搬运到它所在工作区的 pkg 目录下的某个子目录中。
    如果安装的是命令源码文件,那么结果文件会被搬运到它所在工作区的 bin 目录中,或者环境变量GOBIN指向的目录中。

go build 编译

go build -o output <package>

可选项:

  • -gcflags 向编译器传入参数,也就是传给 go tool compile的参数,可通过go tool compile --help查看所有可用参数
    • -N 禁止编译优化(通常用于delve或gdb时,不禁止编译优化可能会观察到奇怪的现象)
    • -l 禁止内联
  • -ldflags 向链接器传入参数,实际是给go tool link的参数,可用go tool link --help查看可用的参数
  • -n 把需要执行的编译命令打印出来,但不执行
  • -x 把需要执行的编译命令打印出来,并执行
  • -tags 'tag list' 设置在编译时使用哪些tag编译

<package>可以是包的绝对路径或相对gopath/src的路径
默认当前目录生成output,output可以是绝对路径

示例

1
2
$ go build -gcflags="-N -l" //关闭编译器优化及内联
$ go build -gcflags="-m -m" //常用于查看逃逸信息

go build 会忽略目录下以”_”或”.”开头的go文件

go build时会选择性地编译以系统名结尾的文件(linux/darwin/windows/freebsd),例如linux系统下面编译只会选择array_linux.go文件

go clean

go clean用来移除当前源码包和关联源码包编译生成的文件
go clean -i -n -r

1
2
3
4
-i 清除关联的安装的包和运行文件
-n 把需要执行的清除命令打印出来,但是不执行
-r 循环清除在import中引入的包
-x 打印出来执行的详细命令

1
2
3
4
5
6
7
$ pwd
/Users/sunxiangke/project/go/src/local
$ go clean -i -n
cd /Users/sunxiangke/project/go/src/local
rm -f local local.exe local.test local.test.exe demo demo.exe
rm -f /Users/sunxiangke/project/go/bin/local

go get

1
2
3
4
5
6
7
8
9
// `go get`动态获取代码
go get -u -v <package>
-u 更新包及依赖包
-v 显示详细信息
-d 只下载不安装
-t 同时下载测试所需的代码包。
-insecure:允许通过非安全的网络协议(比如HTTP)下载和安装代码包
归档文件在Linux下就是扩展名是.a的文件,也就是archive文件(这是程序编译后生成的静态库文件)

当有多个GOPATH时,默认会将go get的内容放在第一个GOPATH目录下

go test 单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
# 自动读取源码目录下的*_test.go文件,并生成测试用的可执行文件
go test
-v //详细输出
-race //检查竞争
-cover //开启测试覆盖率
-coverprofile //输出测试覆盖概述,然后使用go工具在浏览器中可视化结果
# 测试单个文件
$ go test -v app_test.go
# 测试单个文件的指定方法
$ go test -v app_test.go -test.run TestRunDemo
$ go test -coverprofile=c.out && go tool cover -html=c.out

其他

go tool

go tool下聚集了很多命令

  • go tool vet directory|files 语法检查
go fmt

格式化文件代码
go fmt filename.go

go generate

这个命令是go1.4才开始设计的,用于在编译前自动化生成某类代码

go generate和go build是完全不一样的命令,通过分析源码中特殊的注释,然后执行响应的命令。
这里大家要注意,go generate是给包开发者自己用的,不是给包使用者用的

这里举个简单的例子,如生成辅助的helper.txt文件,在任意一个go文件任意一行增加如下配置

1
//go:generate touch helper.txt

触发generate

1
2
3
$ go generate
$ ls -l
helper.txt

1
2
3
4
go version
go env 查看当前go的环境变量
go list 列出当前全部安装的package
go run 编译并运行go程序

Go程序设计的一些规则

1.大写字母开头的变量是可导出的,也就是其他包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量
2.大写字母开头的函数也是一样,相当于class中的public关键字的公有函数;小写字母开头的就是私有函数

go doc

本地开启http godoc, 访问127.0.0.1:6060

1
$ godoc -http=:6060

常量

所谓常量就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。

Go中常量可以定义为数值、布尔值或字符串等类型

定义

1
2
3
4
const constantName = value
//如果需要,也可以明确指定常量的类型
const Pi float32 = 3.14
const prefix = "xiangke_"

变量

定义变量

1
var vname1,vname2,vname3 type = v1,v2,v3

等于简短声明

1
vname1,vname2,vname3 := v1,v2,v3

:=取代了var和type
:=只能在函数内部使用,在外部无法编译通过,所以一般用var 来定义全局变量

_是个特殊变量名,任何赋予它的值将被抛弃

1
_,b := 34,35

Go对于已声明但未使用的变量会在编译阶段报错
例如

1
2
3
4
5
package main
func main(){
var i int
}

格式化输出变量

语法 说明 示例
%v 默认格式化 {张三 12}
%+v 显示成员 {Name:”张三”, Age:12}
%#v go语法格式化 main.Student{Name:”张三”, Age:12}
%T 值的类型 main.Student

整数(缩进/进制/符号)

语法 说明 示例
%d 十进制 15
%+d 带有符号的十进制 +15
%b 二进制 1111
%o 八进制 17
%x 16进制小写 f
%X 16进制大写 F
%#x 带有0x前缀16的进制 0xf
%4d 左侧空格填充至4字符 ␣␣15
%-4d 右侧空格填充至4字符 15␣␣
%04d 用零填充至4字符 0015

浮点数(缩进/精度/科学计数法)

语法 说明 示例
%e 十进制 1.234560e+02
%f 带有符号的十进制 123.456000
%.2f 默认宽度,2位小数点 123.46
%8.2f 宽度为8,2位小数点 ␣␣123.46

字符串或字节切片(引号/缩进/十六进制)

语法 说明 示例
%s 纯字符串 café
%6s 宽度为6的字符串,左侧填充 ␣␣café
%-6s 宽度为6的字符串,右侧填充 café␣␣
%q 带有引号的字符串 “café”
%x 字节值的16进制转储 636166c3a9
% x 字节值的16进制转储 带有空格 63 61 66 c3 a9

字符(引号/unicode)

语法 说明 示例
%c 字符 A
%q 带引号的字符 ‘A’
%U Unicode U+0041
%#U Unicode 以及 字符 U+0041 ‘A’

布尔

语法 说明 示例
%t 格式化为true 或false True

指针

语法 说明 示例
%p 输出地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//打印变量类型 https://golang.org/pkg/fmt/
fmt.Printf("%T",v) 打印变量类型
fmt.Printf("%#v",v) 该值的go语法表现形式
fmt.Printf("%+v",v) 结构体信息打印
fmt.Println(reflect.TypeOf(v)) 打印该值类型
type Person struct {
name string
}
func main() {
a := Person{name: "nihao"}
fmt.Printf("%#v \n%+v \n%v \n%T", a, a, a, a)
}
output:
main.Person{name:"nihao"}
{name:nihao}
{nihao}
main.Person

类型

类型
Boolean 值类型
string 值类型
数组array 值类型
整型/浮点型 值类型
struct 值类型
Slice 引用类型
map 引用类型
Channel 引用类型

值类型

Boolean

布尔类型为bool,值是true或false,默认false

注意: 不可以用数字来代表true/false

1
var isActive bool = false

数值类型

  • 整数类型
    int rune int8 int16 int32 int64 byte uint8 uint16 uint32
    ,其中uint64 rune是int32的别称,byte是uint8的别称
    • rune表示一个unicode码点(code point)
    • byte强调这是一个原始数据

int根据不同平台变化,32位平台就是32位的,64位平台就是64位

  • 浮点数类型

  • 复数类型

注意:
各类型变量之间不允许赋值和操作,会在编译阶段报错
尽管int和int32都是32位,但不能互用

字符串

Cheat sheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
字符串使用一对""``标识,类型为string
var str string
var emptystring = ""
//在Go中字符串类型是不可变的,例如下列代码编译时会报错:cannot assign to s[0]
var s string = "hello"
s[0] = 'c'
//真要修改字符串,通常是转换为slice类型,对slice类型操作,再转换回string
s := "hello"
c := []byte(s)
c[0] = 'c'
s2 := string(c) s2: cello
//连接字符串(连接其他类型需要先使用strconv转换)
a := "hello"
b := " world"
c := a + b c: hello world
//截取字符串
s := "hello"
s2:= s[:len(s)-1] s2: hell 字符串类型
//获取字符串长度
len("日") //3
utf8.RuneCountInString("日") //1
比较
1
2
"Japan" == "Japan" //true
strings.EqualFold("Japan", "JAPAN") //true unicode忽略大小写
遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//遍历字符串 方法1
str := "Go爱好者"
for i, c := range str {
fmt.Printf("%d: %q [% x]\n", i, c, []byte(string(c)))
}
输出:
0: 'G' [47]
1: 'o' [6f]
2: '爱' [e7 88 b1]
5: '好' [e5 a5 bd] //index是按照字节长度来算字符首字节所在位置,所以是5
8: '者' [e8 80 85]
解释下为什么不推荐用`for i:=0;i<len(s);i++`方式来遍历字符串
go中使用utf8编码存储数据,utf8下存放一个ASCII字符需要1个字节,而存放一个非ASCII字符则需要2-4个字节,是不定长的。
如果想使用for...len方式 应该先转化为[]rune,见方法2
//遍历字符串 方法2
a:=[]rune("你好世界")
for i:=0;i<len(a);i++{
fmt.Printf("%d %c", i, a[i])
}

数组

在go中数组是值类型,数组声明后,它的数据类型和长度都不能再改变,如果需要更多元素,那么只能声明新的数组,然后将原数组内容拷贝过去。
数组可以使用==!=,但是不可以使用<>

声明和初始化

1
2
3
4
5
6
7
8
9
10
11
12
//声明数组
var a [3]int
a:=new([3]int)
//声明并定义数组
b := [3]int{1,2,3}
c := [10]int{1,2,3} //数组长度为10
d := [...]int{4,5,6} //Go自动计算元素个数
e := [5]int{4:1} //设定指定位置的值
doubleArray := [2][1]int{[2]int{1,2},[2]int{3,4}} //二维数组
easyArray := [2][2]int{{{1,2,3},{4,5,6}}} //二维数组简单写法

使用数组

1
2
3
4
5
6
7
8
9
10
11
12
13
//访问数组
a[0] = 1
注意不能像php一样使用a[] = 1,会报错
//计算数组长度
len(a) //3
len(b) //3
len(c) //10
len(easyArray) //2
len(easyArray[0]) //3
//其他
e := [2]string{"hello","world"} //类型可以是string

在函数中传递数组

在函数中传递数组是非常昂贵的行为,因为函数间传递变量永远是值传递,所以如果变量是数组,那么意味着整个数组的复制。

举个例子,传建一个有百万元素的数组,在64位机器上它需要8M的空间,来看看我们声明它和传递它时发生了什么

1
2
3
4
5
var array [1e6]int
foo(array)
func foo(array [1e6]int) {
...
}

每一次foo被调用,8M内存将会被分配在栈上,一旦函数返回,会弹栈并释放内存,每次都要8M空间。

更好的方法是使用slice。

联想

  1. 能不能定义一个二维数组,使得外层与内层数据类型不统一?如doublearray = [2][3]int{[2]float32{1.1, 1.2}, [2]float32{2.1, 2.2}}
    答: 不能,会直接报错 cannot use [2]float32 literal (type [2]float32) as type [2]int in array or slice literal

struct类型

struct也是一个值类型

结构体T的指针*T也可以像T一样,使用.访问成员

匿名字段

只提供字段类型的字段就是匿名字段,也叫嵌入字段。

  • 匿名字段等同于字段名和类型相同的一个字段,比如嵌入字段int表示字段名及类型为int的字段

  • 当匿名字段是一个struct时,该struct的所有字段及方法被隐式地引入了当前定义的struct中

  • 可以覆盖匿名字段的方法

  • 所有值类型和自定义类型都可以作为匿名字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type T struct {
    int
    }
    func main() {
    a := new(StructAnonymousFieldType)
    a.int = 1
    fmt.Println(a.int)
    }
demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//使用示例
type Skills []string
type Person struct{
name string
age int
}
type Student struct{
Person //匿名字段 struct
no int
age int //实现对Person中age字段的重载
Skills //匿名字段 自定义类型
}
//初始化方式1
var a Student
a.name="xiaoming"
a.age = 18
a.no = 10000
a.Skills = []string{"go","mysql"}
//初始化方式2
var b Student
b.Person = Person{"xiaoming",18}
b.no = 10000
b.Skills = []string{"go","mysql"}
//初始化方式3
c:=Student{Person:Person{"xiaoming",11},no:10000,Skills:[]string{"go","mysql"}}
//初始化方式4
d:=Student{Person{"xiaoming",11},10000,[]string{"go","mysql"}}
//初始化方式5
var f *Student = &Student{}
或者
f:=&Student{}
f.name="xiaoming"
f.age=18
//访问
name:=a.Person.name //通过匿名字段访问
e.Person.age+=1
var f:=Student{Person{"xiaoming",10},10000,18}
fmt.Println(f.Person.age) //f.Person.age: 10
fmt.Println(f.age) //f.age: 18 对age重载
结构体成员访问顺序

如果多个匿名字段有相同的成员,访问结构体成员时需要显示表明匿名字段

同名的结构体成员存在覆盖的现象
访问优先级 当前对象成员—> 匿名函数成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Teacher struct{
}
func (t *Teacher)Say(){
fmt.Println("i am a teacher.")
}
type Father struct{
}
func (f *Father)Say(){
fmt.Println("i am a father.")
}
type MaleTeacher struct{
Teacher
Father
}
func (f *MaleTeacher)Say(){
fmt.Println("i am a malefather.")
}
// 匿名字段
func TestAnomynousField(t *testing.T) {
m:=MaleTeacher{}
m.Teacher.Say() //i am a teacher.
m.Say() //i am a malefather.
}

iota枚举

关键字iota,用来声明枚举类型,默认值是0,const每增加一行加1

1
2
3
4
5
6
7
8
const (h,i,j = iota,iota,iota) h:0 i:0 j:0
const (
a = iota a:0
b = "B" b:B
c = iota c:2
d,e,f = iota,iota,iota d:3 e:3 f:3
g = iota g:4
)

引用类型

引用类型和值类型恰恰相反,它的修改可以影响到任何引用到它的变量。在Go语言中,引用类型有切片、map、interface、函数类型以及chan。

引用类型之所以可以引用,是因为我们创建引用类型的变量,其实是一个标头值,标头值里包含一个指针,指向底层的数据结构,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层结构并没有被复制传递,这也是引用类型传递高效的原因。

本质上,我们可以理解函数的传递都是值传递,只不过引用类型传递的是一个指向底层数据的指针

1
2
3
4
5
6
7
8
9
10
func main() {
ages := map[string]int{"张三": 20}
fmt.Println(ages) //map[张三:20]
modify(ages)
fmt.Println(ages) //map[张三:10]
}
func modify(m map[string]int) {
m["张三"] = 10
}

slice

很多时候我们并不知道需要多大的数组,所以需要一种大小动态变化的”动态数组”,在Go里这种数据结构就是slice

slice并不是真正意义上的动态数组,而是一个引用类型。
slice总是指向一个底层的array,slice的声明也可以像array一样,只是不用声明长度。
slice无需指明长度。

从概念上来讲slice像一个结构体,结构体包含三个元素:

  • 指针,指向数组中slcie指定的开始位置
  • 长度,即slice的长度
  • 最大长度,也就是slice开始位置到数组的最后位置的长度

slice与array的区别在于:

  • 声明数组时,方括号内需写明数组长度或使用…自动计算长度,而声明slice时,方括号内没有任何字符
  • slice默认开始位置是0,arr[:n]等于arr[0:n]
  • slice第二个序列默认是数组的长度,arr[n:]等于arr[n:len(arr)]
  • arr[:]等于arr[0:len(arr)]
声明定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//声明 (未初始化)
var fslice []int 声明fslice。相比array,slice声明不用指明长度
//声明并初始化 make(slice,len,cap)
fslice := make([]int,3)
//声明并初始化
slice := []byte{'a','b','c','d'} 声明并初始化数据,指向底层匿名数组
//从一个已存在数组中声明
var ar = [10]int{1,2,3,4,5}
r = ar[1:3] //r: []int{2,3}
//从一个已存在的字符串中声明
str3 := "hello world"
r := str3[0:5]
fmt.Println(r,reflect.TypeOf(r))//r: hello string
//多维slice
slice:=[][]int{{1},{1,2}}
a:=int{1,2,3}
fmt.Println(a[3:]) //slice:[]
fmt.Println(a[4:]) //panic: runtime error: slice bounds out of range

创建新切片的时候,最好让新切片的长度和容量一致,这样我们在追加操作时会生成新的底层数组(和原有数组分离),避免了因共用底层数组而引发奇奇怪怪的问题(共用数组修改内容,会影响多个切片)

1
2
3
4
5
6
7
8
var s []int
s[0] = 1 //s未初始化 值为nil 直接赋值会引发error
fmt.Println(s)
正确方式
s:=make([]int,1)
s[0] = 1
fmt.Println(s)
slice操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//计算slice长度
len(ar[1:3]) 2
len(ar[1:]) 9
//获取切片空间容量
cap(ar[1:3]) 9
cap(ar[1:]) 9
//append追加元素
slice1:=[]int{1,2,3}
slice2:=[]int{4,5,6}
slice3:=append(slice1,4,5,6) [1 2 3 4 5 6]
slice4:=append(slice1,slice2...) [1 2 3 4 5 6] 合并两个slice
var a []byte
a = append(a,"nihao"...)
//copy 将第二个slice里的元素拷贝到第一个中
s := []int{1,2,3}
copy(s,[]int{4,5,6,7,8,9}) s:[4 5 6]
//拷贝切片
s1:=[]byte("hello")
s2:=s1[:len(s)-1]
s2[0] = 'a' //此时s1的值也随之改动
如何将s2与s1完全分离呢?使用copy进行数据拷贝
s2:=make([]byte,len(s1))
copy(s2,s1)
s2[0] = 'a' //此时s2与s1独立,不会影响s1的值
//引用类型特性
var aslice,bslice []int
aslice = ar[1:3]
bslice = ar[2:4]
ar[2] = 5 aslice和bslice中的值都会变
//其他
bytes := []byte("hello") 强制类型转换
等同于
bytes := []byte{'h','e','l','l','o'}
len获取slice长度
cap获取slice最大容量
对于底层数组容量是k的切片slice[i:j]来说
长度:j-i
容量:k-i
copy(dst,src)
copy函数从slice的src中复制元素到目标dst,并且返回复制的元素的个数
a := []int{1, 2,3,4}
b := []int{6, 7, 8}
copy(a, b) //a:[6,7,8,4] b:[6,7,8]
从位置1开始复制
copy(a[1:],b) //a:[1,6,7,8] b:[6,7,8]
slice重组(reslice)
1
2
3
4
5
6
7
8
9
s:=make([]int,3,10) //s:[0,0,0]
for i:=0;i<3;i++{
s[i] = i
}
//s:[0,1,2]
s[3] = 3
# output:
panic: runtime error: index out of range

s的长度是3,无法使用s[3],所以报超出索引范围错误。
不过这时我们发现底层对应数组长度是10个,可不可以通过改变切片长度来达到目的呢?

1
s=s[:length]

这样就完成了对切片s长度的改变,也叫切片重组。
以上述例子为例,s=s[:len(s)+1],这样s长度变为4,s[3]就可以使用了。

其实append实现中就包含了slice重组逻辑

  • 如果底层数组足够大,reslice
  • 如果底层数组不够大,重新申请数组
append

append向slice中追加一个或多个元素,然后返回一个和slice同样类型的slice。append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其他slice。

当append后slice超过了cap(不是底层数组长度)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其他引用此数组的slice则不受影响

append函数会智能的增长底层数组的容量,目前的算法是:slice扩容

1
2
3
4
arr:=[5]int{1,2,3,4,5}
slice:=arr[0:3]
slice=append(slice,9) arr:[1 2 3 9 5] slice:[1 2 3 9]
slice=append(slice,7,7) arr:[1 2 3 9 5] slice:[1 2 3 9 7 7]
slice做函数参数

我们知道slice是3个字段构成的结构体类型,所以在函数间以值的形式传递的时候,占用的内存非常小,成本很低。在传递复制切片的时候,底层数据不会被复制,也不会受到影响,复制的只是切片本身。

1
2
3
4
5
6
7
8
9
10
11
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Printf("%p\n", &slice)
modify(slice)
fmt.Println(slice)
}
func modify(slice []int) {
fmt.Printf("%p\n", &slice)
slice[1] = 10
}

打印输出

0xc420082060
0xc420082080
[1 10 3 4 5]

仔细看这两个切片地址是不一样的,可以确认切片在函数间使用值传递。而我们修改一个索引的值后,发现原切片值也改了,说明他们共用一个底层数组。

在函数间传递切片的效率是很高的。这也是为什么函数间传递参数使用切片而不是数组的原因

空切片和nil切片

空切片和nil切片长度和容量都是0,但是它们指向底层数组的指针不一样,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是个地址。

nil切片表示不存在的切片,而空切片表示一个空集合,它们各有用处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建一个nil slice
var emptySlice []int
//创建一个空slice
nilSlice:=make([]int,0)
nilSlice:=[]int{}
if emptySlice == nil {
fmt.Println("emptySlice eq nil")
}
if nilSlice == nil {
fmt.Println("nilSlice eq nil ")
}
//输出 nilSlice eq nil

Map

map是一种无序的键值对的集合。map最重要的就是通过key来快速检索数据,key相当于索引,指向数据的值。

map也是一种引用类型,map的键可以是任意内置类型或struct类型,像切片、函数以及含有切片的结构类型就不能用于map的键了,因为他们具有引用的语义,不可比较。map的值可以是能够使用==操作的类型。

map与其他基本类型不同,他不是线程安全的,在多个协程存取时,必须使用Mutex lock机制

声明定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//声明后不初始化会创建nil map
//nil map不能操作存储键值对,如需存储键值对则必须初始化
//初始化就是为map开启一块可以存储数据的内存,目前有多种初始化方式
//方式1: a:=make(map[string]int)
//方式2: a:=map[string]int{}
var dict map[string]int
dict = make(map[string]int) //注意这里没有:
dict["张三"] = 43
//创建一个键为字符串值为接口的map
user := make(map[string]interface{})
user := map[string]interface{}{"name":"lilei","age":18}
像上述这种情况,由于map..太长了,通常我们会声明一个类型
type H map[string]interface{}
user:=H{"name":"lilei","age":18}
操作
1
2
3
4
5
6
7
8
9
10
11
12
13
//赋值
user["score"] = 100
//判断key是否存在
if v,ok:=user["score"];ok{
println(v)
}
在gomap中如果我们获取一个不存在的键的值,也是可以的,返回的是值类型的空值。
delete(user,"score") //删除索引one
//获取长度
fmt.Println(len(user))
map做函数参数

由于也是引用类型,所以同slice,也是十分廉价的

数组、slice和map小结

1.数组是 slice 和 map 的底层结构。
2.slice 是 Go 里面惯用的集合数据的方法,map 则是用来存储键值对。
3.内建函数 make 用来创建 slice 和 map,并且为它们指定长度和容量等等。slice 和 map 字面值也可以做同样的事。
4.slice 有容量的约束,不过可以通过内建函数 append 来增加元素。
5.map 没有容量一说,所以也没有任何增长限制。
6.内建函数 len 可以用来获得 slice 和 map 的长度。
7.内建函数 cap 只能作用在 slice 上。
8.可以通过组合方式来创建多维数组和 slice。map 的值可以是 slice 或者另一个 map。slice 不能作为 map 的键。
9.在函数之间传递 slice 和 map 是相当廉价的,因为他们不会传递底层数组的拷贝。

底层实现
Go Runtime hashmap实现
Golang hashmap 的使用及实现

channel

见并发章节

自定义类型

1
type <new_type_name> <type_name>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ages int
type money float32
type months map[string]int
m := months{
"January":31,
"February":28,
...
}
type Person struct{
name string
age int
}
type human Person

我们在使用time这个包的时候,对于类型time.Duration应该非常熟悉,它其实就是基于int64 这个基本类型创建的新类型,来表示时间的间隔。

但是这里我们注意,虽然Duration是基于int64创建,觉得他们其实一样,比如都可以使用数字赋值。

1
2
3
type Duration int64
var i Duration = 100
var j int64 = 100

但是本质上,他们并不是同一种类型,所以对于Go这种强类型语言,他们是不能相互赋值的。

1
2
3
4
type Duration int64
var dur Duration
dur=int64(100)
fmt.Println(dur)

上面的例子,在编译的时候,会报类型转换的异常错误。

cannot use int64(100) (type int64) as type Duration in assignment

错误类型

Go内置一个error类型,专门用来处理错误信息,Go的package里还专门有一个errors包用来处理错误

1
2
3
4
err := errors.New("error!")
if err != nil{
fmt.Print(err)
}

make/new操作

内建函数make(T,args)只能创建slice/map/channel,并且返回一个有初始值的T类型

内建函数new本质上和其他语言同名函数功能一样,new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语来说,它返回了一个指针,指向新分配的类型T的值。

示例:

1
2
//p是指向[10]int数组的指针
p:=new([10]int)

零值

go中任意类型被声明后,默认都会被初始化为各自的零值。

1
2
3
4
5
6
7
8
9
10
11
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0
byte 0x0 byte实际是uint
float32 0
float64 0
bool false
string ""

Go 标志符可见性

Go的标志符,这个翻译觉得怪怪的,不过还是按这个起了标题,可以理解为Go的变量、类型、字段等。这里的可见性,也就是说那些方法、函数、类型或者变量字段的可见性,比如哪些方法不想让另外一个包访问,我们就可以把它们声明为非公开的;如果需要被另外一个包访问,就可以声明为公开的,和Java语言里的作用域类似。

在Go语言中,没有特别的关键字来声明一个方法、函数或者类型是否为公开的,Go语言提供的是以大小写的方式进行区分的,如果一个类型的名字是以大写开头,那么其他包就可以访问;如果以小写开头,其他包就不能访问。

1
2
package common
type count int
1
2
3
4
5
6
7
8
9
package main
import (
"flysnow.org/hello/common"
"fmt"
)
func main() {
c:=common.count(10)
fmt.Println(c)
}

这是一个定义在common包里的类型count,因为它的名字以小写开头,所以我们不能在其他包里使用它,否则就会报编译错误。

1
./main.go:9: cannot refer to unexported name common.count

因为这个类型没有被导出,如果我们改为大写,就可以正常编译运行了,大家可以自己试试。

现在这个类型没有导出,不能使用,现在我们修改下例子,增加一个函数,看看是否可行。

1
2
3
4
5
package common
type count int
func New(v int) count {
return count(v)
}
1
2
3
4
func main() {
c:=common.New(100)
fmt.Println(c)
}

这里我们在common包里定义了一个导出的函数New ,该函数返回一个count类型的值。New函数可以在其他包访问,但是count类型不可以,现在我们在main包里调用这个New函数,会发现是可以正常调用并且运行的,但是有个前提,必须使用:=这样的操作符才可以,因为它可以推断变量的类型。

这是一种非常好的能力,试想,我们在和其他人进行函数方法通信的时候,只需约定好接口,就可以了,至于内部实现,使用方是看不到的,隐藏了实现。

1
2
3
4
5
6
7
8
9
10
11
12
package common
import "fmt"
func NewLoginer() Loginer{
return defaultLogin(0) //类型转换 将0 int转换为defaultLogin
}
type Loginer interface {
Login()
}
type defaultLogin int
func (d defaultLogin) Login(){
fmt.Println("login in...")
}
1
2
3
4
func main() {
l:=common.NewLoginer()
l.Login()
}

以上例子,我们对于函数间的通信,通过Loginer接口即可,在main函数中,使用者只需要返回一个Loginer接口,至于这个接口的实现,使用者是不关心的,所以接口的设计者可以把defaultLogin类型设计为不可见,并让它实现接口Loginer,这样我们就隐藏了具体的实现。如果以后重构这个defaultLogin类型的具体实现时,也不会影响外部的使用者,极为方便,这也就是面向接口的编程。

假如一个导出的结构体类型里,有一个未导出的字段,会出现怎样的问题。

1
2
3
4
type User struct {
Name string
email string
}

当我们在其他包声明和初始化User的时候,字段email是无法初始化的,因为它没有导出,无法访问。此外,一个导出的类型,包含了一个未导出的方法也一样,也是无法访问的。

我们再扩展,导出和未导出的类型相互嵌入,会有什么样的发现?

1
2
3
4
5
6
type user struct {
Name string
}
type Admin struct {
user
}

被嵌入的user是未导出的,但是它的外部类型Admin是导出的,所以外部可以声明初始化Admin。

1
2
3
4
5
func main() {
var ad common.Admin
ad.Name="张三"
fmt.Println(ad)
}

这里因为user是未导出的,所以我们不能再使用字面值直接初始化user了,所以只能先定义一个Admin类型的变量,再对Name字段初始化。这里Name可以访问是因为它是导出的,在user嵌入到Admin中时,它已经被提升为Admin的字段,所以它可以被访问。

如果我们还想使用:=操作符怎么做呢?

1
ad:=common.Admin{}

字面值初始化的时候什么都不做就好了,因为user未导出,所以我们不能直接使用字面值初始化Name字段。

还有要注意的是,因为user未导出,所以我们不能通过外部类型访问内部类型了,也就是说ad.user这样的操作,都会编译不通过。

最后,我们做个总结,导出还是未导出,是通过名称首字母的大小写决定的,它们决定了是否可以访问,也就是标志符的可见性。

对于.操作符的调用,比如调用类型的方法,包的函数,类型的字段,外部类型访问内部类型等等,我们要记住:.操作符前面的部分导出了,.操作符后面的部分才有可能被访问;如果.前面的部分都没有导出,那么即使.后面的部分是导出的,也无法访问。

例子 可否访问
Admin.User.Name
Admin.User.name
Admin.user.Name
Admin.user.Name
Admin.Name

以上表格中Admin 为外部类型,User(user)为内部类型,Name(name)为字段,以此来更好的理解最后的总结,当然method也适用这个表格。

指针

go虽然保存了指针,但是go中不支持指针运算及->运算符,而直接采用.选择符来操作指针目标对象的成员

& 取变量地址
指针默认值是nil

1
2
3
4
5
func main() {
a := 1
var ptr *int = &a
fmt.Println(*ptr)
}

数组指针(指向数组的指针)

1
2
3
b := [5]int{1, 2, 3, 4, 5}
var bptr *[5]int = &b
fmt.Println(bptr)

指针数组

1
2
3
4
5
6
//定义指针数组
x:=2
arr:=[5]*int{0:new(int),1:new(int),2:&x}
*arr[3] = 3
*arr[4] = 4

流程控制

if

go中的if支持初始化表达式

1
2
3
4
5
6
7
8
if x:=10;x>0 {
...
}else if{
...
}else{
...
}

for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//普通用法
for i := 0; i < 3; i++ {
println(i)
}
//实现while
sum :=1
for sum<100 {
sum += sum
}
//第三种形式 等于while(true)
a:=0
for{
if a==5{
break;
}
}
//遍历array、slice或map
for k,v := range mapData{
fmt.Print("map's key:",k)
fmt.Print("map's value:",v)
}

goto、continue和break

1
2
3
4
5
6
7
8
9
10
11
//break 跳出当前循环
//break后可以接标签,表示跳出多重循环 默认跳出一重循环
//continue 跳过本次循环
LOOP:
for{
select v,ok:=<-ch:
if !ok {
break LOOP
}
}

switch

go中的switch不需要写break

1
2
3
4
5
6
7
8
switch expr {
case 1:
fmt.Println("value is 1")
case 2,3,4:
fmt.Println("value is 2,3 or 4")
default:
fmt.Println("other value")
}

函数

Go函数不支持嵌套、重载和默认参数

但是支持以下特性:

  • 无需声明原型
  • 不定长度变参
  • 多返回值
  • 命名返回值参数
  • 匿名函数
  • 闭包

函数也可以作为一种类型来使用

定义

1
2
3
4
func funcName(input1 type1,input2 type2,...)(output1 type1,output2 type2){
//返回多个值
return value1,value2
}

如果没有返回值,可以省略返回类型及return

多个返回值

Go语言比C先进的地方,其中一点就是函数可以有多个返回值。

1
2
3
4
5
6
7
func SumAndProduct(A int,B int)(int,int){
return A+B,A*B
}
func main(){
x,y := SumAndProduct(1,2) //x:3 y:2
}

同时我们的出错异常信息再也不用像Java一样使用一个Exception这么重的方式表示了,非常简洁

1
2
3
4
5
6
7
8
func main() {
file, err := os.Open("/usr/tmp")
if err != nil {
log.Fatal(err)
return
}
fmt.Println(file)
}

参数

任何类型(包括slice、map、channel)参数传递采用的都是值拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//接收基本类型
func intfunc(arg int){}
//接收数组 数组必须指定长度
func arrayfunc(arg [3]int){}
//接收slice
func slicefunc(arg []int){}
func main(){
//传基本类型数据
func intfunc(1)
//传数组
arr:=[...]int{1,2,3}
arrayfunc(arr)
//传slice
slice:=arr[:2]
slicefunc(slice)
}

指针做参数

传递大量数据时,使用值拷贝显然是不合理的,此时我们使用指针作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func slicefunc(arg *[]int){
fmt.Printf("函数中变量地址: %p\n",arg)
}
func arrayfunc(arg *[5]int){
fmt.Printf("函数中变量地址: %p\n",arg)
}
func main(){
arr:=[5]int{1}
slice:=[]int{1,2,3}
arrayfunc(&arr)
fmt.Printf("原变量地址: %p\n",&arr)
slicefunc(&slice)
fmt.Printf("原变量地址: %p\n",&slice)
}

不定长变参

1
func myfunc(name string,arg ...int){}
  • 不定长变参只能作为函数参数的最后一个
  • 接收的参数arg是一个slice
1
2
3
4
5
6
7
8
9
10
11
12
//示例
//arg是一个int类型的slice
func cal(arg ...int){
for _,n:=range arg{
fmt.Printf("%d\n",n)
}
}
func main(){
/传递多个参数
cal(1,2,3,4)
}

传值与传指针

当我们传一个参数值到被调用函数时,实际上是传了这个值的一个copy,当被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因此数值变化只作用在copy上

传指针的优势
  • 传指针使得多个函数可以操纵一个对象
  • 传指针更轻量级(8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数传递的话,在每次copy上面就会花费较多的系统开销(内存和时间),所以当你要传递大的结构体的时候用指针是一个明智的选择

Go语言中channel,slice,map三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针(注: 如果需要改变slice的长度,则仍需要取地址传递指针)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//只改值 不修改长度
func setFirstZero(s []int){
s[0] = 0
}
//通过非指针方式修改长度 (不会成功)
func appendIntWithoutPointer(s []int,p int){
s=append(s,p)
}
//通过指针方式修改长度(成功)
func appendIntWithPointer(s *[]int,p int){
*s = append(*s,p)
}
func main(){
slice := []int{1,2,3}
setFirstZero(slice) slice:[0 2 3]
appendIntWithoutPointer(slice,4) slice:[0 2 3] 没能修改slice的长度
appendIntWithPointer(&slice,4) slice:[0 2 3 4]
}

闭包

1
2
3
4
5
6
7
8
9
func main(){
arr:=[]int{1,2,3,4}
for _,v:=range arr{
go func(){
fmt.Println(v)
}()
}
}
// 结果 4 4 4 4

这时候我们需要闭包来解决问题
匿名函数内变量以参数形式传入,在传入时获得了拷贝,这就是闭包

通过参数传入

1
2
3
4
5
6
7
8
9
func main(){
arr:=[]int{1,2,3,4}
for _,v:=range arr{
go func(v int){
fmt.Println(v)
}(v)
}
}
//输出 4 3 1 2

又如

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
sum := func(x int) func(int) int {
fmt.Println(&x)
return func(y int) int {
fmt.Println(&x)
return x + y
}
}
a := sum(10)
fmt.Println(a(1))
}

结果

1
2
3
0xc4200160b0
0xc4200160b0
11

由于闭包内保存了当时的环境,所以两次访问的是同一个x值

不通过参数传入

不通过参数传入,则是某个变量的引用

1
2
3
4
5
6
7
8
9
func main(){
arr:=[]int{1,2,3,4}
for _,v:=range arr{
v:=v
go func(){
fmt.Println(v)
}()
}
}

defer(延迟语句)

执行方式类似其他语言的析构函数,在函数体执行结束后按照调用顺序逆序执行(出栈),常用于资源清理、文件关闭、解锁以及记录时间等操作。

即使函数发生严重错误,defer依旧会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//不通过参数传值,访问外部变量引用
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
结果:
3
3
3
//通过参数传值,传入时发生拷贝
for i := 0; i < 3; i++ {
defer func(v int) {
fmt.Println(v)
}(i)
}
结果:
2
1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//资源清理
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX{
return false
}
if failureY{
return false
}
return true
}

Go defer的坑

函数作为值类型

Go中函数也是一种变量,我们可以通过type定义它,它就是所有拥有相同参数、相同返回值的一种类型

1
type typeName func(input1 inputType1,input2 inputType2[,...]) (result resultType1[,...])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//声明一个函数类型
type testInt func(int) bool
func filter(slice []int,f testInt) []int {
var result []int
for _,value := range slice{
if f(value){
result = append(result,value)
}
}
return result
}
func isOdd(p int) bool{
if p%2 ==0{
return false
}
return true
}
func isEven(p int) bool{
if p%2==0{
return true
}
return false
}
func main(){
slice = []int{1,2,3,4,5}
odd := filter(slice,isOdd)
even := filter(slice,isEven)
}

Panic和Recover

Go中没有java那样的异常机制,而是使用了panic和recover机制

panic是一个内建函数,可以中断原有的控制流程,然后进入一个panic流程中。当函数F调用panic后,函数F的执行会中断,此后开始执行F中的延迟函数,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。

recover是一个内建函数,可以让panic流程中的goroutine恢复过来。recover仅在defer中有效。在正常执行过程中调用recover会返回nil,并且没有其他任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并恢复到正常的执行

panic可以在任何位置发生,但是recover只有在defer调用的函数中有效
同一函数中的panic,只有同一层面中的recover可以捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func main(){
//最后调用 捕获"再次产生恐慌"
defer func(){
if err:=recover();err!=nil{
fmt.Print(err)
}
//完全调用完后
}()
//其次调用 由于是在内层执行的recover,无法捕获恐慌
defer func(){
func(){
if err:=recover();err!=nil{
fmt.Println(err)
}
}()
}()
//首先调用 输出产生恐慌 并再次产生恐慌
defer func(){
if err:=recover();err!=nil{
fmt.Println(err)
}
panic("再次产生恐慌")
}()
panic("产生恐慌")
}

例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func badCall() {
panic("bad end")
}
func test() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("Panicing %s\r\n", e)
}
}()
badCall()
fmt.Printf("After bad call\r\n") // <-- wordt niet bereikt
}
func main() {
fmt.Printf("Calling test\r\n")
test()
fmt.Printf("Test completed\r\n")
}

结果
Calling test
Panicing bad end
Test completed

main和init函数

Go里有两个保留的函数: init函数(能应用于所有package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何参数和返回值。

执行顺序
import -> const -> var -> init() -> main()

20180622152963998729080.png

import

import有以下几点需要了解

  • 一个包被其它多个包 import,但只能被初始化一次
  • 其他的包只有被 main 包 import 才会执行,按照 import 的先后顺序执行
  • 被递归 import 的包的初始化顺序与 import 顺序相反,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main

相对路径(不建议)
import “./model”

绝对路径
import “shorturl/model”

多种用法

点操作
1
2
3
import . "fmt"
意义就是这个包导入后,调用这个包的函数可以省略包名
如fmt.Print("hello world")可以写成Print("hello world")
别名操作
1
2
import f "fmt"
f.Println("hello world")
_操作
1
2
import _ "github.com/ziutek/mysql/godrv"
_操作是引入该包,仅仅调用了该包里面的init函数,之后无法直接使用包里的函数

init

  • 一个package中可以有多个init函数,每个文件都可以有自己的init方法 (建议在一个package中每个文件最多只写一个init函数)
  • 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
  • 对于不同的 package,如果不相互依赖的话,按照 main 包中 import 的顺序调用其包中的 init 函数
  • 如果 package 存在依赖,调用顺序为最后被依赖的最先被初始化,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main,一次执行对应的 init 方法。
  • main 包总是被最后一个初始化,因为它总是依赖别的包

面向对象

方法

1
func (r ReceiverType) funcName(parameters) (results)

类型方法相对于函数多了一个接收者

注意:

  • 方法名相同但是接收者不一样,表示的是不同的方法
  • 接受者可以是任意的类型,如自定义类型、struct类型、內建类型等
  • 方法体中可以访问接收者的内容
  • 接收者可以是值传递,也可以是指针传递
  • 通过.符号调用方法,就像在struct中访问字段一样
  • 不存在方法的重载

方法集

类型有一个与之相关的方法集(method set),这决定了它是否实现某个接口(重点!方法集的概念是对接口而言的)

  • 类型T方法集包含所有receiver T方法
  • 类型*T方法集包含所有 receiver T+*T 方法
  • 匿名嵌入S,T方法集包含所有 receiver S 方法
  • 匿名嵌入*S,T方法集包含所有 receiver S+*S 方法
  • 匿名嵌入S或*S*T方法集包含所有 receiver S+*S 方法

实际上,虽然T的方法集不包含receiver *T,但是T的变量是可以调用*T的方法的,原因在于通过.调用时,go会自动取指针再调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Bird struct {
}
func (b Bird) Fly() {
fmt.Println("flying.")
}
func (b *Bird) Chirp() {
fmt.Println("Chirp.")
}
type Magpie struct {
Bird
}
ma := &Magpie{}
t := reflect.TypeOf(ma)
fmt.Println(t.NumMethod())
for i := 0; i < t.NumMethod(); i++ {
fmt.Println(t.Method(i).Name)
}
//输出
2
Chirp
Fly

再来一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
var c cat
//值作为参数传递
invoke(c)
}
//需要一个animal接口作为参数
func invoke(a animal){
a.printInfo()
}
type animal interface {
printInfo()
}
type cat int
//值接收者实现animal接口
func (c cat) printInfo(){
fmt.Println("a cat")
}

我们都知道,如果要实现一个接口,必须实现这个接口提供的所有方法,但是实现方法的时候,我们可以使用指针接收者实现,也可以使用值接收者实现,这两者是有区别的,下面我们就好好分析下这两者的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
var c cat
//值作为参数传递
invoke(c)
}
//需要一个animal接口作为参数
func invoke(a animal){
a.printInfo()¥
}
type animal interface {
printInfo()
}
type cat int
//值接收者实现animal接口
func (c cat) printInfo(){
fmt.Println("a cat")
}

还是原来的例子改改,增加一个invoke函数,该函数接收一个animal接口类型的参数,例子中传递参数的时候,也是以类型cat的值c传递的,运行程序可以正常执行。现在我们稍微改造一下,使用类型cat的指针&c作为参数传递。

1
2
3
4
5
func main() {
var c cat
//指针作为参数传递
invoke(&c)
}

只修改这一处,其他保持不变,我们运行程序,发现也可以正常执行。通过这个例子我们可以得出结论:实体类型以值接收者实现接口的时候,不管是实体类型的值,还是实体类型值的指针,都实现了该接口

下面我们把接收者改为指针试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var c cat
//值作为参数传递
invoke(c)
}
//需要一个animal接口作为参数
func invoke(a animal){
a.printInfo()
}
type animal interface {
printInfo()
}
type cat int
//指针接收者实现animal接口
func (c *cat) printInfo(){
fmt.Println("a cat")
}

这个例子中把实现接口的接收者改为指针,但是传递参数的时候,我们还是按值进行传递,点击运行程序,会出现以下异常提示:

1
2
./main.go:10: cannot use c (type cat) as type animal in argument to invoke:
cat does not implement animal (printInfo method has pointer receiver)

提示中已经很明显的告诉我们,说cat没有实现animal接口,因为printInfo方法有一个指针接收者,所以cat类型的值c不能作为接口类型animal传参使用。下面我们再稍微修改下,改为以指针作为参数传递。

1
2
3
4
5
func main() {
var c cat
//指针作为参数传递
invoke(&c)
}

其他都不变,只是把以前使用值的参数,改为使用指针作为参数,我们再运行程序,就可以正常运行了。由此可见实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口

现在我们总结下这两种规则,首先以方法接收者是值还是指针的角度看。

(t T) T and T
(t
T) *T

如果方法接受者是值接收者,实体类型的值和指针都可以实现对应的接口;如果是指针接收者,那么只有类型的指针能够实现对应的接口。

其次我们我们以实体类型是值还是指针的角度看。

T (t T)
T (t T) and (tT)

上面的表格可以解读为:类型的值只能实现值接收者的接口;指向类型的指针,既可以实现值接收者的接口,也可以实现指针接收者的接口。

接收者

对类型进行操作的时候,是要改变当前值,还是要创建一个新值进行返回?这些决定我们是采用值传递,还是指针传递。

值接收者

值接收者的方法在调用的时候其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。

指针接收者

指针作为接收者,可以对成员本身做修改(指针接收者传递的是一个指向原值指针的副本,指针的副本指向的还是原来类型的值,所以修改时会影响原类型变量的值)

1
2
3
4
5
6
7
8
9
10
func (s *Student) ChangeName(newName string) { //因为要修改receiver的值,所以使用指针传递
s.name = newName //当使用指针去访问字段时,Go的编译器自动会帮我们取指针,所以在这里 s.name 就等于(*s).name
}
func (s Student) GetName() string{
return s.name
}
s:=Student{name:"xiaoming"}
s.ChangeName("wangxiaoming") //同样的在这里go知道你要通过指针修改对应内容的字段值,在这里s.ChangeName等于(&s).ChangeName

方法继承

struct嵌入昵称字段,可以继承匿名字段的方法

嵌入非匿名字段,struct不可使用匿名字段方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Person struct{
name string
age int
}
func (p Person) GetName() string{
return p.name
}
type Student struct{
Person
no int
}
type Teacher struct {
Person Person
}
s:=Student{Person{"xiaoming",18},18}
sname:=s.GetName() //GetName方法继承自匿名字段Person
t:=Student{Person{"wanghua",28},30}
tname:=t.GetName() //t.GetName undefined (type Teacher has no field or method GetName)

方法重写

如果Student要实现自己的GetName方法,重新定义receiver为Student的GetName方法即可

1
2
3
4
5
func (s Student) GetName() string{
return "敬爱的"+s.name
}
name := s.getNmae() //name: 敬爱的xiaoming

interface

什么是interface

interface是一组方法的组合,我们通过interface来定义对象的一组行为

接口是用来定义行为的类型,它是抽象的,这些定义的行为不是由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型,实现了接口类型声明的所有方法,那么这个用户定义的类型就实现了这个接口,所以这个用户定义类型的值就可以赋值给接口类型的值

1
2
3
4
type MenInterface interface{
SayHi()
Sing()
}

interface的值

1
2
3
4
5
6
7
8
9
10
11
12
13
func (s Student) SayHi(){
fmt.Println("Hi")
}
func (s Student) Sing(){
fmt.Println("singing")
}
var m MenInterface
s := Student{"xiaoming",18}
m = s
m.SayHi()
m.Sing()

空interface

interface{}不包含任何方法,所以任何类型都实现了空interface
如果使用空interface作为函数参数,那么该参数可以接受任意类型的值,如果一个函数返回interface{},那么它也就可以返回任何类型的值

1
2
3
4
5
6
7
8
9
10
var a interface{}
vat i int32 = 5
var j string = "hello"
k := Student{"xiaoming",18}
//a可以存储任意类型的值
a = i
a = j
a = k
a.(Student).GetName()

interface做函数参数

将对象赋值给接口时会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针

例如fmt.Println 只要参数实现了Stringer接口 都可以作为参数传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type MenInterface interface{
SayHi()
Sing()
}
func StudentSing(s MenInterface){
s.Sing()
}
s:=Student{"xiaoming",18}
func (s Student) SayHi() {}
func (s Student) Sing() {}
StudentSing(s) //s实现了Menterface接口,可以直接作为参数传入
//空接口作为参数可接受任意类型值
func doSomething(a interface{}){
...
}

interface转具体类型

我们知道interface类型可以存储任意类型的值,那么我们怎么反向知道这个变量里实际保存了的是哪个类型的对象呢?目前有2个方法

Comma-ok断言

value,ok:=element.(T) 其中T是断言的类型,element是变量,如果element里确实存储了T类型的数据,返回true,否则返回false

只有接口类型才能进行断言

1
2
3
4
5
6
7
8
9
for index := range list{
if value,ok:=element.(int);ok {
...
}else if value,ok:=element.(bool);ok{
...
}else if value,ok:=element.(Student);ok{
...
}
}
switch
1
2
3
4
5
6
7
8
switch value:=element.(type){
case int:
case string:
case Person: //struct
case io.ReadCloser: //接口 只要element实现了io.ReadCloser就会匹配
value.Close()
default
}
单case存在多个值的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type student struct {
Name string
}
func T(v interface{}) {
switch msg := v.(type) {
case *student, student:
fmt.Printf("%#v", msg) //由于对应多个case,msg依旧是interface类型。如果需要访问msg.Name,需要先断言成具体类型再访问Name
}
}
func main() {
s := student{}
s.Name = "hello"
T(s)
}

interface嵌套

1
2
3
4
5
6
7
8
9
10
11
12
type StudyInterface interface{
Study()
}
type SayInterface interface{
Say()
}
type HumanInterface interface{
StudyInterface
SayInterface
}

struct与interface区别

interface相当于抽象的一组行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Closer interface{
Close() error
}
type Resource struct{
}
func (r *Resource) Close() error {
...
}
func createResrouce() Closer{
return &Resource{} //因为&Resource实现了Closer接口 所以可以直接传
}

struct是实体,包含属性和具体行为
虽然语法上可以有包含实现等等,但是实际上两者相互独立,不可替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Resource struct{
}
type Resource2 struct{
Resource
}
func New(r Resource){
}
func main(){
New(Resource2{}) //这里传入Resource2会报错,原因是New方法参数是Resource具体类型,而非interface
}

反射

反射

1
2
3
4
5
6
7
//第一步: 将变量转化为reflect对象
t:=reflect.TypeOf(i) //获得类型t,通过t我们能获取类型定义中的所有元素
v:=reflect.ValueOf(i) //获得实际值v,通过v我们获取里面存储的值,还可以去改变值
//第二步: 将reflect对象转化为相应的值
tag:=t.Elem().Field(0).Tag //获取结构体第一个字段的标签
name:=v.Elem().Field(0).String() //获取存储在第一个字段里的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Type interface{
...
}
func TypeOf(i interface{}) Type
type Value interface{
...
}
func ValueOf(i interface{}) Value
func (v Value) Method(i int) Value
type Method struct{
Name string
PkgPath string
Type Type //method type
Func Value //func with receiver as first argument
Index int //index for Type.Method
}

并发

并发的含义

并发: 逻辑上具备同时处理多个任务
并行: 物理上同一时刻执行多个并发任务

以咖啡机为例,两个队列,一个Coffee机器,机器不断替换为2个队列提供服务,这个叫并发;两个队列,两个Coffee机器,两台机器同时工作,这个叫并行

20171025150894536190708.png

根本上来说,并发主要由切换时间片来实现”同时”运行,并行则是直接利用多核实现多线程的运行

根据go程序开启线程数不同,goroutine有以下表现:

  • 限制开启一个线程
    当go只开启一个线程的时候,所有goroutine是并发。在同一个原生线程里,如果当前goroutine不发生阻塞,则不会让出CPU时间给其他同线程goroutine(详情见GPM模型)

  • 开启多线程
    为了达到真正的并行,需要让go程序开启多个线程(现代版本自动开启),这种情况是并发+并行

Goroutine

什么是Goroutine

Goroutine是go语言并行设计的核心,它是一种比系统线程更小的用户态轻量级线程,十多个goroutine在系统层面可能只是五六个线程。

优点:
  • 为go语言提供了原生并发编程支持
  • 大大简化并发编程成本

goroutine中可以再开goroutine

如何开启Goroutine

1
go hello(a,b,c) //通过go关键字创建一个新的goroutine
开启goroutine背后的事情

关键字go并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适的系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动且运行时也不保证并发任务的执行次序。

每次个任务单元除了保存函数指针、调用参数等还会分配执行所需的栈内存空间,相比系统默认MB级别的线程栈,goroutine自定义栈初始为2KB,所以可以创建大量并发任务。自定义栈采用按需分配策略,在需要时进行扩容,最大能到GB规模。

数据同步

goroutine奉行通过通信来共享内存,而不是共享内存来通信

runtime

1
2
3
4
5
6
7
rumtime.Gosched() //显式的将CPU时间让给其他goroutine,并在下次某个时间从该位置恢复执行
runtime.Goexit() //退出当前执行的goroutine,但是defer函数还会继续调用
runtime.NumCPU() //返回cpu核心数
runtime.NumGoroutine() //返回正在执行和排队的goroutine总数
runtime.GOMAXPROCS //用来设置可以并行计算的CPU核数最大值,并返回之前的值

特殊的Goroutine

main函数本身启动一个协程。特别的是,main函数是执行程序的入口,如果main本身执行完毕,表示程序执行完毕,结果是main中启动的goroutine也会被关闭。

Channel(信道)

goroutine运行在相同的内存地址,因此访问共享内存必须做好同步,这方面Go提供了channel通信机制。

  • channel 是引用类型,通常通过make创建,close关闭
  • for i:=range c会从信道c中循环读取数据,直至信道关闭且无缓冲数据。
  • 信道被关闭后是只读的,没有数据的话返回nil,对已关闭信道写入数据会引发panic。
  • 在发送方进行关闭信道,不要在接收方进行关闭操作
  • 信道和文件不同,你通常可以不关闭它。只有在必须告诉接受者信道中不会再有数据时才有必要关闭信道,比如在需要终止range循环的时候。
1
2
通过信道获取数据
<-ch

获取的数据是原传值的拷贝

无缓冲信道

无缓冲信道的长度为0,永远不存储数据,只负责数据流通

由于长度为0,所以无缓冲channel发送和接收数据都是阻塞的。意思是<-ch会读取ch中的数据,没有数据则阻塞,直至有数据为止;ch<-5如果ch本身有数据,则阻塞直到ch数据被读取消费,然后执行插入。

创建无缓冲信道
1
2
3
4
5
ci := make(chan int)
ch<-v //发送v到ch中
<-ch //从ch中读取数据
len(ch) //永远为0
常见操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//基本操作1
func main(){
c:=make(chan int)
go func(){
fmt.Println("running")
c<-1
}
fmt.Println(<-c) //没有数据时阻塞
}
//基本操作2
func main() {
count := 10
c := make(chan int)
for i := 0; i < count; i++ {
go run(c, i)
}
for i := 0; i < count; i++ {
fmt.Println(<-c)
}
}
func run(c chan int, i int) {
c <- i
}

缓冲信道

缓冲信道简单理解就是可以存储元素的信道,我们可以把缓冲信道看做一个线程安全的队列

缓冲信道操作:

  • <-ch读取ch数据,ch无数据(干涸)则阻塞,否则可以一直读取
  • ch<-x向ch插入数据,如果此时ch容量满则阻塞,否则可以一直插入

举个例子,c:=make(chan int,1) 这样c可以缓存一个数据,也就是说,放入一个数据,c不会挂起当前协程,再放入一个才会挂起直到第一个数据被其他goroutine取走,也就是不达容量不阻塞

想知道信道的容量及里面有几个元素数据怎么办?使用cap和len就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//例1: 定义长度为10的int类型channel
ch := make(chan int,10)
//例2:
func insert(ch chan int){
for i:=0;i<3;i++{
ch<-i
}
}
func main(){
ch := make(chan int,2)
go insert(ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}

上面例子中我们使用消费了3次,但通常我们并不知道channel中有多少数据,无法明确指定消费多少次,那么有没有办法遍历channel,缓冲区有多少消费多少呢?这时我们可以使用range和close解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//例3: 单个channel多次消费
func insert(ch chan int){
for i:=0;i<3;i++{
ch<-i
}
close(ch) //消费者可以使用v,ok:=<-ch来判断channel是否关闭。如果ok返回false说明channel已经没有任何数据并且已经关闭
}
func main(){
ch:=make(chan int,2)
go insert(ch)
//使用range遍历的channel需要执行close操作,否则会导致死锁
//即使channel提前关闭,range依旧可以读取其中已发送的数据
for i:=range ch{ //使用range迭代channel时,只有一个返回值
fmt.Println(i) //这里i已经是数据
}
}
//例4: 等待多goroutine方案:使用容量为goroutines数量的缓冲信道
//缓冲信道个数小于goroutine数量的话有可能造成goroutine泄漏
func main() {
count := 10
c := make(chan int, count)
//执行10个任务
for i := 0; i < count; i++ {
go Go(c, i)
}
//消费10次
for i := 0; i < count; i++ {
<-c
}
//消费完输出done
fmt.Println("done")
}
func Go(c chan int, index int) {
time.Sleep(time.Second)
fmt.Println(index)
c <- 1
}

无缓冲信道与缓冲信道差别

无缓冲不能存储数据,有缓冲数据可以存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//无缓冲信道
func main(){
c:=make(chan int)
go func(){
c<-1 //执行到这里,阻塞 c依旧是没有存储数据
}()
fmt.Println(<-c) //1通过信道c流出,c从始至终长度一直是0
}
//有缓冲信道
func main(){
c:=make(chan int,1)
go func(){
c<-1 //执行到这里,没有阻塞,c存储1,长度变为1
}()
fmt.Println(<-c) //c长度为1,1从c中取得
}

单向信道

1
2
var send <-chan int //只能读取
var send chan<- int //只能写入

select

select用于监听信道中的数据流动, 对应的case关键词后只能是IO操作。

在使用上select默认是阻塞的,当监听的channel中有可以发送或接收时数据时才会运行;当多个channel都准备好时,select是随机的选择一个执行的;case中的default用于设定当监听的channel都没有准备好的时候的默认执行动作(此时select不阻塞channel)

可用空的select来阻塞main函数

值得注意的一点,当信道关闭时可以从信道中取得值(0),与select搭配使用时如果select监听的某个信道被关闭,其default逻辑将永远不会被执行到,正确的做法是当检测到信道关闭且信道中不再有值时将channel置为nil(nil channel发生阻塞),从而达到关闭这条数据链路的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//例1 用于阻塞
select{}
//例2
ch1,ch2 := make(chan int,1),make(chan int,1)
go func() {
time.Sleep(time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- 2
}()
for {
select {
case v,ok:=<-ch1: //如果ch1关闭,则会读到0 false
if !ok {
ch1 = nil //设置为nil channel,关闭这条数据流监控
}
fmt.Println(v,ok)
case v,ok:=<-ch2:
if !ok {
ch2 = nil
}
fmt.Println(v,ok)
default:
// fmt.Println("default")
}
//0.5秒
time.Sleep(1e8*5)
}

设置超时

有时候会出现goroutine出现阻塞的情况,那么如何避免整个程序进入阻塞呢?使用select来设置超时

1
2
3
4
5
6
7
8
9
10
11
12
func main(){
a := make(chan int)
Loop: for {
select {
case v := <-a:
fmt.Println(v)
case <-time.After(5 * time.Second): //表示本次循环内执行超过了5秒,不能于default同时存在,否则就会匹配default
fmt.Println("timeout")
break Loop//这里break是有必要的 否则 5秒和10秒的时候都会输出timeout
}
}
}

注意此时没有default,因为如果有default的话每次select都会执行default动作指令

死锁

两种可能:

  • 信道已满,我们还要加入数据
  • 信道干涩,我们一直向无数据流入的空信道取数据

无缓冲信道上发生流入无流出,流出无流入便会导致死锁

并发竞争

我们对于同一个资源的读写必须是原子化的,也就是说,同一时间只能有一个goroutine对共享资源进行读写操作

检查程序是否有资源竞争

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import (
"fmt"
"runtime"
"sync"
)
var (
count int32
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
value := count
runtime.Gosched()
value++
count = value
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go build -race hello.go
➜ ./hello
==================
WARNING: DATA RACE
Read at 0x0000011a5118 by goroutine 7:
main.incCount()
/Users/xxx/code/go/src/flysnow.org/hello/main.go:25 +0x76
Previous write at 0x0000011a5118 by goroutine 6:
main.incCount()
/Users/xxx/code/go/src/flysnow.org/hello/main.go:28 +0x9a
Goroutine 7 (running) created at:
main.main()
/Users/xxx/code/go/src/flysnow.org/hello/main.go:17 +0x77
Goroutine 6 (finished) created at:
main.main()
/Users/xxx/code/go/src/flysnow.org/hello/main.go:16 +0x5f
==================
4
Found 1 data race(s)

找到一个资源竞争,连在那一行代码出了问题,都标示出来了。goroutine 7在代码25行读取共享资源value := count,而这时goroutine 6正在代码28行修改共享资源count = value,而这两个goroutine都是从main函数启动的,在16、17行,通过go关键字。

更简单的一个例子

1
2
3
4
5
6
7
8
func main() {
var a int = 1
go func() {
a = 2
}()
a = 3
fmt.Println(a)
}

既然我们已经知道共享资源竞争的问题,是因为同时有两个或者多个goroutine对其进行了读写,那么我们只要保证,同时只有一个goroutine读写不就可以了,现在我们就看下传统解决资源竞争的办法–对资源加锁。

并发竞争解决方案

Go语言提供了atomic包和sync包里的一些函数对共享资源同步加锁,有CAS和互斥锁两种解决方法

CAS(atomic)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
count int32
wg sync.WaitGroup
)
func main(){
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount(){
defer wg.Done()
for i:=0;i<2;i++{
value:=atomic.LoadInt32(&count)
runtime.Gosched()
for{
if atomic.CompareAndSwapInt32(&count, value, value+1) {
break
}
}
}
}

CAS(compare and swap) 比较后再置换,CAS表示乐观,不实际加锁,性能比加锁要好,但是有可能失败,所以要使用for循环直至成功

atomic虽然可以解决资源竞争问题,但是比较都是比较简单的,支持的数据类型也有限,所以Go语言还提供了一个sync包,这个sync包里提供了一种互斥型的锁,可以让我们自己灵活的控制哪些代码,同时只能有一个goroutine访问,被sync互斥锁控制的这段代码范围,被称之为临界区,临界区的代码,同一时间,只能有一个goroutine访问。刚刚那个例子,我们还可以这么改造。

原子操作
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(即线程切换)
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,将整个操作视作一个整体是原子性的核心特征

互斥锁(mutex或RWMutex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import (
"fmt"
"runtime"
"sync"
)
var (
count int32
wg sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
mutex.Lock()
value := count
runtime.Gosched()
value++
count = value
mutex.Unlock()
}
}

互斥锁属于悲观锁,性能比CAS弱,但是编写简单

示例中,新声明了一个互斥锁mutex sync.Mutex,这个互斥锁有两个方法,一个是mutex.Lock(),一个是mutex.Unlock(),这两个之间的区域就是临界区,临界区的代码是安全的。

使用时我们先调用mutex.Lock()对有竞争资源的代码加锁,这样当一个goroutine进入这个区域的时候,其他goroutine就进不来了,只能等待,一直到调用mutex.Unlock() 释放这个锁为止。

已经锁定的Mutex并不与特定的goroutine相关联,这样可以利用一个goroutine对其加锁,再利用其他goroutine对其解锁

这种方式比较灵活,可以让代码编写者任意定义需要保护的代码范围,也就是临界区。除了原子函数和互斥锁,Go还为我们提供了更容易在多个goroutine同步的功能,这就是通道chan。

另外Mutex适用于读写不确定场景,并且只允许有一个读或写的场景,所以该锁也叫全局锁。RWMutex是一个读写锁,该锁可以加多个读锁或一个写锁,其经常用于读远远多于锁的场景。

已加锁的锁再次执行Lock会引发fatal error
未对所的锁执行Unlock也会一番fatal error

1
2
3
4
5
func (rw *RWMutex) Lock()  写锁,如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定
func (rw *RWMutex) Unlock() 写锁解锁,如果没有进行写锁定,则就会引起一个运行时错误
func (rw *RWMutex) RLock() 读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于"读多写少"的场景
func (rw *RWMutex)RUnlock() 读锁解锁,RUnlock 撤销单次 RLock 调用,它对于其它同时存在的读取器则没有效果。若 rw 并没有为读取而锁定,调用 RUnlock 就会引发一个运行时错误

简单记就是写写冲突、读写冲突、读读不冲突

读锁与不加锁区别
不加锁读取时可能在并发写入,导致读取一半数据的情况。
加读锁读取时不能写入

channel

并发控制方案

sync.Workgroup

select+chan

context

并发应用

生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
generator := xrange()
for i := 0; i < 1000; i++ {
fmt.Println(<-generator)
}
}
//自增整数生成器
func xrange() chan int {
c := make(chan int) //有泄漏风险?
go func() {
for i := 0; ; i++ {
c <- i
}
}()
return c
}

服务化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//获取消息提示
func getNotification(user string) chan string {
notifications := make(chan string)
go func() {
//此处可以查询数据库获取新消息等...
notifications <- "hello " + user + "!"
}()
return notifications
}
func main() {
//获取xiaoming的消息提示
xiaoming := getNotification("xiaoming")
//获取lilei的消息提示
lilei := getNotification("lilei")
//获取消息的返回
fmt.Println(<-xiaoming)
fmt.Println(<-lilei)
}

多路复合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func main() {
c := multiget(branch(1), branch(2), branch(3))
for i := 0; i < 3; i++ {
fmt.Println(<-c)
}
}
//复合
func multiget(chs ...chan int) chan int {
res := make(chan int)
//取值需要通过goroutine来操作,注意这里由于在for里,需要用到闭包
for _, c := range chs {
go func(c chan int) {
res <- <-c
}(c)
}
return res
}
//分支
func branch(x int) chan int {
c := make(chan int)
go func() {
c <- doStaff(x)
}()
return c
}
//比较费时的计算
func doStaff(x int) int {
time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
return 100 - x
}

select监听信道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func produce(i int) chan int {
c := make(chan int)
go func() {
c <- i
}()
return c
}
func main() {
c := make(chan int)
c1, c2, c3 := produce(1), produce(2), produce(3)
//time是一个计时信道,如果达到时间了,就会发出一个信号来
timeout:=time.After(time.Second)
go func() {
for is_timeout:=false;!is_timeout;{
select {
case v := <-c1:
c <- v
case v := <-c2:
c <- v
case v := <-c3:
c <- v
case <-timeout:
is_timeout=true //超时
}
}
}()
//阻塞主线,取出信道c中的信息
for i := 0; i < 3; i++ {
fmt.Println(<-c)
}
}

随机数生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
num := random()
fmt.Println(<-num)
}
func random() chan int {
c := make(chan int)
go func() {
select {
case c <- 0:
case c <- 1:
case c <- 2:
case c <- 3:
case c <- 4:
}
}()
return c
}

定时器

1
2
3
4
5
6
timer := time.After(time.Second)
select {
case <-timer:
fmt.Println("到时间了")
}

Go内存模型

go的内存模型对并发安全有两种保护措施。一种是通过加锁保护,另一种是通过channel来保护。后者其实就是一个线程安全的队列。

换句话说就是,在go语言中,当有多个goroutine并发操作同一个变量时,除非是全都是只读操作,否则就得加锁或者使用channel来保证并发安全。
go内存模型

错误处理

错误处理

测试

单元测试

基准测试

web基础

文本文件处理

XML处理
JSON处理

socket编程

聊天室

20171103150971327197755.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package main
import (
"fmt"
"net"
"os"
)
/**
服务器接收数据goroutine
*/
func serverHandleRead(conn net.Conn, message chan string) {
fmt.Println("connection is connected from ", conn.RemoteAddr().String())
buf := make([]byte, 4096)
for {
num, err := conn.Read(buf)
if err != nil {
fmt.Println(err.Error())
conn.Close()
break
}
request := string(buf[:num])
message <- request
}
}
/**
服务器广播goroutine
*/
func serverBroadcast(conns map[string]net.Conn, message chan string) {
for {
info := <-message
fmt.Println(info)
for k, conn := range conns {
_, err := conn.Write([]byte(k + ": " + info))
if err != nil {
fmt.Println(err.Error())
delete(conns, k)
}
}
}
}
/**
启动服务器
*/
func startServer() {
service := ":7000"
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
message := make(chan string)
conns := make(map[string]net.Conn)
//启动广播goroutine
go serverBroadcast(conns, message)
listener, err := net.ListenTCP("tcp4", tcpAddr)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
for {
fmt.Println("Listening...")
conn, err := listener.Accept()
if err != nil {
fmt.Println(err.Error())
os.Exit(3)
}
fmt.Println("Accepting ...")
conns[conn.RemoteAddr().String()] = conn
message <- conn.RemoteAddr().String() + " is coming"
go serverHandleRead(conn, message)
}
}
/**
客户端发送goroutine
*/
func clientWrite(conn net.Conn) {
var input string
for {
fmt.Scanln(&input)
if input == "/quit" {
fmt.Println("Bye.")
conn.Close()
os.Exit(0)
}
_, err := conn.Write([]byte(input))
if err != nil {
fmt.Println(err.Error())
os.Exit(3)
}
}
}
/**
客户端启动函数
*/
func startClient(service string) {
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
conn, err := net.DialTCP("tcp4", nil, tcpAddr)
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
//启动客户端发送goroutine
go clientWrite(conn)
buf := make([]byte, 4096)
//轮训读取数据
for {
num, err := conn.Read(buf)
if err != nil {
fmt.Println(err.Error())
conn.Close()
os.Exit(3)
}
fmt.Println(string(buf[:num]))
}
}
/**
主程序
说明:
启动服务器 ./chat.go server [port]
启动客户端 ./chat.go client [ip:port]
*/
func main() {
if len(os.Args) != 3 {
fmt.Println("参数错误")
os.Exit(1)
}
if os.Args[1] == "server" && len(os.Args) == 3 {
startServer()
}
if os.Args[1] == "client" && len(os.Args) == 3 {
startClient(os.Args[2])
}
}

参考

Go语言TCP网络编程(详细)

Websocket

websocket相对于传统Http有如下好处:

  • 一个web客户端只建立一个连接
  • websocket服务端可以推送数据到服务端
  • 有更轻量级的头,减少数据传输量

websocket起始输入是ws://或wss://(在ssl上)

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:1234";
window.onload = function() {
console.log("onload");
sock = new WebSocket(wsuri);
sock.onopen = function() {
console.log("connected to " + wsuri);
}
sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}
sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};
function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>

服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main
import (
"golang.org/x/net/websocket"
"fmt"
"log"
"net/http"
)
func Echo(ws *websocket.Conn) {
var err error
for {
var reply string
if err = websocket.Message.Receive(ws, &reply); err != nil {
fmt.Println("Can't receive")
break
}
fmt.Println("Received back from client: " + reply)
msg := "Received: " + reply
fmt.Println("Sending to client: " + msg)
if err = websocket.Message.Send(ws, msg); err != nil {
fmt.Println("Can't send")
break
}
}
}
func main() {
http.Handle("/", websocket.Handler(Echo))
if err := http.ListenAndServe(":1234", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

标准库

strconv

其他类型转向字符串时方法前缀是Format,从字符串转为其他类型则方法前缀是Parse

strconv.Atoi 字符串转整数
strconv.Itoa 整数转字符串
strconv.ParseBool 字符串转布尔
strconv.FormatBool 布尔转字符串
strconv.ParseFloat 字符串转浮点数
strconv.FormatFloat 浮点数转字符串\

fmt.Println(“shop_id: “ + strconv.FormatFloat(price, ‘f’, 2, 32)) 2位小数

依赖管理

go module

调试

Go调试手段

性能分析

pprof

编译

交叉编译

Mac 下编译, Linux 或者 Windows 下去执行

1
2
3
4
# linux 下去执行
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
# Windows 下去执行
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go

参考

Go标准库示例

FAQ

1. 在go中如何表示字符类型变量

go没有常规意义上的字符类型。字符变量通常使用byte来存储(byte是uint8的别称)

1
2
3
c := 'a'
var c byte = 'a'
arr := [3]byte{'a','b','c'}

2. T是结构体,为什么T.element和*T.element都可以访问到属性

这是go的一个语法糖。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Person struct{
name string
age int
}
var a Person
a.name = "xiaoming"
a.age = 18
b:=new(Person)
b.name = "xiaoming"
b.age = 18
(*b).age = 20 //效果一样,同样可以赋值 编译器会自动取指针

3.字符与整数

在go中,字符类型和整数类型都属于数值类型,可以相互转化比较

1
2
3
4
5
6
7
str:="/hello"
c := '/'
if str[0]=='/'{ //实际用调试器输出pattern值是47
fmt.Println("match")
}
fmt.Println(str[0], c)
fmt.Println(reflect.TypeOf(str[0]), reflect.TypeOf(c))

结果

match
47 47
uint8 int32

4.在itoa.go中这段代码如何理解

1
2
3
4
5
6
7
8
var shifts = []uint{
1 << 1: 1,
1 << 2: 2,
1 << 3: 3,
1 << 4: 4,
1 << 5: 5,
}
fmt.Println(shifts)

这一条语句是数组的初始化,大括号内的每个逗号前的内容代表一个元素,例如1<<1:1其实就是2:1,意思是索引为2的元素值是1,下面几行也是如此。
最终结果是[0 0 1 0 2 0 0 0 3 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5]

5.为什么最后结果是false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a interface{}
fmt.Println(a == nil)
var p *int = nil
a = p
fmt.Println(reflect.TypeOf(a))
fmt.Println(reflect.TypeOf(p))
fmt.Println(a == nil) //具体值赋值给interface,此时接口的数据成员指向值的副本地址,接口本身不为nil,所以返回false
结果:
true
*int
*int
false

6.闭包与非闭包差异

1
2
3
4
5
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}

i不是通过匿名函数参数传入,所以使用的是外部变量的引用,外部变量i在循环结束后是3,所以goroutine中获取到的i是3

解决方法有两种,一是循环内重新声明局部变量,二是通过匿名函数参数传参
方法一

1
2
3
4
5
6
for i := 0; i < 3; i++ {
i:=i
go func() {
fmt.Println(i)
}()
}

方法二
通过匿名函数的参数进行传参(闭包),此时匿名函数中获取到的值是当时传入的值,所以得到结果1 2 3

1
2
3
4
5
for i:=0;i<3;i++{
go func(v int){
fmt.Println(v)
}(i)
}

7. defer错误用法

1
2
3
4
5
file, err := os.Open("studygolang.txt")
defer file.Close()
if err != nil {
...
}

如果studygolang.txt找不到,file无法成功获取,file.Close()就会panic。
正确做法是把defer放到错误检查之后

1
2
3
4
5
file,err := os.Open("studygolang.txt")
if err != nil{
...
}
defer file.Close()

8.goroutine问题。该示例中只开启一个线程,为什么ch阻塞后goroutine会继续运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
runtime.GOMAXPROCS(1)
ch := make(chan int)
for i := 0; i < 2; i++ {
go func(i int) {
fmt.Println("start goroutine")
time.Sleep(time.Second)
ch <- i
fmt.Println(time.Now())
}(i)
}
for i := 0; i < 2; i++ {
//time.Sleep(time.Second)
fmt.Println("output")
fmt.Println(<-ch)
}

答: 当goroutine阻塞时,会主动挂起,并将线程资源让给其他goroutine。注意,阻塞的是goroutine,而不是线程

执行顺序:
16行(输出output)->17行(阻塞,挂起goroutine)->7行(2个goroutine,输出2个start goroutine)->8行(2个goroutine阻塞1秒)->9行(goroutine A执行后阻塞,goroutineB直接阻塞,与此同时2个goroutine挂起)->17行(输出goroutineA传入信道的值,接着开始第二个循环,输出output后再次阻塞)->9行(goroutineB向信道传入值,接着阻塞挂起)->17行(获取信道中的值)->主goroutine结束,整个执行结束,goroutineA、B中的第10行没有执行到

9.值类型和引用类型的坑

1
2
3
4
5
6
7
8
9
10
11
s1 := [2]int{1, 2}
func(i [2]int) {
i[1], i[0] = i[0], i[1]
}(s1)
fmt.Println(s1)
s2 := []int{1, 2}
func(i []int) {
i[1], i[0] = i[0], i[1]
}(s2)
fmt.Println(s2)
结果:
[1 2]
[2 1]

[2]int{1,2}是数组,值类型,函数传参时是值传递,所以函数中操作对s1无影响
[]int{1,2}是slice,引用类型,函数传参时是引用传递,所以函数中操作对s2有影响

10.ioutil.ReadAll阻塞的问题

http://blog.csdn.net/u011686226/article/details/52217698
ioutil.ReadAll一定要读到EOF或error时才会返回结果

11conn.Read读不到数据的问题

1
2
3
4
5
6
7
8
//tcp server
conn, err := listener.Accept()
checkError3(err)
conn.Write([]byte("nihaoa"))
var str []byte
num, _ := conn.Read(str)
fmt.Println(string(str), num)
// "" 0

没有读取到内容,原因是用于存放内容的slice容量为0,显式加上缓冲容量后成功拿到数据

1
2
3
str:=make([]byte,4096)
num, _ := conn.Read(str)
fmt.Println(str[:num],num)

12.如何正确读取tcp连接中的数据

1
2
3
4
5
6
7
8
9
func read(conn net.Con) []byte,err{
str:=make([]byte,4096)
num,err:=conn.Read(str)
if num>0 {
return str[:num],nil
}
return str,err
}

13.如何获取整数类型的绝对值

1
2
var a int = 123
abv:= int(math.abs(float64(a)))

math.abs()限定输入类型是float64,而且我发现math中大多数运算时数据类型都是float64

14.结构体加锁失效问题

1
2
3
4
5
6
7
8
9
10
type data struct {
sync.Mutex
}
func (d data) test(s string) {
d.Lock() // 无效
defer func() {
d.Unlock()
println("success")
}()
}

其中d.Lock在并发场景下是无效的,原因在于d data是值接收者,d是原值的拷贝,锁同样是拷贝,导致每次test方法中的锁不是同一把锁

解决方案1: receiver用 *receiver
解决方案2: 结构体中的锁用指针 *sync.Mutex

最佳实践是结构体加锁直接使用匿名成员sync.Mutex,不要使用*sync.Mutex

Golang锁失效原因之value receiver

15.函数类型传值

函数类型与普通类型不同
只要与函数参数及返回值相同,即可传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//函数类型mt
type mt func(int)
func (m mt) say(){
fmt.Println("it works")
}
func say(m mt){
m.say()
}
func main(){
//声明函数变量f
f:=func(a int){}
//f与mt属性相同,自动转为mt类型数据传入
say(f)
}

16.通过go build和go run结果不同 怎么办

使用go build -ngo run -n 对比查看打印出执行的内容,其中-n表示打印内容且不执行

17.slice和map初始化问题

map使用前注意要初始化(make),make(map[int]int)
slice超出边界会引发报错,推荐使用append向slice添加元素

18.切片...的使用

1
2
3
4
5
6
7
8
9
func main() {
a:=[]string{"1","2"}
test(a...) //这里报错Cannot use 'a' (type []string) as type []interface{} 改为[]interface{}{"1","2"}后问题解决
test("1","2") //无报错
}
func test(args ...interface{}){
fmt.Println(args)
}

如何确保类型满足某接口

1
2
3
type T struct{}
var _ I = T{} // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I

如果T或*T没有实现I,在编译时可以捕获到相应错误

字段嵌套字段实现了继承吗

go中没有继承的概念,go所做的是通过嵌入字段的方式实现了类型之间的组合,更为灵活

踩坑记录

深坑之interface与nil

go-环境配置

练习题

golang面试笔试题

答案如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//第4题
type People struct {
Name string
}
func (p *People) String() string {
return fmt.Sprintf("print: %v", p) //fmt.Sprintf内部调用了String 陷入死循环进而导致栈溢出
}
func main() {
p := People{}
p.Name = "ello"
fmt.Println(p.String())
}

参考

Go语言并发与并行学习笔记(一)
Go语言并发与并行学习笔记(二)
Go语言并发与并行学习笔记(三)
Go语言标准库
神奇的go语言