go语言关键基础知识梳理
golang关键基础知识梳理
目录
学习路线
基础扫盲
这里扫盲的意思是:列出一些我们觉得是golang关键的和独有的基础知识,其他语言相通的知识或容易理解的基础知识,不再赘述。
针对已经初步认识golang基础的童鞋,再次回顾并进一步认识转化
-
GOPATH
在进行Go语言开发的时候,我们的代码总是会保存在$GOPATH/src目录下。在工程经过go build、go install或go get等指令后,会将下载的第三方包源代码文件放在$GOPATH/src目录下, 产生的二进制可执行文件放在 $GOPATH/bin目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 下。
如果我们使用版本管理工具(Version Control System,VCS。常用如Git)来管理我们的项目代码时,我们只需要添加$GOPATH/src目录的源代码即可。bin 和 pkg 目录的内容无需版本控制。
可以通过go env查看GOPATH。
-
vscode go扩展安装
-
可见性
1)声明在函数内部,是函数的本地值,类似private
2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
3)声明在函数外部且首字母大写是所有包可见的全局值,类似public
-
声明方式
var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)
Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),package声明下来就是import声明,再下来是类型,变量,常量,函数的声明
-
值类型与引用类型
除了开发语言常有的值类型(boolean、string、int、array、struct等),golang的引用类型有slice序列序列数组、map映射、channel管道
注意:数组array和结构体struct都是值类型
-
init与main函数
init函数是package初始化必须执行
1 init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等 2 每个包可以拥有多个init函数 3 包的每个源文件也可以拥有多个init函数 4 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明) 5 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序 6 init函数不能被其他函数调用,而是在main函数执行之前,自动被调用
main函数是默认入口函数
init、main两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。 init可以应用于任意包中,且可以重复定义多个。 main函数只能用于main包中,且只能定义一个。
-
命令行
go直接运行查看
go env用于打印Go语言的环境信息。 go run命令可以编译并运行命令源码文件。 go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。 go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。 go install用于编译并安装指定的代码包及它们的依赖包。 go clean命令会删除掉执行其它命令时产生的一些文件和目录。 go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。 go test命令用于对Go语言编写的程序进行测试。 go list命令的作用是列出指定的代码包的信息。 go fix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。 go vet是一个用于检查Go语言源码中静态错误的简单工具。 go tool pprof命令来交互式的访问概要文件的内容
-
运算符
算数、关系、逻辑、赋值、位
和java、php语言的运算符基本一致。
注意:有取余运算符,没有取整运算符,需要通过math包实现取整:math.Ceil(f),math.Floor(f)
-
下划线 _ 特殊标识
基础含义是:忽略结果
比如:import _ “helo”,仅仅是执行helo的init函数,而不需要导入整个helo包
比如:f,_ := os.open(file),忽略返回的第二个变量,这里可以理解为占位符
-
变量声明
var 变量名 变量类型
var name string = "9ong" //批量声明 var ( a string b int c bool )
-
常量
除了关键字const外,其他和var变量声明类似
-
初始化类型推导
//这里没有声明变量类型,但编译器会根据右边的值推导变量的类型完成初始化 var name = '9ong'
-
短变量声明:=
注意是声明并初始化,并不是赋值。
短变量声明方式只能用于函数内部局部变量,不能在函数外使用
:= 这个符号标识短变量声明,在golang中常用,其他语言少见有这样的标识符
如果之前已经通过短变量声明过变量,变量再次赋值时,不能使用短变量声明符号:=,而是直接赋值=
基本类型
-
整型
int8,8位,1个字节(byte)
整型分为以下两个大类: 按长度分为:int8、int16、int32、int64,对应的无符号整型:uint8、uint16、uint32、uint64
其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。
默认的int,根据系统字长而定,可能是4或8个字节,即32位或63位。同理uint
-
浮点数
Go语言支持两种浮点型数:float32和float64。
-
复数
complex64(实部与虚部各32位) complex128
-
布尔值
布尔型数据只有true(真)和false(假)两个值,比弱类型的php更严谨
默认是false,不参与数值运算,无法与其他类型进行转换
-
字符串
默认UTF-8编码
使用双引号
-
反引号支持多行字符串
s1 := `第一行 第二行 第三行 ` fmt.Println(s1)
-
字符串常用操作
- 长度:len(str)
- 拼接:+ 和 , pirntln(“xxx”,“dd"+"44”)
- 分割:str.Split()
- 包含:str.Contains()
- join:str.Join()
- 子串位置:str.Index()
- 前缀/后缀:str.HasPrefix() str.HasSuffix()
-
byte
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来。
var a := '中' var b := 'x'
Go 语言的字符有两种:byte和rune。
uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。汉字在utf-8编码下需要3~4个byte(int32)
-
rune类型
rune类型,代表一个 UTF-8字符。
用来处理unicode,比如中文、日文等,比如我们经常计算字符串长度,汉字在len函数计算时,默认是按照byte计算的,而UTF-8编码的汉字往往是3~4个byte(int32),所以len计算出来就不准确(php、python这些语言中也都存在类似的问题,也都通过额外参数或是转换类型后才能正确处理汉字字符串长度)
在golang中可以通过range将带有中文的字符串转换成rune类型,再处理
-
字符串string、字符byte、字符rune的关系
字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。
修改字符串,需要先转换成字符数组,修改后,再转换为string,这真的不友好呢:
func changeString() { s1 := "hello" // 强制类型转换 byteS1 := []byte(s1) byteS1[0] = 'H' fmt.Println(string(byteS1)) s2 := "博客" runeS2 := []rune(s2) runeS2[0] = '狗' fmt.Println(string(runeS2)) }
-
强制类型转换
golang中没有隐式类型转换,需要手动强制换换,比如Sqrt函数接受的参数必须是float64,需要通过float64(a)转换整型变量a
数组
-
数组
golang的数组不同于php。php数组的长度是动态的,数组赋值、传参默认都是引用赋值、传参,不是另开辟内存空间。
在声明定义的时候,长度就固定了,而且值类型必须一致。数组是值类型,不是引用类型,就是说赋值和传参都是复制整个数组另开内存空间。
元素item使用{}包起来
var a [4]int = [4]int{1,2,3} //不足4个元素,用int默认值0补足 var b = [...]int{1,2,3,4} //[...]表示初始化时确定长度 var c = [...]int{0:1,1:2,2:3} //支持索引 var dd = [...][2]int{{1,2,3},{3,2,1}} //多维数组 var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}} //第二维不能使用... var d = [...]struct { //数组元素是结构体 name string age uint8 }{ {"user1", 10}, // 可省略元素类型。 {"user2", 20}, // 别忘了最后一行的逗号。 }
注意:数组是采用值拷贝,而值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
-
数组传参
前面说数组是值类型的,传参时默认也是值类型传参;
我们通常通过&来实现数组引用传参(除非一定要求值类型传参):
package main import "fmt" func sumArr(a [5]int) int { var sum int = 0 for i := 0; i < len(a); i++ { sum += a[i] } return sum } func printArr(arr *[5]int) { arr[0] = 10 for i, v := range arr { fmt.Println(i, v) } } func main() { var arr1 [5]int sum := sumArr(arr1) //值类型 printArr(&arr1) //引用类型,传址 fmt.Println(arr1) arr2 := [...]int{2, 4, 6, 8, 10} printArr(&arr2) fmt.Println(arr2) }
注意:& 和 *,可以解释成传址和取值(根据地址取值)。在c、php也通过&和*实现取址与取值。
是不是觉得golang的数组也太麻烦了,php的数组简单,正是因为php的解释型和动态性,使得php数组强大且编码简单,在性能上比不上golang在编译阶段就确定类型和长度的数组执行效率。
指针
前面数组中提到取址与取值,我们就先了解下golang的指针。
指针,从学编程开始,很多地方都在强调指针多重要,也不好理解,有常用到指针的语言通常都是比较难以精通的语言
指针是什么?指针就是内存地址。
在golang中,不能进行指针偏移和运算,属于安全性指针
两个符号:
- & :
变量取址
- * :
指针取值
三个概念:指针地址、指针类型、指针取值
- 指针地址:指针,变量的内存地址
- 指针类型:每个变量类型都对应一个指针类型,比如*string,*[4]int
- 指针取值:通过内存地址取得地址存储的值(也是变量的值,因为变量的内存地址就是指针)
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中,也就是变量b指向变量a指向的内存地址
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
输出:
type of b:*int
type of c:int
value of c:10
Slice切片
-
slice切片
slice表面上是数组的子集,但slice其实是
引用类型
,通过内部指针和相关属性引用数组片段实现slice的语法和python的列表切片语法是类似的
var a = [10]int{1,2,3,4,5} var slice1 = a[1:3] var slice2 = [:]
slice同样适合使用len、cap等数组可使用的函数方法,但受限于数组,比如长度不可能超出原数组
更多数组切片操作:
s[n] 切片s中的索引位置为n的元素 s[:] s[low:] s[:high] s[low:high] s[low:high:max] 从切片s的索引位置low到high获得切片,len=high-low,cap=max-low len(s) 切片长度 cap(s) 切片容量
data := [...]int{0, 1, 2, 3, 4, 5} s := data[2:4] s[0] += 100 s[1] += 200 fmt.Println(s) fmt.Println(data)
输出:
[102 203] [0 1 102 203 4 5]
注意:切片采用的是
左闭右开
方式,即包含索引low但不包含索引high的元素,也是大家说的前包后不包;切片赋值默认是引用类型,所以更改了切片元素时,也会更改原数组的元素值参考切片内存地址:
回过头来看php的数组,对开发者太友好了,不需要记忆太多语法糖,学习成本低,看起来符合自然易理解,不用关心固定不固定长度,开发者想怎么处理数组就怎么处理,不用担心是不是会出错,出错的概率太低了,php的数组兼容性太强,表达出了简约不简单的理念。
-
make直接创建切片
make是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
make可以创建切片
var slice []type = make([]type, len, cap)
slice := make([]int,0,5) //创建切片:类型int、初始长度0、容量5
除了make可以创建切片外,还可以通过初始化表达式构造:
var a int[] = []int{1,2,3,4,5}
直接创建切片,底层会自动创建数组。也就是说切片都是基于数组基础上
-
二维切片
data := [][]int{ []int{1, 2, 3}, []int{100, 200}, []int{11, 22, 33, 44}, } fmt.Println(data)
[[1 2 3] [100 200] [11 22 33 44]]
php开发者看到这里估计有点不耐烦了,为什么这么麻烦呢,哈哈
//php一个动态数组就这样定义,只因为类型不要求,甚至可以类型混合在一起,还支持键值对。确实php给开发者的心里负担少了很多,不用关心太多类型、长度等问题,只要关心业务逻辑问题,当然php赢了空间输了时间。 $data = [ [1,2,3], [100,200], [11,22,33,44], ["a",'b',3], ["a"=>"apple",2] ]; var_dump($data);
-
append
切片追加
var a = []int{1, 2, 3} fmt.Printf("slice a : %v\n", a) var b = []int{4, 5, 6} fmt.Printf("slice b : %v\n", b) c := append(a, b...) fmt.Printf("slice c : %v\n", c) d := append(c, 7) fmt.Printf("slice d : %v\n", d) e := append(d, 8, 9, 10) fmt.Printf("slice e : %v\n", e)
注意:如果append后超出原切片的cap容量,将复制数据并分配一个新数组(重新分配地址,即切片与原数组已经不再引用同一个地址)
-
copy
切片复制,函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
-
遍历
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} slice := data[:] for index, value := range slice { fmt.Printf("inde : %v , value : %v\n", index, value) }
-
字符串与切片
string在底层是一个byte的数组,也支持切片操作
对于静态编译语言,string定义后是不可变的,要修改字符,需要转换成[]byte(str)或[]rune(str)处理后,再强制转换成string。rune用于处理中文字符串,前面已经提及
对于php动态脚本语言,string变量运行时是动态可变的,牺牲性能与空间来不麻烦php开发人员
map映射
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用
-
定义map
语法:make(map[KeyType]ValueType, [cap])
func main() { scoreMap := make(map[string]int, 8){ "小红":99 } scoreMap["张三"] = 90 scoreMap["小明"] = 100 fmt.Println(scoreMap) fmt.Println(scoreMap["小明"]) fmt.Printf("type of a:%T\n", scoreMap) }
在实际运用中,我们经常遇到valueType不同类型的情况,比如json转换,我们就可以这么定义一个支持不同valueType的map键值对:
data := make(map[string]interface{},5)
看了这个示例,有php开发者可能又要说php数组了,php数组可以是列表、对象、结构体,动态混合可调整,还能互相转换,不考虑编译/解释、空间效率问题,让php开发者会觉得很轻松。强类型语言就是要求变量定义的时候尽量明确类型、大小,因为在编译的时候,尽量要知道变量的类型及要分配的空间大小,减少运行时的检查与转换,提高运行时的效率。
-
判断键值对是否存在
func main() { scoreMap := make(map[string]int) scoreMap["小红"] = 90 scoreMap["小明"] = 100 // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值 v, ok := scoreMap["小红"] if ok { fmt.Println(v) } else { fmt.Println("找不到") } }
-
遍历
采用迭代器range进行遍历,map遍历是无序的
-
删除
delete(map,key)
struct结构体
Go语言中没有“类”的概念,也没有“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
-
type of
获取变量的类型,类似js的语法type of
var b int fmt.Printf("type of b:%T\n", b) //type of b:int
-
类型别名
语法:type alias=Type
type rune = int32 //比如前面我们提到的rune就是int32的别名 type myInt = int //我们可以定义myInt的别名,具有int的特征
-
类型定义
语法:type NewType Type
type NewInt int
-
struct自定义
golang参考了很多c语言的语法,struct基本类似于c的struct
type MyStruct struct{ name string age int }
-
struct实例化
虽然没有类(class),但golang中struct也需要实例化,才会分配内存空间。
struct,我们可以理解成是一个抽象的数据结构,实例化就是使用数据通过这个数据结构进行具象化。如果有面向对象的经验,可以参考类和对象的关系
匿名结构体,可以视为使用最原始的结构体。
js的函数是函数,又可以算是结构体,也能像类,java、php有严格的类和函数区分,go的结构体代替了类,区别于函数
-
struct初始化
type person struct { name string city string age int8 } func main(){ p := person{ name: "9ong.com", city: "广州", } fmt.Printf("p=%#v\n", p) //p=main.person{name:"9ong.com", city:"广州", age:0} }
-
struct构造函数
func newPerson(name, city string, age int8) *person { return &person{ name: name, city: city, age: age, } }
-
嵌套结构体
这很好理解,结构体中属性也是个结构体(类中的属性也是个类)
-
结构体继承
可以利用结构体嵌套完成结构体的继承,有点反人类的感觉,就看怎么理解,像面向对象的语言,继承是继承父级以上类,golang中结构体的继承像是在结构体基因中流淌着父结构体的血。但golang并不是面向对象编程,golang中结构体的函数,是去中心化的实现方式,不像面向对象语言中类的方法是集中式定义实现。
//Animal 动物 type Animal struct { name string } func (a *Animal) move() { fmt.Printf("%s会动!\n", a.name) } //Dog 狗 type Dog struct { Feet int8 *Animal //通过嵌套匿名结构体实现继承 指针类型 } func (d *Dog) wang() { fmt.Printf("%s会汪汪汪~\n", d.name) } func main() { d1 := &Dog{ Feet: 4, Animal: &Animal{ //注意嵌套的是结构体指针 name: "旺财", }, } d1.wang() //旺财会汪汪汪~ d1.move() //旺财会动! }
有点别扭诡异的写法,不自然(面向对象语言在趋近自然语言上还是有很多可取之处的)
-
属性公开与私有
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
简单直接粗暴
-
结构体与JSON
在现代语言中,json是一种人类容易阅读和编写的轻量级的数据交换格式
package main import ( "encoding/json" "fmt" ) type Class struct { Title string `json:"title"` Name string `json:"name"` } func main() { //json 转换成 struct,需要定义并指定struct的结构 c1 := &Class{} str := `{"title":"标题","name":"名字"}` _ = json.Unmarshal([]byte(str), c1) fmt.Println(c1) //struct 转换成 json c2 := Class{ Title: "标题2", Name: "名字2", } jsonBtyes, _ := json.Marshal(c2) fmt.Println(string(jsonBtyes)) }
输出:
I:\src\go\src\helo>go run test.go {标题 名字} {"title":"标题2","name":"名字2"}
流程控制
-
for循环
for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。 println(s[i]) }
golang中只有for循环,没有while
//可以完成无限循环的效果 for { }
-
range
Golang range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。
类似于java迭代器,类似php的foreach迭代循环,类似于python的xrange,不需要开发者操心是否需要迭代器,甚至开发者都不知道迭代器这个概念。
golang中range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap { newMap[key] = value }
-
switch
golang的switch要注意一个break问题,case中不需要加break,匹配执行完后会直接终止,不需要等break
switch var1 { case val1: ... case val2: ... default: ... }
虽然不需要break,但也可以实现case匹配多个值:
switch marks { case 90: grade = "A" case 80: grade = "B" case 50,60,70 : grade = "C" default: grade = "D" }
另外有个特殊TypeSwitch的语法:
switch x.(type){ case type: statement(s) case type: statement(s) /* 你可以定义任意个数的case */ default: /* 可选 */ statement(s) }
switch还支持省略条件表达式
var n = 0 switch { //省略条件表达式,可当 if...else if...else case n > 0 && n < 10: fmt.Println("i > 0 and i < 10") case n > 10 && n < 20: fmt.Println("i > 10 and i < 20") default: fmt.Println("def") }
-
break/continue
continue、break除了可以像java、php等语言一样,正常从当前循环跳出或进入下一次循环外,还可以配合标签(label)可用于多层循环跳出
func SelectTest() { i := 0 Loop: for { select { case <-time.After(time.Second * time.Duration(2)): i++ if i == 5 { fmt.Println("跳出for循环") break Loop //goto Loop2 } } fmt.Println("for循环内 i=", i) } //Loop2: fmt.Println("for循环外") }
-
select
select是golang一个很有意思的控制结构
简单说:select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
相当于操作系统的处理器管理进程,select(调度)一直检查哪个case(进程)处于就绪状态(得到资源从等待状态恢复),就执行哪个case
如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。 如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
- 如果给出了default语句,那么就会执行default的流程,同时程序的执行会从select语句后的语句中恢复。
- 如果没有default语句,那么select语句将被阻塞,直到至少有一个case可以进行下去。
select的case语句必须是IO操作
-
超时判断场景
//比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行 var resChan = make(chan int) // do request func test() { select { case data := <-resChan: doData(data) case <-time.After(time.Second * 3): //如果超过3秒,上一个case还未满足,将会执行本case,意味着已经超时(time下的定时器本质上也是channel实现) fmt.Println("request time out") } } func doData(data int) { //... }
-
判断channel是否阻塞
//在某些情况下是存在不希望channel缓存满了的需求的,可以用如下方法判断 ch := make (chan int, 5) //... data:=0 select { case ch <- data: default: //一旦case不满足,就会马上进入default,也意味着case中的channel是阻塞的 //做相应操作,比如丢弃data。视需求而定 }
函数
-
函数支持当做参数传递
如同js和php一样支持将有名与匿名函数作为参数传递
-
参数
golang中默认是值传参,参数的改变不影响外部变量
也支持引用传参,参数的改变影响外部变量(共用同一个地址)
如何实现引用传参,注意&与*符号的使用:
&:变量取址
*:指针取值
package main import ( "fmt" ) /* 定义相互交换值的函数 ,需要使得swap函数外的a、b值交换,swap实际上通过地址交换实现a、b的值交换*/ func swap(x, y *int) { var temp int temp = *x /* 保存 x 的值 */ *x = *y /* 将 y 值赋给 x */ *y = temp /* 将 temp 值赋给 y*/ } func main() { var a, b int = 1, 2 /* 调用 swap() 函数 &a 指向 a 指针,a 变量的地址 &b 指向 b 指针,b 变量的地址 */ swap(&a, &b) fmt.Println(a, b) }
-
支持可变形参
语法:(args …Type)
func test(s string, n ...int) string { var x int for _, i := range n { x += i } return fmt.Sprintf(s, x) }
-
返回值
-
下划线标识符用来忽略函数的某个返回值,前面有介绍过
-
裸返回
我们更愿意叫他
缺省返回
,有其他语言经验的开发者,就很容易从缺省值理解这个缺省返回,缺省返回就是没有参数的return语句,返回所有定义的返回变量的当前值,比如以下的默认返回sum与avg。func calc(a, b int) (sum int, avg int) { sum = a + b avg = (a + b) / 2 return } func main() { var a, b int = 1, 2 c := add(a, b) sum, avg := calc(a, b) fmt.Println(a, b, c, sum, avg) }
-
不支持使用容器接受多返回值,只能用多个变量或_
-
-
defer延迟调用
-
- 关键字 defer 用于注册延迟调用。
-
- 这些调用直到 return 前才被执。因此,可以用来做资源清理。
-
- 多个defer语句,按先进后出的方式执行。可以理解成defer就是把操作注册到栈里,在return前从栈中取出依次执行,栈是先进后出的
-
- defer语句中的变量,在defer声明时就决定了
defer的使用场景,一般有文件句柄关闭、资源释放,类似java/php中的类解析方法,在类销毁前执行,defer一般在return前执行
前面函数多个返回值,我们可以用defer通过闭包读取和修改
defer 闭包,在renturn前一步执行
package main func add(x, y int) (z int) { defer func() { z += 100 }() z = x + y return z + 10 //执行顺序:z=x+y -> z+10 -> (defer z+100) -> return } func main() { println(add(1, 2)) //113 }
-
-
defer与return
package main import "fmt" func foo() (i int) { i = 0 defer func() { fmt.Println(i) }() return 2 } func main() { foo() //2 } // 输出 2 ,因为return 2,这句中的2就已经先把2赋值给返回变量i了,而defer闭包都是取得当前变量最新值,也就是2,所以Println(i),就是输出2
-
匿名函数
有过js、php、python经验的童鞋,对匿名函数会比较熟悉
func main() { getSqrt := func(a float64) float64 { return math.Sqrt(a) } fmt.Println(getSqrt(4)) }
-
闭包
有过js经验的童鞋,也会很熟悉
golang的闭包也是类似的
package main import "fmt" // 返回2个函数类型的返回值 func test01(base int) (func(int) int, func(int) int) { // 定义2个函数,并返回 // 相加 add := func(i int) int { base += i return base } // 相减 sub := func(i int) int { base -= i return base // base2:= base-i // return base2 } // 返回 return add, sub } func main() { f1, f2 := test01(10) // base一直是没有消 fmt.Println(f1(1), f2(2)) // 此时base是9 fmt.Println(f1(3), f2(4)) }
延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。也就是说,只有闭包被执行时,才复制变量参数值(当前最新值,而不是定义时的值)
和js类似,变量的当前值取的不是定义时的值,而是运行时的当前值
-
递归
所有语言的递归都类似
- 子问题须与原始问题为同样的事,且更为简单。
- 不能无限制地调用本身,至少有1个出口,化简为非递归状况处理。
-
异常处理
关键词:
panic
、recover
、defer
异常和错误要有所区分,异常一般是指关键流程不可修复的错误,错误是指普通错误,甚至可以用于业务逻辑错误
golang中抛出异常:panic函数
在defer中通recover函数捕获异常得到err信息
func test() { defer func() { if err := recover(); err != nil { println(err.(string)) // 将 interface{} 转型为具体类型。 } }() if true{//这里可以根据需要条件判断是否抛出异常 panic("panic error!") } }
基于golang的异常函数panic、recover,我们可以稍微改装实现类似try的异常处理(当然golang中没有try关键字)
以下示例,相当于封装了Try函数的实现,只是类似(还是java、js、php等语言的throw、try…catch…finally比较顺手)
package main import "fmt" //Try函数定义了两个参数,第一个参数用于执行逻辑函数,第二个参数用于接收处理异常的函数 func Try(fun func(), handler func(interface{})) { defer func() { if err := recover(); err != nil { handler(err) } }() fun() } func main() { Try(func() { panic("test panic") }, func(err interface{}) { fmt.Println(err) }) }
这里新手可能会问golang是不是面向对象的语言,严格来说不是,官网自己也说了yes and no,可以有面向对象的编程思维,支持面向过程结构化编程,其实是不是面向对象不紧要,要紧的是能很好的处理好事情,php虽然现在也算是面向对象语言,但php开发者大部分没有面向对象的思想的,况且php也像golang一样借鉴了c、js、java等语言
后面我们也会了解下golang中面向对象的一些基础知识
面向对象
方法
-
方法
方法区别于函数,在其他面向对象语言中,方法一般是指类中的方法块,只有类能调用,函数一般是单独结构块,从属于文件,可以单独调用。
在golang中,函数也是文件中的单独可调用的代码块,而方法需要结合结构体struct一起使用,从属于结构体,但不定义在结构体中,属于半隐式定义方法,个人把java、php面向对象语言中的方法定义方法称为中心化定义,而golang就像是去中心化的方法定义方式。
语法:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
示例
//Person 结构体 type Person struct { name string age int8 } //NewPerson 构造函数 返回结构体的指针类型 func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, } } //Dream Person做梦的方法 func (p Person) Dream() { fmt.Printf("%s的梦想是传递golang!\n", p.name) } // SetAge 设置p的年龄 // 使用指针接收者 func (p *Person) SetAge(newAge int8) { p.age = newAge } func main() { p1 := NewPerson("goer", 25) p1.Dream() p1.SetAge(30) //SetAge定义时使用了*Person指针类型接收者 fmt.Println(p1.age) // 30 }
注意:指针类型接收者和值类型接收者,指针类型接收者就是引用类型接收者,类似于js中的this,php的$this引用,方法中修改了相应值,会影响外部结构体属性值。其实java、php类中的方法使用的this,是语言经过处理隐藏了类本体,使用this代替。
什么时候使用指针类型接收者:
- 1.需要修改接收者中的值
- 2.接收者是拷贝代价比较大的大对象
- 3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
-
任意类型添加方法
类型与js的prototype原型
看示例更清楚:
//MyInt 以int为原型定义自定义MyInt类型 type MyInt int //为MyInt添加一个Say的方法 func (m MyInt) Say() { fmt.Println("Hi, I am int.") } func main() { var m1 MyInt m1.Say() //Hi,I am int. m1 = 99 fmt.Printf("%#v %T\n", m1, m1) //99 main.MyInt }
接口
golang面向对象的知识并不多,但我们重点关注下接口
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节
在Go语言中接口(interface)是一种类型,一种抽象的类型。记住是一种类型。
空接口类型的变量可以存储任意类型的变量。相当于java中的Object,所有对象都继承于Object。golang中interface{}表示任意类型都可以传进来,空接口都能处理。
接口教程很简单,新手会用,但不知道为什么用,用在哪里,以下有两个很经典的示例,一个实现多态,一个实现反射:
-
多态
有面向对象经验的童鞋,知道封装、继承、多态,有助于阅读
package main import "fmt" type Filter interface { About() string Process([]int) []int } // A UniqueFilter will remove duplicated numbers. type UniqueFilter struct{} func (uf UniqueFilter) About() string { return "remove diplicated numbers" } func (uf UniqueFilter) Process(inputs []int) []int { var outs = make([]int, 0, len(inputs)) var pusheds = make(map[int]bool) for _, n := range inputs { if !pusheds[n] { pusheds[n] = true outs = append(outs, n) } } return outs } // A MultipleFilter will only keep numbers which are // multiples of the MultipleFilter as an integer. type MultipleFilter int func (mf MultipleFilter) About() string { return fmt.Sprintf("keep multiples of %v", mf) } func (mf MultipleFilter) Process(inputs []int) []int { var outs = make([]int, 0, len(inputs)) for _, n := range inputs { if n % int(mf) == 0 { outs = append(outs, n) } } return outs } // With the help of polymorphism, only one "filteAndPrint" // function is needed. func filteAndPrint(fltr Filter, unfiltered []int) []int { // Call the methods of "fltr" will call the methods // of the value boxed in "fltr" acctually. filtered := fltr.Process(unfiltered) fmt.Println(fltr.About() + ":\n\t", filtered) return filtered } func main() { numbers := []int{12, 7, 21, 12, 12, 26, 25, 21, 30} fmt.Println("before filtering:\n\t", numbers) // Three non-interface values are boxed into three Filter // interface slice element values. filters := []Filter{ UniqueFilter{}, MultipleFilter(2), MultipleFilter(3), } // Each slice element will be assigned to the local variable // "fltr" (of interface type Filter) one by one. The value // boxed in each element will also be copied to "fltr". for _, fltr := range filters { numbers = filteAndPrint(fltr, numbers) } }
-
反射
反射就是动态的获取对象/结构体的信息 golang反射理解
反射也可以理解成多态的一种实现方式,只不过对象/结构体是代码本身,比如golang中的其他类型实现空接口类型(空接口类型可以接受任意参数
反射只是概念上高级了些,实际不复杂,也很自然的事。为什么编程语言要有反射呢?,这里说到如果静态语言没有反射的话,编译之后,类型都是固定的,将难以实现一些运行时的接受动态类型的函数等,比如Println(a),Println是允许打印任何类型的变量的,有了反射,实现起来就轻松多了
package main import "fmt" func main() { values := []interface{}{ 456, "abc", true, 0.33, int32(789), []int{1, 2, 3}, map[int]bool{}, nil, } for _, x := range values { // Here, v is declared once, but it denotes // different varialbes in different branches. switch v := x.(type) { case []int: // type literal // The type of v is []int. fmt.Println("int slice:", v) case string: // one type name // The type of v is string. fmt.Println("string:", v) case int, float64, int32: // multiple type names // The type of v is always same as x. // In this example, it is interface{}. fmt.Println("number:", v) case nil: // The type of v is always same as x. // In this example, it is interface{}. fmt.Println(v) default: // The type of v is always same as x. // In this example, it is interface{}. fmt.Println("others:", v) } // Note, each variable denoted by v in the // last three branches is a copy of x. } }
在java/php等面向对象语言中,实现接口通常会有关键词implement,而在golang中接口的实现,属于隐式实现,结构体不需要和接口有特殊的关键词关联,只要结构体完全实现接口的所有方法,就实现了接口
测试
-
单元测试
在国内的环境,很多开发并不是不关注单元测试,而是公司或环境使得开发不能太关注单元测试。
golang测试工具:go test , 可进行单元测试和性能测试
golang的单元测试,并不需要借助外部工具,也不需要学习新的规则或语法
go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
- 1、文件名必须以xx_test.go命名
- 2、方法必须是Test[^a-z]开头
- 3、方法参数必须 t *testing.T
- 4、使用go test执行单元测试
func TestSplit(t *testing.T) { // 定义一个测试用例类型 type test struct { input string sep string want []string } // 定义一个存储测试用例的切片 tests := []test{ {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}}, {input: "a:b:c", sep: ",", want: []string{"a:b:c"}}, {input: "abcd", sep: "bc", want: []string{"a", "d"}}, } // 遍历切片,逐一执行测试用例 for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%v, got:%v", tc.want, got) } } }
-
测试覆盖率
Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:
split $ go test -cover PASS coverage: 100.0% of statements ok jm/test_demo/split 0.005s
-
基准测试/压力测试
测试CustomFunc函数性能
go test -bench=CustomFunc 命令执行基准测试
func BenchmarkCustomFunc(b *testing.B) { for i := 0; i < b.N; i++ { CustomeFunc("args") } }
结果:
split $ go test -bench=CustomFunc goos: darwin goarch: amd64 pkg: jm/test_demo/custom BenchmarkSplit-8 10000000 167 ns/op PASS ok jm/test_demo/split 1.8525s
网络编程
Socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。
-
TCP
以下是一个tcp的范例:
tcpserver.go
package main import ( "bufio" "fmt" "net" ) // 处理函数 func process(conn net.Conn) { defer conn.Close() // 关闭连接 for { reader := bufio.NewReader(conn) var buf [128]byte n, err := reader.Read(buf[:]) // 读取数据 if err != nil { fmt.Println("read from client failed, err:", err) break } recvStr := string(buf[:n]) fmt.Println("收到client端发来的数据:", recvStr) conn.Write([]byte(recvStr + " from server")) // 发送数据 } } func main() { listen, err := net.Listen("tcp", "127.0.0.1:20000") if err != nil { fmt.Println("listen failed, err:", err) return } for { conn, err := listen.Accept() // 建立连接 if err != nil { fmt.Println("accept failed, err:", err) continue } go process(conn) // 启动一个goroutine处理连接 } }
tcpclient.go
package main import ( "bufio" "fmt" "net" "os" "strings" ) // 客户端 func main() { conn, err := net.Dial("tcp", "127.0.0.1:20000") if err != nil { fmt.Println("err :", err) return } defer conn.Close() // 关闭连接 inputReader := bufio.NewReader(os.Stdin) for { input, _ := inputReader.ReadString('\n') // 读取用户输入 inputInfo := strings.Trim(input, "\r\n") if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出 return } _, err = conn.Write([]byte(inputInfo)) // 发送数据 if err != nil { return } buf := [512]byte{} n, err := conn.Read(buf[:]) if err != nil { fmt.Println("recv failed, err:", err) return } fmt.Println(string(buf[:n])) } }
启动服务端:
go run tcpserver.go
启动客户端:
go run tpcclient.go
在客户端发送数据,服务端马上能接收到客户端发送的来数据,服务端也可以马上响应客户端的请求。
udp协议是相似的。
-
TCP粘包
-
什么是TCP粘包
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
-
为什么会出现TCP粘包
简单得说,在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows网络编程)
-
1、发送端需要等缓冲区满才发送出去,造成粘包
-
2、接收方不及时接收缓冲区的包,造成多个包接收
-
-
TCP粘包是bug吗
send(2) Upon successful completion, the number of bytes which were sent is returned. Otherwise, -1 is returned and the global variable errno is set to indicate the error.recv(2) These calls return the number of bytes received, or -1 if an error occurred.
官方文档里说了:send和recv的返回值表示成功发送和接收的字节数。
所以TCP流式传输,是一种原始朴素的传输方式,通过send和recv我们完全可以自主判断流中所有的消息边界。
可以精确判断是否发完了没有,没有发完继续发,没有收完,继续收。
如何判断:可以在消息中约定特殊的内容作为结束代表,或者约定带上内容长度。
-
如何解决TCP粘包
出现“粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。封包就是给一段数据加上包头。
-
HTTP服务
HTTP服务主要是用于web服务实现:
- 客户机通过TCP/IP协议建立到服务器的TCP连接
- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
- 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
- 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果
HTTP协议也是基于TCP/IP协议的基础上实现的应用层协议,只不过这是一些国际通用的协议,我们使用这个协议可以实现全球通用的web服务,而无需自己去实现自己的web服务的协议。
httpserver.go
package main
import (
"fmt"
"net/http"
)
func main() {
//http://127.0.0.1:8000/go 单独写关于/go的处理
http.HandleFunc("/go", index)
http.HandleFunc("/helo", helo)
// addr:监听的地址ip:port
//第二个参数: handler:回调函数 统一处理,一般单独处理,可通过路由实现
http.ListenAndServe("127.0.0.1:8000", nil)
}
// handler函数
func index(response http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "连接成功")
// 请求方式:GET POST DELETE PUT UPDATE
fmt.Println("method:", r.Method)
// /go
fmt.Println("url:", r.URL.Path)
fmt.Println("header:", r.Header)
fmt.Println("body:", r.Body)
// 回复
response.Write([]byte("www.9ong.com"))
}
func helo(response http.ResponseWriter, r *http.Request) {
response.Write([]byte("helo world"))
}
启动http服务
go run httpserver.go
浏览器中即可访问:http://127.0.0.1:8000/go 或 http://127.0.0.1:8000/helo
websocket
我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
websocket是面向浏览器的一个支持双向全工通信的协议,弥补了http协议长连接的短板。
并发编程
并发
协程goroutine
goroutine是golang的很好的机制
在Go语言编程中不需要自己写进程、线程、协程,需要让某个任务并发执行的时候,只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
注意:在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。也就是说主协程结束,其他协程也就结束。
func hello() {
fmt.Println("Hello Goroutine!")
}
//串行
func main() {
hello()
fmt.Println("main goroutine done!")
}
//并行
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine。
为什么main函数还加了time.Sleep,是因为前面我们说的main函数默认创建一个goroutine,一旦main函数创建的goroutine结束,所有在main函数中启动的goroutine也一同结束,而go hello(),创建协程需要一定时间,在这个时间里main函数就已经结束了,所以go hello()的协程也一并结束了,就不执行了。
Go语言中的操作系统线程和goroutine的关系:
- 1.一个操作系统线程对应用户态多个goroutine。
- 2.go程序可以同时使用多个操作系统线程。
- 3.goroutine和OS线程是多对多的关系,即m:n。一个协程也就是用户线程是绑定到内核态线程(线程)的,多个协程可以同时绑定到同一个线程上,所以协程和OS线程是多对多的关系。
runtime包
-
runtime.Gosched()
协程让出CPU时间片,就绪状态,等待重新安排
-
runtime.Goexit()
退出当前协程
-
runtime.GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。
channel通道
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
-
channel创建
创建:声明+初始化
声明:
//var 变量 chan 元素类型 var ch1 chan int // 声明一个传递整型的通道 var ch2 chan bool // 声明一个传递布尔型的通道 var ch3 chan []int // 声明一个传递int切片的通道
初始化:
//make(chan 元素类型, [缓冲大小]) ch1 := make(chan int) //channel的空值是nil
-
channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用
<-
符号。无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
无缓冲的通道:
注意:无缓冲的通道只有在有人接收值的时候才能发送值。缓存就是能够提供暂存数据的地方,如果没有缓存的通道,必须发送者和接收者同时存在,发送者才能发送数据到通道,接收者实时在通道中接收数据。所以无缓冲的通道会阻塞,直到另一个goroutine在该通道上执行接收操作。
无缓冲通道,我们也可以称为同步通道。
func recv(c chan int) { ret := <-c //从ch中接收值,并赋值给变量ret fmt.Println("接收成功", ret) } func main() { ch := make(chan int) go recv(ch) // 启用goroutine从通道接收值 ch <- 10 //把10发送到ch中 fmt.Println("发送成功") }
有缓冲的通道:
func main() { ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道 ch <- 10 fmt.Println("发送成功") }
可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)
close(ch)
关闭通道一些特殊问题:
- 1.对一个关闭的通道再发送值就会导致panic。
- 2.对一个关闭的通道进行接收会一直获取值直到通道为空。
- 3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 4.关闭一个已经关闭的通道会导致panic。
-
channel遍历
我们通常使用的是for range的方式判断通道是否被关闭,并从通道里遍历取值。
// 在主goroutine中从ch2中接收值打印 for i := range ch2 { // 通道关闭后会退出for range循环 fmt.Println(i) }
-
channel生产消费demo
package main import "fmt" func main() { ch1 := make(chan int) ch2 := make(chan int) // 开启goroutine将0~100的数发送到ch1中 【生产】 go func() { for i := 0; i < 100; i++ { ch1 <- i } close(ch1) }() // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中 【消费->再加工】 go func() { for { i, ok := <-ch1 // 通道关闭后再取值ok=false if !ok { break } ch2 <- i * i } close(ch2) }() // 在主goroutine中从ch2中接收值打印 【输出】 for i := range ch2 { // 通道关闭后会退出for range循环 fmt.Println(i) } }
-
单向通道
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的
func squarer(out chan<- int, in <-chan int) { for i := range in { out <- i * i } close(out) }
上面的例子中:
chan<- int 这是一个只能发送的通道,可以发送但是不能接收; <-chan int 这是一个只能接收的通道,可以接收但是不能发送。
-
channel小结
channel | nil | 非空 | 空的 | 满了 | 没满 |
---|---|---|---|---|---|
接收 | 阻塞 | 接收值 | 阻塞 | 接收值 | 接收值 |
发送 | 阻塞 | 发送值 | 发送值 | 阻塞 | 发送值 |
关闭 | panic | 关闭成功,读完数据后返回零值 | 关闭成功,返回零值 | 关闭成功,读完数据后返回零值 | 同上 |
多协程及同步实现sync.WaitGroup
为了解决多协程,且保证所有协程完成,主协程才退出,我们需要有个协程同步机制。
同步实现一直都是通过同步锁实现协同同步,golang中sync包提供了sync.WaitGroup实现goroutine的同步,判断所有协程(异步)任务是否都已经完成(wg.Wait())
sync.WaitGroup有三个方法:
func (wg * WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
协程并发时,goroutine的调度是随时无序的,所以最后打印时不是按数字顺序的,和多核下线程并发类似
协程串行实现sync.Once
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等、只处理一次写操作等
sync.Once 有个do方法
func (o *Once) Do(f func()) {}
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
sync.Map
Go语言中内置的map不是并发安全的。
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
goroutine池
定时器
package main
import (
"fmt"
"time"
)
func main() {
timer1 := time.NewTimer(2 * time.Second)
t1 := time.Now()
fmt.Printf("t1:%v\n", t1)
t2 := <-timer1.C //执行定时器
fmt.Printf("t2:%v\n", t2)
}
select
前面流程控制里已经介绍过一次select,select用于同时从多个通道接收数据,select负责监听case里的所有通道,直到其中一个通道处于ready就绪状态
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
package main
import (
"fmt"
"time"
)
func test1(ch chan string) {
time.Sleep(time.Second * 5)
ch <- "test1"
}
func test2(ch chan string) {
time.Sleep(time.Second * 2)
ch <- "test2"
}
func main() {
// 2个管道
output1 := make(chan string)
output2 := make(chan string)
// 跑2个子协程,写数据
go test1(output1)
go test2(output2)
// 用select监控
select {
case s1 := <-output1:
fmt.Println("s1=", s1)
case s2 := <-output2:
fmt.Println("s2=", s2)
}
}
并发安全与锁机制
在高并发场景,我们经常会遇到资源竞争问题,通常通过加锁及事务来解决并发导致数据读写错乱问题。
比如redis提供锁,sql提供事务
在golang中,多协程更容易出现资源竞争问题,golang在sync包中提供了锁机制,有两种锁:互斥锁和读写锁
-
互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。
var x int64 var wg sync.WaitGroup var lock sync.Mutex func add() { for i := 0; i < 5000; i++ { lock.Lock() // 加锁 x = x + 1 lock.Unlock() // 解锁 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) }
-
读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在golang中使用sync包中的 RWMutex 类型。
读写锁分为两种:
读锁
和写锁
。当一个goroutine获取读锁之后,其他的goroutine可以随时获取读锁,如果是获取写锁就要等待第一个goroutine的读锁释放;当一个goroutine获取写锁之后,其他的goroutine都要等待,不论是获取读锁还是写锁。var ( x int64 wg sync.WaitGroup lock sync.Mutex rwlock sync.RWMutex ) func write() { // lock.Lock() // 加互斥锁 rwlock.Lock() // 加写锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒 rwlock.Unlock() // 解写锁 // lock.Unlock() // 解互斥锁 wg.Done() } func read() { // lock.Lock() // 加互斥锁 rwlock.RLock() // 加读锁 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒 rwlock.RUnlock() // 解读锁 // lock.Unlock() // 解互斥锁 wg.Done() } func main() { start := time.Now() for i := 0; i < 10; i++ { wg.Add(1) go write() } for i := 0; i < 1000; i++ { wg.Add(1) go read() } wg.Wait() end := time.Now() fmt.Println(end.Sub(start)) }
原子操作atomic
互斥锁也是通过原子性解决并发安全问题,golang对于基础类型还提供了简单原子操作方式,比起互斥锁,性能更好。
golang原子操作由内置的标准库sync/atomic提供,目前支持更多的是计数级别的原子操作
var x int64
var l sync.Mutex
var wg sync.WaitGroup
// 普通版加函数
func add() {
// x = x + 1
x++ // 等价于上面的操作
wg.Done()
}
// 互斥锁版加函数
func mutexAdd() {
l.Lock()
x++
l.Unlock()
wg.Done()
}
// 原子操作版加函数
func atomicAdd() {
atomic.AddInt64(&x, 1)
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
// go add() // 普通版add函数 不是并发安全的
// go mutexAdd() // 加锁版add函数 是并发安全的,但是加锁性能开销大
go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版
}
wg.Wait()
end := time.Now()
fmt.Println(x)
fmt.Println(end.Sub(start))
}
GMP调度器
常用标准库
输入与输出-fmt包
输入与输出
-
常用输出函数
Print、Printf、Println:直接输出内容
Sprint、Sprintf、Sprintln:生成内容并返回字符串
Fprint:将内容输出到一个io.Writer接口类型的变量,经常用于写入文件
Errorf:根据format参数生成格式化字符串并返回一个包含该字符串的错误
fmt.Println("打开文件出错,err:", err)//输出带换行 s2 := fmt.Sprintf("name:%s,age:%d", name, age)//带格式生成并返回 fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)//带格式写入文件 err := fmt.Errorf("这是一个错误")
-
常用占位符
占位符 说明 %v 值的默认格式表示 %+v 类似%v,但输出结构体时会添加字段名 %#v 值的golang语法表示 %t 布尔值 %T 打印值的类型 %% 百分号 %d 表示10进制数 %b 表示2进制数 %f 浮点数,有小数 %9.2f 宽度9,精度2 %e 科学计数法 %s 直接输出字符串或[]byte %q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示 %o 8进制 %x 16进制 使用a-f %X 16进制 使用A-F %p 指针,表示16进制,并加上前缀0x -
常用输入函数
Scan、Scanf、Scanln:可以在程序运行过程中从标准输入获取用户的输入。
Scanln比较常用:在终端扫描标准输入,以空格分隔,直到换行结束扫描
fmt.Scanln(&name, &age, &married) fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
bufio.NewReader:获取完整输入内容
FScan、Fscanf、Fscanln:从文件中获取输入
Sscan、Sscanf、Sscanln:从字符串获取输入
时间与日期-time包
-
时间与日期转换
func timeDemo() { now := time.Now() //获取当前时间 fmt.Printf("current time:%v\n", now) year := now.Year() //年 month := now.Month() //月 day := now.Day() //日 hour := now.Hour() //小时 minute := now.Minute() //分钟 second := now.Second() //秒 fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second) timestamp1 := now.Unix() //时间戳 timestamp2 := now.UnixNano() //纳秒时间戳 fmt.Printf("current timestamp1:%v\n", timestamp1) fmt.Printf("current timestamp2:%v\n", timestamp2) timeObj := time.Unix(timestamp1, 0)//时间戳转换成时间对象,再通过类似以上当前时间转换成时间格式 }
-
时间间隔(单位)
time包中时间间隔的定义:
const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute )
var _time = 10 * time.Second //10秒
例如:time.Duration表示1纳秒,time.Second表示1秒。
-
时间格式化
golang的常用时间格式化模板并不是常见的:Y-m-d H:i:s,而是2006-01-02 15:04 ,这是24小时制,2006-01-02 03:04 PM,则是12小时制
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan")) // 12小时制 fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan")) fmt.Println(now.Format("2006/01/02 15:04")) fmt.Println(now.Format("15:04 2006/01/02")) fmt.Println(now.Format("2006/01/02"))
golang大佬,何必呢,增加跨语言语法记忆难度,虽然只是一点点,但如果其他地方也这样有’意思’,累计下难度就不小了。
-
时间操作
golang中的时间,并不是简单的数字加减,time包提供了实践操作的方法:
-
Add:时刻+时间段
func main() { now := time.Now() later := now.Add(time.Hour) // 当前时间加1小时后的时间 beforer := now.Add(-time.Hour) // 当前时间减1小时后的时间 fmt.Println(later) }
-
Sub:时刻1 - 时刻2,求两个时间的差值,注意这里并不是(时刻-时间段)的实现,(时刻-时间段)仍然可以用Add(-时间段)来实现
-
Equal:判断时间是否相等,会考虑时区
-
Before:判断是否在某个时刻之前
-
After:判断是否在某个时刻之后
-
-
定时器
定时器,本质上是一个channel,golang中使用time.Ticker(duration)来设置定时器
-
一次性定时器(延时)
package main import ( "fmt" "time" ) func main() { /* 用sleep实现定时器 */ fmt.Println(time.Now()) time.Sleep(time.Second) fmt.Println(time.Now()) /* 用timer实现定时器 */ timer := time.NewTimer(time.Second) fmt.Println(<-timer.C) /* 用after实现定时器 */ fmt.Println(<-time.After(time.Second)) }
-
周期性定时器
func tickDemo() { ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器 //ticker := time.NewTicker(time.Second) for i := range ticker { fmt.Println(i)//每秒都会执行的任务 } }
-
命令行参数解析-flag包
-
flag
flag包是的golang开发命令行工具更为简单。
看一个完整示例,我们就更清楚flag的用途了:
执行命令时要求输入4个参数,并指定了参数的类型与默认值
package main import ( "flag" "fmt" "time" ) func main() { //定义命令行参数方式1 var name string var age int var married bool var delay time.Duration flag.StringVar(&name, "name", "张三", "姓名") flag.IntVar(&age, "age", 18, "年龄") flag.BoolVar(&married, "married", false, "婚否") flag.DurationVar(&delay, "d", 0, "延迟的时间间隔") //解析命令行参数 flag.Parse() fmt.Println(name, age, married, delay) //返回命令行参数后的其他参数 fmt.Println(flag.Args()) //返回命令行参数后的其他参数个数 fmt.Println(flag.NArg()) //返回使用的命令行参数个数 fmt.Println(flag.NFlag()) } }
首先flag提供了命令行help功能,执行命令行会给出相应提示:
$ go run flag.go -help Usage of flag.exe: -age int 年龄 (default 18) -d duration 时间间隔 -married 婚否 -name string 姓名 (default "张三")
其次flag提供命令行参数parse解析能力:
注意:Args()\NArg()\NFlag()的含义
$ go run flag.go -name pprof --age 28 -married=false -d=1h30m pprof 28 false 1h30m0s [] 0 4
-
os.Args
当然如果我们仅仅只是需要简单命令行输入的参数,我们也可以简单的考虑os.Args来获取命令行参数。
os.Args是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称,这和大部分语言命令行模式是类似的(python、php等)
package main import ( "fmt" "os" ) //os.Args demo func main() { //os.Args是一个[]string if len(os.Args) > 0 { for index, arg := range os.Args { fmt.Printf("args[%d]=%v\n", index, arg) } } }
执行输出:
$ go run args.go tsingchan 9ong.com good args[0]=D:\exe\args.exe args[1]=tsingchan args[2]=9ong.com args[3]=good
日志-log包
官方标准简单log包,功能有限,更多可以实现流水账的日志记录,如果我们需要更多比如不同级别的日志记录,可以选择第三方日志库:logrus、zap等
//使用标准日志log,设置日志输出到xx.log文件,设置flags支持文件名、行号、日志前缀、时间格式
func main() {
logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open log file failed, err:", err)
return
}
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.SetPrefix("[JM]")
log.Println("这里记录一条日志。")
}
[JM]2020/01/14 15:32:45.431506 .../log_demo/main.go:13: 这里记录一条日志。
IO操作-os包
os包提供了Create、NewFile、Open、OpenFile、Remove方法
返回的文件对象,提供了读写方法,比如Write、WriteAt、WriteString、Read、ReadAt方法
-
文件打开与关闭
package main import ( "fmt" "os" ) func main() { // 只读方式打开当前目录下的main.go文件 file, err := os.Open("./main.go") if err != nil { fmt.Println("open file failed!, err:", err) return } // 关闭文件 file.Close() }
golang可以考虑再简化下文件的打开及异常的处理和关闭,python的with open简化了try异常和close的处理,让代码更简洁,编写者不用去关心异常处理和文件关闭处理代码,这本不属于业务逻辑代码,趋势应该是让语言或机器自动实现了
-
写文件
file.WriteString("ab\n") file.Write([]byte("cd\n"))
我们会更新一篇关于byte与string区别的文章
-
读文件
golang文件读取可以用file.Read()和file.ReadAt(),读到文件末尾会返回io.EOF的错误,EOF这是大部分语言读取结尾的标识符了
golang的os包的读写还需要偏上层封装,不要让开发者去了解这么多读写的原理,比如读取一行、整个文件,这些是开发者更乐意用到的,不用关心读写的原理,至于性能开发者会认为是语言要解决的问题,当然当我们有足够经验以后,就可以使用更底层些的方法来实现,提高性能。如果硬件的发展更快,我们更希望大家都可以不关心底层实现的去使用更上层的方法
package main import ( "fmt" "io" "os" ) func main() { // 打开文件 file, err := os.Open("./xxx.txt") if err != nil { fmt.Println("open file err :", err) return } defer file.Close() // 定义接收文件读取的字节数组 var buf [128]byte var content []byte for { n, err := file.Read(buf[:]) if err == io.EOF { // 读取结束 break } if err != nil { fmt.Println("read file err ", err) return } content = append(content, buf[:n]...) } fmt.Println(string(content)) }
IO操作-bufio包与ioutil包
bufio包实现了带缓冲区的读写,是对文件读写的封装
前面说到开发者更喜欢使用更上层的读写方法,golang的bufio包除了实现带缓冲区的读写提高效率和稳定性外,还提供按行读方法,ioutil包提供了读取整个文件、写文件方法
bufio、ioutil包更多文件、目录读写详见官方标准库
strconv包
-
Atoi
Atoi()函数用于将字符串类型的整数转换为int类型
func Atoi(s string) (i int, err error)
s1 := "100" i1, err := strconv.Atoi(s1)
-
Itoa
将int转换成string
i2 := 200 s2 := strconv.Itoa(i2)
tip:为什么是Atoi和Itoa,而不是Stoi和Itos呢,c语言没有string类型,用array字符串数组表示字符串,所以一直延续下来用a表示string
-
ParaseType系列
将string转换成指定Type类型
b, err := strconv.ParseBool("true") f, err := strconv.ParseFloat("3.1415", 64) i, err := strconv.ParseInt("-2", 10, 64) u, err := strconv.ParseUint("2", 10, 64)
-
FormatType系列
将给定类型数据格式化为string类型数据的功能
s1 := strconv.FormatBool(true) s2 := strconv.FormatFloat(3.1415, 'E', -1, 64) s3 := strconv.FormatInt(-2, 16) s4 := strconv.FormatUint(2, 16)
strconv包中还有Append系列、Quote系列等函数。详细见官方标准库
模板-template包
html/template包实现了数据驱动的模板,用于生成可对抗代码注入的安全HTML输出。
-
模板语法
{{.}}
模板语法都包含在{{和}}中间,其中{{.}}中的点表示当前对象。
当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:
type UserInfo struct { Name string Gender string Age int } func sayHello(w http.ResponseWriter, r *http.Request) { // 解析指定文件生成模板对象 tmpl, err := template.ParseFiles("./hello.html") if err != nil { fmt.Println("create template failed, err:", err) return } user := UserInfo{ Name: "jm", Gender: "男", Age: 18, } // 利用给定数据渲染模板,并将结果写入w tmpl.Execute(w, user) }
<body> <p>Hello {{.Name}}</p> <p>性别:{{.Gender}}</p> <p>年龄:{{.Name}}</p> </body>
.表示当前对象/结构体user(w只是服务端的一个变量,也保存了user这个结构体而已)
-
模板注释
{{/* a comment */}} 注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。
-
管道pipeline
Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。
-
Actions
以下这些动作基本包含golang模板中常用的动作与含义说明
{{/* a comment */}} 注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止,就像这里表示的一样。 {{pipeline}} pipeline的值的默认文本表示会被拷贝到输出里。 {{if pipeline}} T1 {{end}} 如果pipeline的值为empty,不产生输出,否则输出T1执行结果。不改变dot的值。 Empty值包括false、0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典。 {{if pipeline}} T1 {{else}} T0 {{end}} 如果pipeline的值为empty,输出T0执行结果,否则输出T1执行结果。不改变dot的值。 {{if pipeline}} T1 {{else if pipeline}} T0 {{end}} 用于简化if-else链条,else action可以直接包含另一个if;等价于: {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}} {{range pipeline}} T1 {{end}} pipeline的值必须是数组、切片、字典或者通道。 如果pipeline的值其长度为0,不会有任何输出; 否则dot依次设为数组、切片、字典或者通道的每一个成员元素并执行T1; 如果pipeline的值为字典,且键可排序的基本类型,元素也会按键的顺序排序。 {{range pipeline}} T1 {{else}} T0 {{end}} pipeline的值必须是数组、切片、字典或者通道。 如果pipeline的值其长度为0,不改变dot的值并执行T0;否则会修改dot并执行T1。 {{template "name"}} 执行名为name的模板,提供给模板的参数为nil,如模板不存在输出为"" {{template "name" pipeline}} 执行名为name的模板,提供给模板的参数为pipeline的值。 {{with pipeline}} T1 {{end}} 如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。 {{with pipeline}} T1 {{else}} T0 {{end}} 如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。
-
比较
eq 如果arg1 == arg2则返回真 ne 如果arg1 != arg2则返回真 lt 如果arg1 < arg2则返回真 le 如果arg1 <= arg2则返回真 gt 如果arg1 > arg2则返回真 ge 如果arg1 >= arg2则返回真
{{eq arg1 arg2 arg3}}
-
嵌套模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>tmpl test</title> </head> <body> <h1>测试嵌套template语法</h1> <hr> {{template "ul.html"}} <hr> {{template "ol.html"}} </body> </html> {{ define "ol.html"}} <h1>这是ol.html</h1> <ol> <li>AA</li> <li>BB</li> <li>CC</li> </ol> {{end}}
ul.html:
<ul> <li>注释</li> <li>日志</li> <li>测试</li> </ul>
ul.html模板 不在当前html文档内,需要通过服务端template.ParseFiles指定加载的模板页,才能使用(有点耦合,要是都由前端模板文件去include包含模板可能会更自然些)
func tmplDemo(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles("./t.html", "./ul.html") if err != nil { fmt.Println("create template failed, err:", err) return } user := UserInfo{ Name: "jm", Gender: "男", Age: 18, } tmpl.Execute(w, user) }
http包
Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现
-
客户端
resp, err := http.Get("http://www.baidu.com/") resp, err := http.Post("http://www.9ong.com/post", "image/jpeg", &buf) resp, err := http.PostForm("http://www.9ong.com/form", url.Values{"key": {"Value"}, "id": {"123"}}) if err != nil { // handle error } //使用完response后必须关闭回复的主体 defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body)
使用完response后必须关闭回复的主体,总是会有不完美的地方存在,还需要自己关闭Body,从编码友好角度上看,为什么语言不能处理这个问题。
-
带参数的请求
带参数get请求
apiUrl := "http://127.0.0.1:9090/get" // URL param data := url.Values{} data.Set("name", "jm") data.Set("age", "18") u, err := url.ParseRequestURI(apiUrl) if err != nil { fmt.Printf("parse url requestUrl failed,err:%v\n", err) } u.RawQuery = data.Encode() // URL encode fmt.Println(u.String()) resp, err := http.Get(u.String()) if err != nil { fmt.Println("post failed, err:%v\n", err) return }
带参数post请求
// 表单数据 //contentType := "application/x-www-form-urlencoded" //data := "name=jm&age=20" // json contentType := "application/json" data := `{"name":"jm","age":20}` resp, err := http.Post(url, contentType, strings.NewReader(data))
看完以下的编写方式,我们觉得应该可以有封装的更舒适的第三方http库,只需要:
http.Get(url,json对象参数|结构体参数) http.Post(url,options,data) //options负责设置http头等信息,data是参数json对象或结构体
-
服务端
// http server func sayHello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello world") } func main() { http.HandleFunc("/", sayHello) err := http.ListenAndServe(":9527", nil) if err != nil { fmt.Printf("http server failed, err:%v\n", err) return } }
自定义server
s := &http.Server{ Addr: ":9527", Handler: myHandler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } log.Fatal(s.ListenAndServe())
context
在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
介绍goroutine时,我们看到范例并没有在main函数里使用context,goroutine也会自动退出。原因是只有一种情况正在运行的goroutine会因为其他goroutine的结束被终止,就是main函数的退出或程序停止执行.
sync.WaitGroup解决了协程协同同步完成问题,context主要为了解决协程协同取消问题
-
Context接口
context.Context是一个接口,该接口定义了四个需要实现的方法
type Context interface { Deadline() (deadline time.Time, ok bool) //返回当前Context被取消的时间,也就是完成工作的截止时间 Done() <-chan struct{} //返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel Err() error //返回当前Context结束的原因 Value(key interface{}) interface{} //从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据 }
注意:以下介绍的都是函数,并不是创建后context上下问对像的方法,而是context包的函数
-
context.Background函数
-
context.TODO函数
Go内置两个函数:Background()和TODO(),这两个函数分别返回一个实现了
Context接口
的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。
background和todo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
-
context.withCancel函数
WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。
func gen(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // return结束该goroutine,防止泄露 case dst <- n: n++ } } }() return dst } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 当我们取完需要的整数后调用cancel for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } }
-
context.withDeadline函数
返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。
func main() { d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。 // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。 defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) } }
-
context.WithTimeout函数
func main() { // 设置一个50毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println("over") }
-
context.WithValue函数
WithValue返回父节点的副本,其中与key关联的值为val。
仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。
type TraceCode string func worker(){ ... key := TraceCode("TRACE_CODE") traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code ... } func main(){ ... // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合 ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "2009") wg.Add(1) go worker(ctx) ... }
-
关于cancel()函数
以上范例中的cancel函数,是通过context.With*系列函数返回得到的第二个值,用于通知同一个context下的所有goroutine结束/取消。
golang的context设计,让我们明白一个道理,能简单处理好一个问题,就是好的解决方案,没有高贵之分。
json/xml
-
json
json可以和map、struct、interface相互转换
// 将struct、map转换成json 字符串 json.Marshal(struct|map) //将json字符串转换成Person结构体 type Person struct{ ... } jsonStr := []byte(`{"age":"18","name":"9ong.com","marry":false}`) var p Person json.Unmarshal(jsonStr,&p)
弱类型的js、php可以随时动态自由的转换json字符串,这个确实舒服太多,怪不得php开发者总说数组强大。
-
xml
与json包的方法是一样,只是数据源不一样
xml.Marshal(struct|map) xml.Unmarshal(xmlStr,&p)
-
msgpack
MSGPack是二进制的json,性能更快,更省空间
需要安装第三方包:
go get -u github.com/vmihailenco/msgpack
msgpack.Marshal(struct|map) msgpack.Unmarshal(msgpackbinary,&p)
reflect反射
反射是指在程序运行期对程序本身进行访问和修改的能力
reflect包封装了反射相关的方法:
获取类型信息:reflect.TypeOf,是静态的
获取值信息:reflect.ValueOf,是动态的
反射可以获取interface类型信息、获取值信息、修改值信息
反射可以查看结构体字段、类型、方法,修改结构体的值,调用方法
-
空接口结合反射
可以通过 空接口 可以表示任何参数,利用反射判断参数类型
官方标准库
包和工具
数据层
-
mysql
建议如果使用web框架,请使用框架的orm,如xrom
-
redis
第三方redis包,github.com/garyburd/redigo/redis
c, err := redis.Dial("tcp", "localhost:6379") defer c.Close() _, err = c.Do("Set", "abc", 100)
封装的很简洁,但对开发者和IDE不是很友好,开发者需要很了解redis各个方法用法。
提供了redis连接池功能。
-
memcache