Go语言学习笔记Part01
参考资料
Go语言开发环境搭建
安装go开发包
下载:官方镜像站 https://golang.google.cn/dl/
cmd输入go version查看版本
- 配置GO PATH
新建一个文件夹作为自己的工作文件夹(存放go语言源码)
计算机-属性-高级系统设置-环境变量
添加新的系统变量
GOPATH
,文件夹为自己的工作文件夹
- 在工作文件夹下新建三个文件夹
bin
、src
、pkg
- 将
bin
目录添加到PATH
环境变量中
- 重新打开cmd,使用
go env
查看环境变量配置
下载安装vs code
具体步骤略
安装Go扩展包
编译
使用go build
- 在项目目录下执行
go build
- 在其他路径下需要在指令后加上项目路径,生成的可执行文件在当前目录
使用GoLand编译器
GoLand:由 JetBrains 公司开发的一个新的商业 IDE;
GoLand目前不是免费软件,在GoLand官网下载软件之后需要进行破解。
PS:网上现有的破解码都没法用,参考https://shimo.im/docs/dKYCkd8PrX3ckX99/read使用插件的方式进行破解使用
GO语言学习笔记part02
Go语言基础语法
标识符
25个关键字+37个保留字
变量
Go语言中的变量必须先声明再使用
var s1 string
:声明一个保存字符串类型的s1变量
1 | var name string |
批量声明:
1 | var( |
Go语言中 ,变量声明了必须使用,否则无法通过编译
可以在声明的同时进行初始化
var name string = "aa"
或者一次初始化多个变量
var name,age = "bb",20
类型推导
1 | var name = "cc" |
简短变量声明
只能在函数内部使用
1 | s3 := "ddd" |
匿名变量
匿名变量使用一个_
表示
1 | func foo(int,string){ |
注意事项:
- 函数外的每个语句都必须以关键字开始
:=
不能用在函数外_
多用于占位,表示忽略值
常量
const
关键字
1 | const pi = 3.1415 |
1 | //批量声明常量 |
1 | //常量声明时如果省略初始值,则默认和上一行相同 |
iota
iota
是go语言的常量计数器,只能在常量表达式中使用
iota
在const关键字出现的时候被重置为0,const中每新增一行常量声明将使iota
计数一次
1 | const( |
1 | //定义数量级 |
基本数据类型
整型
类型 | 描述 |
---|---|
uint8 | 无符号8位整型(0~255) |
uint16 | 无符号16位整型(0~65535) |
uint32 | 无符号32位整型(0~2^32-1) |
uint64 | 无符号64位整型(0~2^64-1) |
int8 | 有符号8位整型 |
int16 | 有符号16位整型 |
int32 | 有符号32位整型 |
int64 | 有符号64位整型 |
特殊整型
类型 | 描述 |
---|---|
uint | 32位操作系统上就是uint32,64位操作系统上就是uint64 |
int | 32位操作系统上就是int32,64位操作系统上就是int64 |
uintptr | 无符号整型,用于存放一个指针 |
八进制&十六进制
1 | var b int = 077 //八进制,0开头 |
格式化输出:%b
、%o
、%x
分别表示二进制、八进制、十六进制
布尔值
略
字符串
Go语言中字符串用双引号包裹的,字符用单引号包裹
多行字符串
1 | //多行字符串 |
字符串常用操作:
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.contains | 判断是否包含 |
strings.HasPrefix,string.HasSuffix | 前缀/后缀判断 |
strings.Index(),string.LastIndex | 子串出现的位置 |
strings.Join(a[]string,sep string) | join操作 |
流程控制
if条件判断
略
for循环
1 | //基本格式 |
1 | //无限循环 |
for循环可以通过break
、goto
、return
、panic
语句强制退出循环
for range
Go语言中可以使用for range
遍历数组、切片、字符串、map 及通道(channel)。 通过for range
遍历的返回值有以下规律:
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
switch和goto
switch基本使用和其他语言类似,和C不同的是执行完一个case就结束了
一个分支可以有多个值,多个case值中间使用英文逗号分隔。
1 | func testSwitch3() { |
分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。例如:
1 | func switchDemo4() { |
fallthrough
语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。
goto可以快速跳转,不建议在代码中使用
continue和break
略
运算符
略
复合数据类型
Array数组
数组定义:
1 | var 数组变量名 [元素数量]T |
数组初始化:
1 | func main() { |
1 | //按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度 |
1 | //我们还可以使用指定索引值的方式来初始化数组 |
切片(slice)
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
声明切片类型的基本语法如下:
1 | var name []T |
1 | func main() { |
切片拥有自己的长度和容量,我们可以通过使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量。
由数组得到切片:
1 | a1 := []int{1,3,5,7,9,11,13} |
切片的容量指底层数组从切片的第一个元素到最后一个的容量
1 | func main() { |
输出结果为:s:[2 3] len(s):2 cap(s):4
注意:
对于数组或字符串,如果0 <= low <= high <= len(a)
,则索引合法,否则就会索引越界(out of range)。对切片再执行切片表达式时(切片再切片),high
的上限边界是切片的容量cap(a)
,而不是长度。
make函数创建切片
1 | make([]T,size,cap) |
其中:
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
切片的比较:切片之间不能比较,唯一合法的比较是和 nil
比较。所以要判断一个切片是否是空的,要是用len(s) == 0
来判断,不应该使用s == nil
来判断。
切片的扩容
append方法
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
1 | func main(){ |
切片复制
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
1 | copy(destSlice, srcSlice []T) |
其中:
- srcSlice: 数据来源切片
- destSlice: 目标切片
切片删除
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
1 | func main() { |
指针
GO语言不存在指针操作,只需要记住两个符号
&
取地址*
根据地址取值
new和make
new是一个内置的函数,它的函数签名如下:
1 | func new(Type) *Type |
其中,
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
1 | func make(t Type, size ...IntegerType) Type |
make和new的区别
- make和new都是用来申请内存的
- new很少用,一般用来给基本数据类型申请内存,返回对应类型的指针
- make用来给slice、map、chan申请内存,返回引用类型(本身)
map
- Go语言中
map
的定义语法如下:
1 | map[KeyType]ValueType |
其中,
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
1 | make(map[KeyType]ValueType, [cap]) |
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
- Go语言中有个判断map中键是否存在的特殊写法,格式如下:
1 | value, ok := map[key] |
- Go语言中使用
for range
遍历map。
使用delete()
内建函数从map中删除一组键值对,delete()
函数的格式如下:
1 | delete(map, key) |
函数
Go语言中定义函数使用func
关键字,具体格式如下:
1 | func 函数名(参数)(返回值){ |
其中:
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分隔。 - 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。 - 函数体:实现指定功能的代码块。
参数
类型简写
函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:
1 | func intSum(x, y int) int { |
- 可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识。
注意:可变参数通常要作为函数的最后一个参数。
举个例子:
1 | func intSum2(x ...int) int { |
返回值
Go语言中通过return
关键字向外输出返回值。
- Go语言中函数支持多返回值,函数如果有多个返回值时必须用
()
将所有返回值包裹起来。
1 | func calc(x, y int) (int, int) { |
- 函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过
return
关键字返回。
1 | func calc(x, y int) (sum, sub int) { |
函数类型与变量
- 定义函数类型
我们可以使用type
关键字来定义一个函数类型,具体格式如下:
1 | type calculation func(int, int) int |
上面语句定义了一个calculation
类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
我们可以声明函数类型的变量并且为该变量赋值:
1 | func main() { |
闭包
如以下代码段所示代码,
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
。 首先我们来看一个例子:
1 | func adder() func(int) int { |
变量f
是一个函数并且它引用了其外部作用域中的x
变量,此时f
就是一个闭包。 在f
的生命周期内,变量x
也一直有效。
Go学习笔记part03
defer
Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
举个例子:
1 | func main() { |
输出结果:
1 | start |
内置函数介绍
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
fmt标准库
fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。
占位符 | 说明 |
---|---|
%v | 值的默认格式表示 |
%+v | 类似%v,但输出结构体时会添加字段名 |
%#v | 值的Go语法表示 |
%T | 打印值的类型 |
%% | 百分号 |
结构体相关
类型别名和自定义类型
自定义类型
在Go语言中有一些基本的数据类型,如string
、整型
、浮点型
、布尔
等数据类型, Go语言中可以使用type
关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
1 | //将MyInt定义为int类型 |
通过type
关键字的定义,MyInt
就是一种新的类型,它具有int
的特性。
类型别名
类型别名是Go1.9
版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
1 | type TypeAlias = Type |
我们之前见过的rune
和byte
就是类型别名,他们的定义如下:
1 | type byte = uint8 |
结构体
使用type
和struct
关键字来定义结构体,具体代码格式如下:
1 | type 类型名 struct { |
其中:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
1 | type person struct{ |
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
1 | package main |
结构体指针
我们还可以通过使用new
关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
1 | var p2 = new(person) |
需要注意的是在Go语言中支持对结构体指针直接使用.
来访问结构体的成员。
1 | var p2 = new(person) |
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作。
结构体初始化
键值对初始化
列表初始化
结构体是值类型,赋值操作都是拷贝
空结构体不占空间
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person
的构造函数。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
1 | func newPerson(name, city string, age int8) *person { |
1 | p9 := newPerson("张三", "沙河", 90) |
方法和接收者
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
。
方法的定义格式如下:
1 | func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { |
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
接收者多用类型名首字母小写表示
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this
或者self
。 例如我们为Person
添加一个SetAge
方法,来修改实例变量的年龄。
1 | // SetAge 设置p的年龄 |
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
1 | // SetAge2 设置p的年龄 |
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
1 | //MyInt 将int定义为自定义MyInt类型 |
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
1 | //Person 结构体Person类型 |
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
略
结构体的“继承”
1 | //Animal 动物 |
结构体的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体与JSON
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。
1 | //Student 学生 |
Go语言学习笔记part4
接口(interface)
接口是一种抽象的类型。
接口的定义
每个接口由数个方法组成,接口的定义格式如下:
1 | type 接口类型名 interface{ |
其中:
- 接口名:使用
type
将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
1 | type speaker interface{ |
接口的实现
一个变量如果实现了接口中规定的所有类型,所有的方法,那么这个变量就实现了这个接口(这个变量可以成为这个接口类型)。
例子:
1 | // Sayer 接口 |
这里dog
和cat
实现了Sayer
接口,dog
、cat
就成为了Sayer
类型,可以使用Sayer
类型变量调用对应的方法
1 | func main(){ |
1 | func(s Sayer)say{ |
Q:是先定义接口,再实现结构体,还是先实现结构体再抽象出接口?
值接收者和指针接收者的区别
使用值接收者实现接口,那么不管是结构体还是结构体指针都可以赋给该接口变量;
使用指针接收者实现接口,只有结构体指针能赋给该接口变量。
eg.值接收者
1 | type Mover interface { |
eg.指针接收者
1 | func (d *dog) move() { |
实现多个接口和接口嵌套
略
空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
1 | func main() { |
空接口应用
- 空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
1 | // 空接口作为函数参数 |
- 空接口作为map的值
使用空接口实现可以保存任意值的字典。
1 | // 空接口作为map值 |
类型断言
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
一个接口的值(简称接口值)是由一个具体类型
和具体类型的值
两部分组成的。这两部分分别称为接口的动态类型
和动态值
。
我们来看一个具体的例子:
1 | var w io.Writer |
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
1 | x.(T) |
其中:
- x:表示类型为
interface{}
的变量 - T:表示断言
x
可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
包
包(package)
是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt
、os
、io
等。
我们还可以根据自己的需要创建自己的包。一个包可以简单理解为一个存放.go
文件的文件夹。 该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。
1 | package 包名 |
注意事项:
- 一个文件夹下面直接包含的文件只能归属一个
package
,同样一个package
的文件不能在多个文件夹下。 - 包名可以不和文件夹的名字一样,包名不能包含
-
符号。 - 包名为
main
的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main
包的源代码则不会得到可执行文件。
init()函数
在Go语言程序执行时导入包语句会自动触发包内部init()
函数的调用。需要注意的是: init()
函数没有参数也没有返回值。 init()
函数在程序运行时自动被调用执行,不能在代码中主动调用它。
包初始化执行的顺序如下图所示:
Go语言包会从main
包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最先初始化并调用其init()
函数, 如下图示:
Go语言学习笔记part5
文件操作
打开和关闭文件
os.Open()
函数能够打开一个文件,返回一个*File
和一个err
。对得到的文件实例调用close()
方法能够关闭文件。
1 | package main |
读取文件
file.Read()
Read方法定义如下:
1 | func (f *File) Read(b []byte) (n int, err error) |
它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回0
和io.EOF
。
bufio()
bufio是在file的基础上封装了一层API,支持更多的功能。
1 | package main |
ioutil()
io/ioutil
包的ReadFile
方法能够读取完整的文件,只需要将文件名作为参数传入。
1 | package main |
文件写入
os.OpenFile()
函数能够以指定模式打开文件,从而实现文件写入相关功能。
1 | func OpenFile(name string, flag int, perm FileMode) (*File, error) { |
模式 | 含义 |
---|---|
os.O_WRONLY |
只写 |
os.O_CREATE |
创建文件 |
os.O_RDONLY |
只读 |
os.O_RDWR |
读写 |
os.O_TRUNC |
清空 |
os.O_APPEND |
追加 |
perm
:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。
Write()和WriteString()
1 | func main() { |
ioutil.WriteFile()
1 | func main() { |
Go语言学习笔记part6
strconv标准库介绍
strconv包实现了基本数据类型与其字符串表示的转换,主要有以下常用函数: Atoi()
、Itia()
、parse系列、format系列、append系列。
string与int类型转换
Atoi()
Atoi()
函数用于将字符串类型的整数转换为int类型,函数签名如下。
1 | func Atoi(s string) (i int, err error) |
Itoa()
Itoa()
函数用于将int类型数据转换为对应的字符串表示,具体的函数签名如下。
1 | func Itoa(i int) string |
Parse系列函数
Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint()。
ParseBool()
1 | func ParseBool(str string) (value bool, err error) |
返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。
ParseInt()
1 | func ParseInt(s string, base int, bitSize int) (i int64, err error) |
返回字符串表示的整数值,接受正负号。
base指定进制(2到36),如果base为0,则会从字符串前置判断,”0x”是16进制,”0”是8进制,否则是10进制;
bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;
返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。
ParseFloat()
1 | func ParseFloat(s string, bitSize int) (f float64, err error) |
解析一个表示浮点数的字符串并返回其值。
如果s合乎语法规则,函数会返回最为接近s表示值的一个浮点数(使用IEEE754规范舍入)。
bitSize指定了期望的接收类型,32是float32(返回值可以不改变精确值的赋值给float32),64是float64;
返回值err是*NumErr类型的,语法有误的,err.Error=ErrSyntax;结果超出表示范围的,返回值f为±Inf,err.Error= ErrRange。
Format系列
Format系列函数实现了将给定类型数据格式化为string类型数据的功能。
FormatBool()
1 | func FormatBool(b bool) string |
根据b的值返回”true”或”false”。
FormatInt()
1 | func FormatInt(i int64, base int) string |
返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母’a’到’z’表示大于10的数字。
FormatUint()
1 | func FormatUint(i uint64, base int) string |
是FormatInt的无符号整数版本。
FormatFloat()
1 | func FormatFloat(f float64, fmt byte, prec, bitSize int) string |
函数将浮点数表示为字符串并返回。
bitSize表示f的来源类型(32:float32、64:float64),会据此进行舍入。
fmt表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。
prec控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f。
其他
isPrint()
1 | func IsPrint(r rune) bool |
返回一个字符是否是可打印的,和unicode.IsPrint
一样,r必须是:字母(广义)、数字、标点、符号、ASCII空格。
CanBackquote()
1 | func CanBackquote(s string) bool |
返回字符串s是否可以不被修改的表示为一个单行的、没有空格和tab之外控制字符的反引号字符串。
并发
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语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
使用goroutine
Go语言中使用goroutine
非常简单,只需要在调用函数的时候在前面加上go
关键字,就可以为一个函数创建一个goroutine
。
一个goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数。
启动多个goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine
。让我们再来一个例子: (这里使用了sync.WaitGroup
来实现goroutine的同步)
1 | var wg sync.WaitGroup |
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine
是并发执行的,而goroutine
的调度是随机的。
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,但不会太多,切换太频繁的话得不偿失。
GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
Go语言中的操作系统线程和goroutine的关系:
- 一个操作系统线程对应用户态多个goroutine。
- go程序可以同时使用多个操作系统线程。
- goroutine和OS线程是多对多的关系,即m:n。
channel
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine
中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes)
,提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine
是Go程序并发的执行体,channel
就是它们之间的连接。channel
是可以让一个goroutine
发送特定值到另一个goroutine
的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel类型
channel
是一种类型,一种引用类型。声明通道类型的格式如下:
1 | var 变量 chan 元素类型 |
创建channel
通道是引用类型,通道类型的空值是nil
。
1 | var ch chan int |
声明的通道后需要使用make
函数初始化之后才能使用。
创建channel的格式如下:
1 | make(chan 元素类型, [缓冲大小]) |
举几个例子:
1 | //无缓冲区的通道 |
缓冲区
- 无缓冲区的通道,又称阻塞的通道,代码会阻塞在发送这一行,必须有另外一个
goroutine
接收通道中的值,代码才可以继续执行下去。使用无缓冲通道进行通信将导致发送和接收的goroutine
同步化。因此,无缓冲通道也被称为同步通道
。 - 有缓冲区的通道,只要通道的没有占满,代码就不会阻塞。
channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-
符号。
现在我们先使用以下语句定义一个通道:
1 | ch := make(chan int) |
发送:将一个值发送到通道中。
1
ch <- 10 // 把10发送到ch中
接收:从一个通道中接收值。
1
2x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果关闭:我们通过调用内置的
close
函数来关闭通道。1
close(ch)
单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
chan<- int
是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;<-chan int
是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
1 | func counter(out chan<- int) { |
通道总结
channel
常见的异常总结,如下图:
worker pool(goroutine池)
在工作中我们通常会使用可以指定启动的goroutine数量–worker pool
模式,控制goroutine
的数量,防止goroutine
泄漏和暴涨。
一个简易的work pool
示例代码如下:
1 | func worker(id int, jobs <-chan int, results chan<- int) { |
select多路复用
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。为了应对这种场景,Go内置了select
关键字,可以同时响应多个通道的操作。
select
的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select
会一直等待,直到某个case
的通信操作完成时,就会执行case
分支对应的语句。具体格式如下:
1 | select{ |
使用select
语句能提高代码的可读性。
- 可处理一个或多个channel的发送/接收操作。
- 如果多个
case
同时满足,select
会随机选择一个。 - 对于没有
case
的select{}
会一直等待,可用于阻塞main函数。
Go语言学习笔记part7
并发2
互斥锁
Go语言中使用sync
包的Mutex
类型来实现互斥锁
1 | var x int64 |
总结一下,Go语言中的互斥锁在sync包中,操作如下:
1 | import "sync" |
读写锁
1 | import "sync" |
sync.WaitGroup
Go语言中可以使用sync.WaitGroup
来实现并发任务的同步。
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 计数器+delta |
(wg *WaitGroup) Done() | 计数器-1 |
(wg *WaitGroup) Wait() | 阻塞直到计数器变为0 |
sync.Once
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync
包中提供了一个针对只执行一次场景的解决方案–sync.Once
。
sync.Once
只有一个Do
方法,其签名如下:
1 | func (o *Once) Do(f func()) {} |
备注:如果要执行的函数f
需要传递参数就需要搭配闭包来使用。
1 | import "sync" |
sync.Map
Go语言中内置的map不是并发安全的。Go语言的sync
包中提供了一个开箱即用的并发安全版map—sync.Map
。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map
内置了诸如Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
1 | var m = sync.Map{} |
原子操作
atomic包
代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic
提供。
方法 | 解释 |
---|---|
func LoadInt32(addr int32) (val int32) func LoadInt64(addr int64) (val int64) func LoadUint32(addr uint32) (val uint32) func LoadUint64(addr uint64) (val uint64) func LoadUintptr(addr uintptr) (val uintptr) func LoadPointer(addr unsafe.Pointer) (val unsafe.Pointer) |
读取操作 |
func StoreInt32(addr int32, val int32) func StoreInt64(addr int64, val int64) func StoreUint32(addr uint32, val uint32) func StoreUint64(addr uint64, val uint64) func StoreUintptr(addr uintptr, val uintptr) func StorePointer(addr unsafe.Pointer, val unsafe.Pointer) |
写入操作 |
func AddInt32(addr int32, delta int32) (new int32) func AddInt64(addr int64, delta int64) (new int64) func AddUint32(addr uint32, delta uint32) (new uint32) func AddUint64(addr uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) |
修改操作 |
func SwapInt32(addr int32, new int32) (old int32) func SwapInt64(addr int64, new int64) (old int64) func SwapUint32(addr uint32, new uint32) (old uint32) func SwapUint64(addr uint64, new uint64) (old uint64) func SwapUintptr(addr uintptr, new uintptr) (old uintptr) func SwapPointer(addr unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) |
交换操作 |
func CompareAndSwapInt32(addr int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr int64, old, new int64) (swapped bool) func CompareAndSwapUint32(addr uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr uintptr, old, new uintptr) (swapped bool) func CompareAndSwapPointer(addr unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) |
比较并交换操作 |
Go语言学习笔记part8
网络编程
TCP
TCP服务端程序的处理流程:
- 监听端口
- 接收客户端请求建立链接
- 创建goroutine处理链接。
1 | //server端 |
1 | //client端 |
TCP粘包
为什么会出现粘包
主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。
“粘包”可发生在发送端也可发生在接收端:
- 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
解决办法
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
1 | // socket_stick/proto/proto.go |
UDP
UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
服务端和客户端
1 | //server |
HTTP
Go语言内置的net/http
包十分的优秀,提供了HTTP客户端和服务端的实现。
http server端
ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。处理器参数通常是nil,这表示采用包变量DefaultServeMux作为处理器。
Handle和HandleFunc函数可以向DefaultServeMux添加处理器。
1 | http.Handle("/foo", fooHandler) |
使用Go语言中的net/http
包来编写一个简单的接收HTTP请求的Server端示例,net/http
包是对net包的进一步封装,专门用来处理HTTP协议的数据。具体的代码如下:
1 | // http server |
http client端
Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。
1 | resp, err := http.Get("http://example.com/") |
程序在使用完response后必须关闭回复的主体。
1 | resp, err := http.Get("http://example.com/") |
单元测试
go test工具
Go语言中的测试依赖go test
命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
测试函数
每个测试函数必须导入testing
包,测试函数的基本格式(签名)如下:
1 | func TestName(t *testing.T){ |
测试函数的名字必须以Test
开头,可选的后缀名必须以大写字母开头,举几个例子:
1 | func TestAdd(t *testing.T){ ... } |
其中参数t
用于报告测试失败和附加的日志信息。 testing.T
的拥有的方法如下:
1 | func (c *T) Error(args ...interface{}) |
测试函数示例
我们定义一个split
的包,包中定义了一个Split
函数,具体实现如下:
1 | // split/split.go |
在当前目录下,我们创建一个split_test.go
的测试文件,并定义一个测试函数如下:
1 | // split/split_test.go |
此时split
这个包中的文件如下:
1 | split $ ls -l |
在split
包路径下,执行go test
命令,可以看到输出结果如下:
1 | split $ go test |
一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go
中添加如下测试函数:
1 | func TestMoreSplit(t *testing.T) { |
再次运行go test
命令,输出结果如下:
1 | split $ go test |
这一次,我们的测试失败了。我们可以为go test
命令添加-v
参数,查看测试函数名称和运行时间:
1 | split $ go test -v |
这一次我们能清楚的看到是TestMoreSplit
这个测试没有成功。 还可以在go test
命令后添加-run
参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test
命令执行。
1 | split $ go test -v -run="More" |
测试组
1 | func TestSplit(t *testing.T) { |
子测试
Go1.7+中新增了子测试,我们可以按照如下方式使用t.Run
执行子测试:
1 | func TestSplit(t *testing.T) { |
我们都知道可以通过-run=RegExp
来指定运行的测试用例,还可以通过/
来指定要运行的子测试用例,例如:go test -v -run=Split/simple
只会运行simple
对应的子测试用例。
测试覆盖率
测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover
来查看测试覆盖率。例如:
1 | split $ go test -cover |
从上面的结果可以看到我们的测试用例覆盖了100%的代码。
Go还提供了一个额外的-coverprofile
参数,用来将覆盖率相关的记录信息输出到一个文件。例如:
1 | split $ go test -cover -coverprofile=c.out |
上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out
文件中,然后我们执行go tool cover -html=c.out
,使用cover
工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。
性能基准测试
基准测试示例
基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:
1 | func BenchmarkName(b *testing.B){ |
基准测试以Benchmark
为前缀,需要一个*testing.B
类型的参数b,基准测试必须要执行b.N
次,这样的测试才有对照性,b.N
的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B
拥有的方法如下:
1 | func (c *B) Error(args ...interface{}) |
我们为split包中的Split
函数编写基准测试如下:
1 | func BenchmarkSplit(b *testing.B) { |
基准测试并不会默认执行,需要增加-bench
参数,所以我们通过执行go test -bench=Split
命令执行基准测试,输出结果如下:
1 | split $ go test -bench=Split |
性能比较函数
性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举个例子如下:
1 | func benchmark(b *testing.B, size int){/* ... */} |
重置时间
b.ResetTimer
之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。例如:
1 | func BenchmarkSplit(b *testing.B) { |
并行测试
func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
RunParallel
会创建出多个goroutine
,并将b.N
分配给这些goroutine
执行, 其中goroutine
数量的默认值为GOMAXPROCS
。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel
之前调用SetParallelism
。RunParallel
通常会与-cpu
标志一同使用。
1 | func BenchmarkSplitParallel(b *testing.B) { |
Setup与TearDown
通过在*_test.go
文件中定义TestMain
函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
如果测试文件包含函数:func TestMain(m *testing.M)
那么生成的测试会先调用 TestMain(m),然后再运行具体测试。TestMain
运行在主goroutine
中, 可以在调用 m.Run
前后做任何设置(setup)和拆卸(teardown)。退出测试的时候应该使用m.Run
的返回值作为参数调用os.Exit
。
一个使用TestMain
来设置Setup和TearDown的示例如下:
1 | func TestMain(m *testing.M) { |
示例函数
被go test
特殊对待的第三种函数就是示例函数,它们的函数名以Example
为前缀。它们既没有参数也没有返回值。标准格式如下:
1 | func ExampleName() { |
下面的代码是我们为Split
函数编写的一个示例函数:
1 | func ExampleSplit() { |
为你的代码编写示例代码有如下三个用处:
示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。
示例函数只要包含了
// Output:
也是可以通过go test
运行的可执行测试。1
2
3split $ go test -run Example
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s示例函数提供了可以直接运行的示例代码,可以直接在
golang.org
的godoc
文档服务器上使用Go Playground
运行示例代码。下图为strings.ToUpper
函数在Playground的示例函数效果。
pprof调试工具
Go语言项目中的性能优化主要有以下几个方面:
- CPU profile:报告程序的 CPU 使用情况,按照一定频率去采集应用程序在 CPU 和寄存器上面的数据
- Memory Profile(Heap Profile):报告程序的内存使用情况
- Block Profiling:报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
- Goroutine Profiling:报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的
性能采集工具
Go语言内置了获取程序的运行数据的工具,包括以下两个标准库:
runtime/pprof
:采集工具型应用运行数据进行分析net/http/pprof
:采集服务型应用运行时数据进行分析
pprof开启后,每隔一段时间(10ms)就会收集下当前的堆栈信息,获取各个函数占用的CPU以及内存资源;最后通过对这些采样数据进行分析,形成一个性能分析报告。
注意,我们只应该在性能测试的时候才在代码中引入pprof。
工具型应用
如果你的应用程序是运行一段时间就结束退出类型。那么最好的办法是在应用退出的时候把 profiling 的报告保存到文件中,进行分析。对于这种情况,可以使用runtime/pprof
库。 首先在代码中导入runtime/pprof
工具:
1 | import "runtime/pprof" |
CPU性能分析
开启CPU性能分析:
1 | pprof.StartCPUProfile(w io.Writer) |
停止CPU性能分析:
1 | pprof.StopCPUProfile() |
应用执行结束后,就会生成一个文件,保存了我们的 CPU profiling 数据。得到采样数据之后,使用go tool pprof
工具进行CPU性能分析。
内存性能分析
记录程序的堆栈信息
1 | pprof.WriteHeapProfile(w io.Writer) |
得到采样数据之后,使用go tool pprof
工具进行内存性能分析。
go tool pprof
默认是使用-inuse_space
进行统计,还可以使用-inuse-objects
查看分配对象的数量。
服务型应用
如果你的应用程序是一直运行的,比如 web 应用,那么可以使用net/http/pprof
库,它能够在提供 HTTP 服务进行分析。
如果使用了默认的http.DefaultServeMux
(通常是代码直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil)),只需要在你的web server端代码中按如下方式导入net/http/pprof
1 | import _ "net/http/pprof" |
go tool pprof命令
不管是工具型应用还是服务型应用,我们使用相应的pprof库获取数据之后,下一步的都要对这些数据进行分析,我们可以使用go tool pprof
命令行工具。
go tool pprof
最简单的使用方式为:
1 | go tool pprof [binary] [source] |
其中:
- binary 是应用的二进制文件,用来解析各种符号;
- source 表示 profile 数据的来源,可以是本地的文件,也可以是 http 地址。
注意事项: 获取的 Profiling 数据是动态的,要想获得有效的数据,请保证应用处于较大的负载(比如正在生成中运行的服务,或者通过其他工具模拟访问压力)。否则如果应用处于空闲状态,得到的结果可能没有任何意义。
flag库
如果你只是简单的想要获取命令行参数,可以像下面的代码示例一样使用os.Args
来获取命令行参数。os.Args
是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称。
Go语言内置的flag
包实现了命令行参数的解析,flag
包使得开发命令行工具更为简单。
flag包介绍
1 | import flag |
flag包支持的命令行参数类型有bool
、int
、int64
、uint
、uint64
、float
float64
、string
、duration
。
flag参数 | 有效值 |
---|---|
字符串flag | 合法字符串 |
整数flag | 1234、0664、0x1234等类型,也可以是负数。 |
浮点数flag | 合法浮点数 |
bool类型flag | 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。 |
时间段flag | 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。 合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。 |
有以下两种常用的定义命令行flag
参数的方法。
flag.Type()
基本格式如下:
flag.Type(flag名, 默认值, 帮助信息)*Type
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
1 | name := flag.String("name", "张三", "姓名") |
需要注意的是,此时name
、age
、married
、delay
均为对应类型的指针。
flag.TypeVar()
基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
1 | var name string |
flag.Parse()
通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()
来对命令行参数进行解析。
支持的命令行参数格式有以下几种:
-flag xxx
(使用空格,一个-
符号)--flag xxx
(使用空格,两个-
符号)-flag=xxx
(使用等号,一个-
符号)--flag=xxx
(使用等号,两个-
符号)
其中,布尔类型的参数必须使用等号的方式指定。
Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–“之后停止。
flag其他函数
1 | flag.Args() ////返回命令行参数后的其他参数,以[]string类型 |
Go语言学习笔记part9
MySQL
在Go语言中使用MySql
Go语言中的database/sql
包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。使用database/sql
包时必须注入(至少)一个数据库驱动。
下载依赖
1 | go get -u github.com/go-sql-driver/mysql |
使用MySQL驱动
1 | func Open(driverName, dataSourceName string) (*DB, error) |
Open打开一个dirverName指定的数据库,dataSourceName指定数据源,一般至少包括数据库文件名和其它连接必要的信息。
Open函数可能只是验证其参数格式是否正确,实际上并不创建与数据库的连接。如果要检查数据源的名称是否真实有效,应该调用Ping方法。
返回的DB对象可以安全地被多个goroutine并发使用,并且维护其自己的空闲连接池。因此,Open函数应该仅被调用一次,很少需要关闭这个DB对象。
1 | // 定义一个全局对象db |
其中sql.DB
是表示连接的数据库对象(结构体实例),它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池,它可以安全地被多个goroutine同时使用。
SetMaxOpenConns
1 | func (db *DB) SetMaxOpenConns(n int) |
SetMaxOpenConns
设置与数据库建立连接的最大数目。 如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n<=0,不会限制最大开启连接数,默认为0(无限制)。
SetMaxIdleConns
1 | func (db *DB) SetMaxIdleConns(n int) |
SetMaxIdleConns设置连接池中的最大闲置连接数。 如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n<=0,不会保留闲置连接。
CRUD
建库建表
1 | CREATE DATABASE sql_test; |
进入该数据库:
1 | use sql_test; |
执行以下命令创建一张用于测试的数据表:
1 | CREATE TABLE `user` ( |
查询
为了方便查询,我们事先定义好一个结构体来存储user表的数据。
1 | type user struct { |
- 单行查询:
单行查询db.QueryRow()
执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果)
1 | func (db *DB) QueryRow(query string, args ...interface{}) *Row |
非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
- 多行查询
多行查询db.Query()
执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。
1 | func (db *DB) Query(query string, args ...interface{}) (*Rows, error) |
插入、更新、删除
插入、更新和删除操作都使用Exec
方法。
1 | func (db *DB) Exec(query string, args ...interface{}) (Result, error) |
Exec执行一次命令(包括查询、删除、更新、插入等),返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。
1 | // 插入数据 |
预处理
普通SQL语句执行过程:
- 客户端对SQL语句进行占位符替换得到完整的SQL语句。
- 客户端发送完整SQL语句到MySQL服务端
- MySQL服务端执行完整的SQL语句并将结果返回给客户端。
预处理执行过程:
- 把SQL语句分成两部分,命令部分与数据部分。
- 先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。
- 然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。
- MySQL服务端执行完整的SQL语句并将结果返回给客户端。
为什么要进行预处理?
- 优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。
- 避免SQL注入问题。
Go语言实现预处理
database/sql
中使用下面的Prepare
方法来实现预处理操作。
1 | func (db *DB) Prepare(query string) (*Stmt, error) |
Prepare
方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。
1 | / 预处理查询示例 |