环境配置
GOROOT
用于存放标准库GOPATH
用于存放非标准库
GOPATH目录
路径 | 说明 |
---|---|
src | 用于存放源代码,是开发程序的主要目录(Go使用utf-8字符集) |
pkg | 编译后生成的文件 |
bin | 编译后生成的可执行文 |
package. main表示可执行程序 其他表示是应用包
包(package)
Go使用package来组织代码,其中main.main()是每个可运行程序的入口
声明
|
|
上面告诉我们当前文件属于哪个包
新建包
新建应用或代码包时在src目录下新建一个文件夹,文件夹名称一般是代码包名称。
main包
每个可独立运行的Go程序,必定包含一个package main的文件,在这个文件中必定包含一个入口函数main,而这个函数既没有参数也没有返回值。
main包在编译后产生可执行文件,其他包最后都会生成*.a文件并放置在$GOPATH/kpg/xxx中
包文件组织结构
一个目录一个package,等同于namespace
一个目录下所有文件(子目录除外)属于同一个package
go 开发工具
go install
编译并安装,这个命令在内部实际分成两步:
- 第一步生成结果文件件(可执行文件或.a包)
- 第二步把编译好的结果移到
$GOPATH/pkg
或者$GOPATH/bin
中1234567go 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可以是绝对路径
示例
go build 会忽略目录下以”_”或”.”开头的go文件
go build时会选择性地编译以系统名结尾的文件(linux/darwin/windows/freebsd),例如linux系统下面编译只会选择array_linux.go文件
go clean
go clean
用来移除当前源码包和关联源码包编译生成的文件
go clean -i -n -r
|
|
go get
|
|
当有多个GOPATH时,默认会将go get的内容放在第一个GOPATH目录下
go test 单元测试
|
|
其他
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文件任意一行增加如下配置
触发generate
|
|
Go程序设计的一些规则
1.大写字母开头的变量是可导出的,也就是其他包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量
2.大写字母开头的函数也是一样,相当于class中的public关键字的公有函数;小写字母开头的就是私有函数
go doc
本地开启http godoc, 访问127.0.0.1:6060
|
|
常量
所谓常量就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。
Go中常量可以定义为数值、布尔值或字符串等类型
定义
|
|
变量
定义变量
|
|
等于简短声明
|
|
:=取代了var和type
:=只能在函数内部使用,在外部无法编译通过,所以一般用var 来定义全局变量
_
是个特殊变量名,任何赋予它的值将被抛弃
|
|
Go对于已声明但未使用的变量会在编译阶段报错
例如
|
|
格式化输出变量
语法 | 说明 | 示例 |
---|---|---|
%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 | 输出地址 |
|
|
类型
类型 | |
---|---|
Boolean | 值类型 |
string | 值类型 |
数组array | 值类型 |
整型/浮点型 | 值类型 |
struct | 值类型 |
Slice | 引用类型 |
map | 引用类型 |
Channel | 引用类型 |
值类型
Boolean
布尔类型为bool,值是true或false,默认false
注意: 不可以用数字来代表true/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位,但不能互用
字符串
|
|
比较
|
|
遍历
|
|
数组
在go中数组是值类型,数组声明后,它的数据类型和长度都不能再改变,如果需要更多元素,那么只能声明新的数组,然后将原数组内容拷贝过去。
数组可以使用==
或!=
,但是不可以使用<
或>
声明和初始化
|
|
使用数组
|
|
在函数中传递数组
在函数中传递数组是非常昂贵的行为,因为函数间传递变量永远是值传递,所以如果变量是数组,那么意味着整个数组的复制。
举个例子,传建一个有百万元素的数组,在64位机器上它需要8M的空间,来看看我们声明它和传递它时发生了什么
|
|
每一次foo被调用,8M内存将会被分配在栈上,一旦函数返回,会弹栈并释放内存,每次都要8M空间。
更好的方法是使用slice。
联想
- 能不能定义一个二维数组,使得外层与内层数据类型不统一?如
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中
可以覆盖匿名字段的方法
所有值类型和自定义类型都可以作为匿名字段
123456789type T struct {int}func main() {a := new(StructAnonymousFieldType)a.int = 1fmt.Println(a.int)}
demo
|
|
结构体成员访问顺序
如果多个匿名字段有相同的成员,访问结构体成员时需要显示表明匿名字段
同名的结构体成员存在覆盖的现象
访问优先级 当前对象成员—> 匿名函数成员
123456789101112131415161718192021222324252627282930 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{TeacherFather}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
|
|
引用类型
引用类型和值类型恰恰相反,它的修改可以影响到任何引用到它的变量。在Go语言中,引用类型有切片、map、interface、函数类型以及chan。
引用类型之所以可以引用,是因为我们创建引用类型的变量,其实是一个标头值,标头值里包含一个指针,指向底层的数据结构,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层结构并没有被复制传递,这也是引用类型传递高效的原因。
本质上,我们可以理解函数的传递都是值传递,只不过引用类型传递的是一个指向底层数据的指针
|
|
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)]
声明定义
|
|
创建新切片的时候,最好让新切片的长度和容量一致,这样我们在追加操作时会生成新的底层数组(和原有数组分离),避免了因共用底层数组而引发奇奇怪怪的问题(共用数组修改内容,会影响多个切片)
|
|
slice操作
|
|
slice重组(reslice)
|
|
s的长度是3,无法使用s[3]
,所以报超出索引范围错误。
不过这时我们发现底层对应数组长度是10个,可不可以通过改变切片长度来达到目的呢?
|
|
这样就完成了对切片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扩容
例
|
|
slice做函数参数
我们知道slice是3个字段构成的结构体类型,所以在函数间以值的形式传递的时候,占用的内存非常小,成本很低。在传递复制切片的时候,底层数据不会被复制,也不会受到影响,复制的只是切片本身。
|
|
打印输出
0xc420082060
0xc420082080
[1 10 3 4 5]
仔细看这两个切片地址是不一样的,可以确认切片在函数间使用值传递。而我们修改一个索引的值后,发现原切片值也改了,说明他们共用一个底层数组。
在函数间传递切片的效率是很高的。这也是为什么函数间传递参数使用切片而不是数组的原因
空切片和nil切片
空切片和nil切片长度和容量都是0,但是它们指向底层数组的指针不一样,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是个地址。
nil切片表示不存在的切片,而空切片表示一个空集合,它们各有用处。
|
|
Map
map是一种无序
的键值对的集合。map最重要的就是通过key来快速检索数据,key相当于索引,指向数据的值。
map也是一种引用类型,map的键可以是任意内置类型或struct类型,像切片、函数以及含有切片的结构类型就不能用于map的键了,因为他们具有引用的语义,不可比较。map的值可以是能够使用==操作的类型。
map与其他基本类型不同,他不是线程安全的,在多个协程存取时,必须使用Mutex lock机制
声明定义
|
|
操作
|
|
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
见并发章节
自定义类型
|
|
示例:
|
|
我们在使用time这个包的时候,对于类型time.Duration应该非常熟悉,它其实就是基于int64 这个基本类型创建的新类型,来表示时间的间隔。
但是这里我们注意,虽然Duration是基于int64创建,觉得他们其实一样,比如都可以使用数字赋值。
|
|
但是本质上,他们并不是同一种类型,所以对于Go这种强类型语言,他们是不能相互赋值的。
|
|
上面的例子,在编译的时候,会报类型转换的异常错误。
cannot use int64(100) (type int64) as type Duration in assignment
错误类型
Go内置一个error类型,专门用来处理错误信息,Go的package里还专门有一个errors包用来处理错误
|
|
make/new操作
内建函数make(T,args)
只能创建slice/map/channel,并且返回一个有初始值的T类型
内建函数new本质上和其他语言同名函数功能一样,new(T)分配了零值填充的T类型的内存空间,并且返回其地址
,即一个*T类型的值。用Go的术语来说,它返回了一个指针,指向新分配的类型T的值。
示例:
|
|
零值
go中任意类型被声明后,默认都会被初始化为各自的零值。
|
|
Go 标志符可见性
Go的标志符,这个翻译觉得怪怪的,不过还是按这个起了标题,可以理解为Go的变量、类型、字段等。这里的可见性,也就是说那些方法、函数、类型或者变量字段的可见性,比如哪些方法不想让另外一个包访问,我们就可以把它们声明为非公开的;如果需要被另外一个包访问,就可以声明为公开的,和Java语言里的作用域类似。
在Go语言中,没有特别的关键字来声明一个方法、函数或者类型是否为公开的,Go语言提供的是以大小写的方式进行区分的,如果一个类型的名字是以大写开头,那么其他包就可以访问;如果以小写开头,其他包就不能访问。
|
|
|
|
这是一个定义在common包里的类型count,因为它的名字以小写开头,所以我们不能在其他包里使用它,否则就会报编译错误。
|
|
因为这个类型没有被导出,如果我们改为大写,就可以正常编译运行了,大家可以自己试试。
现在这个类型没有导出,不能使用,现在我们修改下例子,增加一个函数,看看是否可行。
|
|
|
|
这里我们在common包里定义了一个导出的函数New ,该函数返回一个count类型的值。New函数可以在其他包访问,但是count类型不可以,现在我们在main包里调用这个New函数,会发现是可以正常调用并且运行的,但是有个前提,必须使用:=这样的操作符才可以,因为它可以推断变量的类型。
这是一种非常好的能力,试想,我们在和其他人进行函数方法通信的时候,只需约定好接口,就可以了,至于内部实现,使用方是看不到的,隐藏了实现。
|
|
|
|
以上例子,我们对于函数间的通信,通过Loginer接口即可,在main函数中,使用者只需要返回一个Loginer接口,至于这个接口的实现,使用者是不关心的,所以接口的设计者可以把defaultLogin类型设计为不可见,并让它实现接口Loginer,这样我们就隐藏了具体的实现。如果以后重构这个defaultLogin类型的具体实现时,也不会影响外部的使用者,极为方便,这也就是面向接口的编程。
假如一个导出的结构体类型里,有一个未导出的字段,会出现怎样的问题。
|
|
当我们在其他包声明和初始化User的时候,字段email是无法初始化的,因为它没有导出,无法访问。此外,一个导出的类型,包含了一个未导出的方法也一样,也是无法访问的。
我们再扩展,导出和未导出的类型相互嵌入,会有什么样的发现?
|
|
被嵌入的user是未导出的,但是它的外部类型Admin是导出的,所以外部可以声明初始化Admin。
|
|
这里因为user是未导出的,所以我们不能再使用字面值直接初始化user了,所以只能先定义一个Admin类型的变量,再对Name字段初始化。这里Name可以访问是因为它是导出的,在user嵌入到Admin中时,它已经被提升为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
|
|
数组指针(指向数组的指针)
|
|
指针数组
|
|
流程控制
if
go中的if支持初始化表达式
|
|
for
|
|
goto、continue和break
|
|
switch
go中的switch不需要写break
|
|
函数
Go函数不支持嵌套、重载和默认参数
但是支持以下特性:
- 无需声明原型
- 不定长度变参
- 多返回值
- 命名返回值参数
- 匿名函数
- 闭包
函数也可以作为一种类型来使用
定义
|
|
如果没有返回值,可以省略返回类型及return
多个返回值
Go语言比C先进的地方,其中一点就是函数可以有多个返回值。
|
|
同时我们的出错异常信息再也不用像Java一样使用一个Exception这么重的方式表示了,非常简洁
|
|
参数
任何类型(包括slice、map、channel)参数传递采用的都是值拷贝
|
|
指针做参数
传递大量数据时,使用值拷贝显然是不合理的,此时我们使用指针作为参数
|
|
不定长变参
|
|
- 不定长变参只能作为函数参数的最后一个
- 接收的参数arg是一个slice
|
|
传值与传指针
当我们传一个参数值到被调用函数时,实际上是传了这个值的一个copy,当被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因此数值变化只作用在copy上
传指针的优势
- 传指针使得多个函数可以操纵一个对象
- 传指针更轻量级(8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数传递的话,在每次copy上面就会花费较多的系统开销(内存和时间),所以当你要传递大的结构体的时候用指针是一个明智的选择
Go语言中channel,slice,map三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针(注: 如果需要改变slice的长度,则仍需要取地址传递指针)
|
|
闭包
|
|
这时候我们需要闭包来解决问题
匿名函数内变量以参数形式传入,在传入时获得了拷贝,这就是闭包
通过参数传入
|
|
又如
结果
|
|
由于闭包内保存了当时的环境,所以两次访问的是同一个x值
不通过参数传入
不通过参数传入,则是某个变量的引用
defer(延迟语句)
执行方式类似其他语言的析构函数,在函数体执行结束后按照调用顺序逆序执行(出栈),常用于资源清理、文件关闭、解锁以及记录时间等操作。
即使函数发生严重错误,defer依旧会执行。
|
|
|
|
函数作为值类型
Go中函数也是一种变量,我们可以通过type定义它,它就是所有拥有相同参数、相同返回值的一种类型
|
|
|
|
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可以捕获
|
|
例2
|
|
结果
Calling test
Panicing bad end
Test completed
main和init函数
Go里有两个保留的函数: init函数(能应用于所有package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何参数和返回值。
执行顺序
import -> const -> var -> init() -> main()
import
import
有以下几点需要了解
- 一个包被其它多个包 import,但只能被初始化一次
- 其他的包只有被 main 包 import 才会执行,按照 import 的先后顺序执行
- 被递归 import 的包的初始化顺序与 import 顺序相反,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main
相对路径(不建议)
import “./model”
绝对路径
import “shorturl/model”
多种用法
点操作
|
|
别名操作
|
|
_操作
|
|
init
- 一个package中可以有多个
init
函数,每个文件都可以有自己的init
方法 (建议在一个package中每个文件最多只写一个init函数) - 同一个包的
init
执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。 - 对于不同的 package,如果不相互依赖的话,按照 main 包中 import 的顺序调用其包中的
init
函数 - 如果 package 存在依赖,调用顺序为最后被依赖的最先被初始化,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main,一次执行对应的 init 方法。
- main 包总是被最后一个初始化,因为它总是依赖别的包
面向对象
方法
|
|
类型方法相对于函数多了一个接收者
注意:
- 方法名相同但是接收者不一样,表示的是不同的方法
- 接受者可以是任意的类型,如自定义类型、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会自动取指针再调用
|
|
再来一个例子
|
|
我们都知道,如果要实现一个接口,必须实现这个接口提供的所有方法,但是实现方法的时候,我们可以使用指针接收者实现,也可以使用值接收者实现,这两者是有区别的,下面我们就好好分析下这两者的区别。
|
|
还是原来的例子改改,增加一个invoke函数,该函数接收一个animal接口类型的参数,例子中传递参数的时候,也是以类型cat的值c传递的,运行程序可以正常执行。现在我们稍微改造一下,使用类型cat的指针&c作为参数传递。
|
|
只修改这一处,其他保持不变,我们运行程序,发现也可以正常执行。通过这个例子我们可以得出结论:实体类型以值接收者实现接口的时候,不管是实体类型的值,还是实体类型值的指针,都实现了该接口。
下面我们把接收者改为指针试试。
|
|
这个例子中把实现接口的接收者改为指针,但是传递参数的时候,我们还是按值进行传递,点击运行程序,会出现以下异常提示:
|
|
提示中已经很明显的告诉我们,说cat没有实现animal接口,因为printInfo方法有一个指针接收者,所以cat类型的值c不能作为接口类型animal传参使用。下面我们再稍微修改下,改为以指针作为参数传递。
|
|
其他都不变,只是把以前使用值的参数,改为使用指针作为参数,我们再运行程序,就可以正常运行了。由此可见实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口
现在我们总结下这两种规则,首先以方法接收者是值还是指针的角度看。
(t T) T and T
(tT) *T
如果方法接受者是值接收者,实体类型的值和指针都可以实现对应的接口;如果是指针接收者,那么只有类型的指针能够实现对应的接口。
其次我们我们以实体类型是值还是指针的角度看。
T (t T)
T (t T) and (tT)
上面的表格可以解读为:类型的值只能实现值接收者的接口;指向类型的指针,既可以实现值接收者的接口,也可以实现指针接收者的接口。
接收者
对类型进行操作的时候,是要改变当前值,还是要创建一个新值进行返回?这些决定我们是采用值传递,还是指针传递。
值接收者
值接收者的方法在调用的时候其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。
指针接收者
指针作为接收者,可以对成员本身做修改(指针接收者传递的是一个指向原值指针的副本,指针的副本指向的还是原来类型的值,所以修改时会影响原类型变量的值)
|
|
方法继承
struct嵌入昵称字段,可以继承匿名字段的方法
嵌入非匿名字段,struct不可使用匿名字段方法
|
|
方法重写
如果Student要实现自己的GetName方法,重新定义receiver为Student的GetName方法即可
|
|
interface
什么是interface
interface是一组方法的组合,我们通过interface来定义对象的一组行为。
接口是用来定义行为的类型,它是抽象的,这些定义的行为不是由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型,实现了接口类型声明的所有方法,那么这个用户定义的类型就实现了这个接口,所以这个用户定义类型的值就可以赋值给接口类型的值
|
|
interface的值
|
|
空interface
interface{}不包含任何方法,所以任何类型都实现了空interface
如果使用空interface作为函数参数,那么该参数可以接受任意类型的值,如果一个函数返回interface{},那么它也就可以返回任何类型的值
|
|
interface做函数参数
将对象赋值给接口时会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针
例如fmt.Println 只要参数实现了Stringer接口 都可以作为参数传入
|
|
interface转具体类型
我们知道interface类型可以存储任意类型的值,那么我们怎么反向知道这个变量里实际保存了的是哪个类型的对象呢?目前有2个方法
Comma-ok断言
value,ok:=element.(T) 其中T是断言的类型,element是变量,如果element里确实存储了T类型的数据,返回true,否则返回false
只有接口类型才能进行断言
|
|
switch
|
|
单case存在多个值的情况
|
|
interface嵌套
|
|
struct与interface区别
interface相当于抽象的一组行为
|
|
struct是实体,包含属性和具体行为
虽然语法上可以有包含实现等等,但是实际上两者相互独立,不可替代
|
|
反射
|
|
|
|
并发
并发的含义
并发: 逻辑上具备同时处理多个任务
并行: 物理上同一时刻执行多个并发任务
以咖啡机为例,两个队列,一个Coffee机器,机器不断替换为2个队列提供服务,这个叫并发;两个队列,两个Coffee机器,两台机器同时工作,这个叫并行
根本上来说,并发主要由切换时间片来实现”同时”运行,并行则是直接利用多核实现多线程的运行
根据go程序开启线程数不同,goroutine有以下表现:
限制开启一个线程
当go只开启一个线程的时候,所有goroutine是并发。在同一个原生线程里,如果当前goroutine不发生阻塞,则不会让出CPU时间给其他同线程goroutine(详情见GPM模型)开启多线程
为了达到真正的并行,需要让go程序开启多个线程(现代版本自动开启),这种情况是并发+并行
Goroutine
什么是Goroutine
Goroutine是go语言并行设计的核心,它是一种比系统线程更小的用户态轻量级线程,十多个goroutine在系统层面可能只是五六个线程。
优点:
- 为go语言提供了原生并发编程支持
- 大大简化并发编程成本
goroutine中可以再开goroutine
如何开启Goroutine
|
|
开启goroutine背后的事情
关键字go并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适的系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动且运行时也不保证并发任务的执行次序。
每次个任务单元除了保存函数指针、调用参数等还会分配执行所需的栈内存空间,相比系统默认MB级别的线程栈,goroutine自定义栈初始为2KB,所以可以创建大量并发任务。自定义栈采用按需分配策略,在需要时进行扩容,最大能到GB规模。
数据同步
goroutine奉行通过通信来共享内存,而不是共享内存来通信
runtime
|
|
特殊的Goroutine
main函数本身启动一个协程。特别的是,main函数是执行程序的入口,如果main本身执行完毕,表示程序执行完毕,结果是main中启动的goroutine也会被关闭。
Channel(信道)
goroutine运行在相同的内存地址,因此访问共享内存必须做好同步,这方面Go提供了channel通信机制。
- channel 是引用类型,通常通过make创建,close关闭
for i:=range c
会从信道c中循环读取数据,直至信道关闭且无缓冲数据。- 信道被关闭后是只读的,没有数据的话返回nil,对已关闭信道写入数据会引发panic。
- 在发送方进行关闭信道,不要在接收方进行关闭操作。
- 信道和文件不同,你通常可以不关闭它。只有在必须告诉接受者信道中不会再有数据时才有必要关闭信道,比如在需要终止range循环的时候。
|
|
获取的数据是原传值的拷贝
无缓冲信道
无缓冲信道的长度为0,永远不存储数据,只负责数据流通
由于长度为0,所以无缓冲channel发送和接收数据都是阻塞的。意思是<-ch会读取ch中的数据,没有数据则阻塞,直至有数据为止;ch<-5如果ch本身有数据,则阻塞直到ch数据被读取消费,然后执行插入。
创建无缓冲信道
|
|
常见操作
|
|
缓冲信道
缓冲信道简单理解就是可以存储元素的信道,我们可以把缓冲信道看做一个线程安全的队列
缓冲信道操作:
- <-ch读取ch数据,ch无数据(干涸)则阻塞,否则可以一直读取
- ch<-x向ch插入数据,如果此时ch容量满则阻塞,否则可以一直插入
举个例子,c:=make(chan int,1) 这样c可以缓存一个数据,也就是说,放入一个数据,c不会挂起当前协程,再放入一个才会挂起直到第一个数据被其他goroutine取走,也就是不达容量不阻塞
想知道信道的容量及里面有几个元素数据怎么办?使用cap和len就可以了
|
|
上面例子中我们使用消费了3次,但通常我们并不知道channel中有多少数据,无法明确指定消费多少次,那么有没有办法遍历channel,缓冲区有多少消费多少呢?这时我们可以使用range和close解决问题
|
|
无缓冲信道与缓冲信道差别
无缓冲不能存储数据,有缓冲数据可以存储数据
|
|
单向信道
|
|
select
select用于监听信道中的数据流动, 对应的case关键词后只能是IO操作。
在使用上select默认是阻塞的,当监听的channel中有可以发送或接收时数据时才会运行;当多个channel都准备好时,select是随机的选择一个执行的;case中的default用于设定当监听的channel都没有准备好的时候的默认执行动作(此时select不阻塞channel)
可用空的select来阻塞main函数
值得注意的一点,当信道关闭时可以从信道中取得值(0),与select搭配使用时如果select监听的某个信道被关闭,其default逻辑将永远不会被执行到,正确的做法是当检测到信道关闭且信道中不再有值时将channel置为nil(nil channel发生阻塞),从而达到关闭这条数据链路的目的。
|
|
设置超时
有时候会出现goroutine出现阻塞的情况,那么如何避免整个程序进入阻塞呢?使用select来设置超时
|
|
注意此时没有default,因为如果有default的话每次select都会执行default动作指令
死锁
两种可能:
- 信道已满,我们还要加入数据
- 信道干涩,我们一直向无数据流入的空信道取数据
无缓冲信道上发生流入无流出,流出无流入便会导致死锁
并发竞争
我们对于同一个资源的读写必须是原子化的,也就是说,同一时间只能有一个goroutine对共享资源进行读写操作。
检查程序是否有资源竞争
|
|
|
|
找到一个资源竞争,连在那一行代码出了问题,都标示出来了。goroutine 7在代码25行读取共享资源value := count,而这时goroutine 6正在代码28行修改共享资源count = value,而这两个goroutine都是从main函数启动的,在16、17行,通过go关键字。
更简单的一个例子
|
|
既然我们已经知道共享资源竞争的问题,是因为同时有两个或者多个goroutine对其进行了读写,那么我们只要保证,同时只有一个goroutine读写不就可以了,现在我们就看下传统解决资源竞争的办法–对资源加锁。
并发竞争解决方案
Go语言提供了atomic包和sync包里的一些函数对共享资源同步加锁,有CAS和互斥锁两种解决方法
CAS(atomic)
|
|
CAS(compare and swap) 比较后再置换,CAS表示乐观,不实际加锁,性能比加锁要好,但是有可能失败,所以要使用for循环直至成功
atomic虽然可以解决资源竞争问题,但是比较都是比较简单的,支持的数据类型也有限,所以Go语言还提供了一个sync包,这个sync包里提供了一种互斥型的锁,可以让我们自己灵活的控制哪些代码,同时只能有一个goroutine访问,被sync互斥锁控制的这段代码范围,被称之为临界区,临界区的代码,同一时间,只能有一个goroutine访问。刚刚那个例子,我们还可以这么改造。
原子操作
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(即线程切换)
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,将整个操作视作一个整体是原子性的核心特征
互斥锁(mutex或RWMutex)
|
|
互斥锁属于悲观锁,性能比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
|
|
简单记就是写写冲突、读写冲突、读读不冲突
读锁与不加锁区别
不加锁读取时可能在并发写入,导致读取一半数据的情况。
加读锁读取时不能写入
channel
并发控制方案
sync.Workgroup
select+chan
context
并发应用
生成器
|
|
服务化
|
|
多路复合
|
|
select监听信道
|
|
随机数生成器
|
|
定时器
|
|
Go内存模型
go的内存模型对并发安全有两种保护措施。一种是通过加锁保护,另一种是通过channel来保护。后者其实就是一个线程安全的队列。
换句话说就是,在go语言中,当有多个goroutine并发操作同一个变量时,除非是全都是只读操作,否则就得加锁或者使用channel来保证并发安全。
go内存模型
错误处理
测试
web基础
文本文件处理
XML处理
JSON处理
socket编程
聊天室
|
|
参考
Websocket
websocket相对于传统Http有如下好处:
- 一个web客户端只建立一个连接
- websocket服务端可以推送数据到服务端
- 有更轻量级的头,减少数据传输量
websocket起始输入是ws://或wss://(在ssl上)
客户端
|
|
服务器
|
|
标准库
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位小数
依赖管理
调试
性能分析
编译
交叉编译
Mac 下编译, Linux 或者 Windows 下去执行
|
|
参考
FAQ
1. 在go中如何表示字符类型变量
go没有常规意义上的字符类型。字符变量通常使用byte来存储(byte是uint8的别称)
|
|
2. T是结构体,为什么T.element和*T.element都可以访问到属性
这是go的一个语法糖。
|
|
3.字符与整数
在go中,字符类型和整数类型都属于数值类型,可以相互转化比较
|
|
结果
match
47 47
uint8 int32
4.在itoa.go中这段代码如何理解
|
|
这一条语句是数组的初始化,大括号内的每个逗号前的内容代表一个元素,例如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
|
|
6.闭包与非闭包差异
|
|
i不是通过匿名函数参数传入,所以使用的是外部变量的引用,外部变量i在循环结束后是3,所以goroutine中获取到的i是3
解决方法有两种,一是循环内重新声明局部变量,二是通过匿名函数参数传参
方法一
|
|
方法二
通过匿名函数的参数进行传参(闭包),此时匿名函数中获取到的值是当时传入的值,所以得到结果1 2 3
|
|
7. defer错误用法
|
|
如果studygolang.txt找不到,file无法成功获取,file.Close()就会panic。
正确做法是把defer放到错误检查之后
|
|
8.goroutine问题。该示例中只开启一个线程,为什么ch阻塞后goroutine会继续运行
|
|
答: 当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]
[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读不到数据的问题
|
|
没有读取到内容,原因是用于存放内容的slice容量为0,显式加上缓冲容量后成功拿到数据
|
|
12.如何正确读取tcp连接中的数据
|
|
13.如何获取整数类型的绝对值
|
|
math.abs()限定输入类型是float64,而且我发现math中大多数运算时数据类型都是float64
14.结构体加锁失效问题
|
|
其中d.Lock
在并发场景下是无效的,原因在于d data
是值接收者,d是原值的拷贝,锁同样是拷贝,导致每次test方法中的锁不是同一把锁
解决方案1: receiver用 *receiver
解决方案2: 结构体中的锁用指针 *sync.Mutex
最佳实践是结构体加锁直接使用匿名成员sync.Mutex,不要使用*sync.Mutex
15.函数类型传值
函数类型与普通类型不同
只要与函数参数及返回值相同,即可传入
|
|
16.通过go build和go run结果不同 怎么办
使用go build -n
和go run -n
对比查看打印出执行的内容,其中-n
表示打印内容且不执行
17.slice和map初始化问题
map使用前注意要初始化(make),make(map[int]int)
slice超出边界会引发报错,推荐使用append向slice添加元素
18.切片...
的使用
|
|
如何确保类型满足某接口
|
|
如果T或*T没有实现I,在编译时可以捕获到相应错误
字段嵌套字段实现了继承吗
go中没有继承的概念,go所做的是通过嵌入字段的方式实现了类型之间的组合,更为灵活
踩坑记录
练习题
答案如下
参考
Go语言并发与并行学习笔记(一)
Go语言并发与并行学习笔记(二)
Go语言并发与并行学习笔记(三)
Go语言标准库
神奇的go语言