第一阶段:Go 快速入门(下篇) 一、内置运算符 1.1 算数运算符 {width=”4.1686034558180225in” height=”2.2823097112860893in”}
1、算数运算符使用
Go快速入门详细讲义-下篇.md
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" ) func main () { fmt.Println("10+3=" , 10 +3 ) fmt.Println("10-3=" , 10 -3 ) fmt.Println("10*3=" , 10 *3 ) fmt.Println("10/3=" , 10 /3 ) fmt.Println("10.0/3=" , 10.0 /3 ) fmt.Println("10%3=" , 10 %3 ) fmt.Println("-10%3=" , -10 %3 ) fmt.Println("10%-3=" , 10 %-3 ) fmt.Println("-10%-3=" , -10 %-3 ) }
2、i++
1 2 3 4 5 6 7 8 9 package mainimport ( "fmt" ) func main () { var i int = 1 i++ fmt.Println("i=" , i) }
1.2 关系运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" ) func main () { var n1 int = 9 var n2 int = 8 fmt.Println(n1 == n2) fmt.Println(n1 != n2) fmt.Println(n1 > n2) fmt.Println(n1 >= n2) fmt.Println(n1 < n2) fmt.Println(n1 <= n2) flag := n1 > n2 fmt.Println("flag=" , flag) }
1.3 逻辑运算符 {width=”6.383173665791776in” height=”2.3970166229221346in”}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "fmt" ) func main () { var age int = 40 if age > 30 && age < 50 { fmt.Println("ok1" ) } if age > 30 && age < 40 { fmt.Println("ok2" ) } if age > 30 || age < 50 { fmt.Println("ok3" ) } if age > 30 || age < 40 { fmt.Println("ok4" ) } if age > 30 { fmt.Println("ok5" ) } if !(age > 30 ) { fmt.Println("ok6" ) } }
1.4 赋值运算符 {width=”6.16953302712161in” height=”4.93458552055993in”}
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" ) func main () { d := 8 + 2 *8 fmt.Println(d) x := 10 x += 5 fmt.Println("x += 5 的值:" , x) }
二、条件循环 2.1 if else(分支结构) 1、if 条件判断基本写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" ) func main () { score := 65 if score >= 90 { fmt.Println("A" ) } else if score > 75 { fmt.Println("B" ) } else { fmt.Println("C" ) } }
2、if 条件判断特殊写法
if 条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { if score := 56 ; score >= 90 { fmt.Println("A" ) } else if score > 75 { fmt.Println("B" ) }else { fmt.Println("C" ) fmt.Println(score) } }
2.2 for(循环结构) 2.1 for循环
1.1、普通for循环
1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { for i := 0 ; i < 10 ; i++ { fmt.Println(i) } }
1.2 外部定义 i
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { i := 0 for i < 10 { fmt.Println(i) i++ } }
2、模拟while循环
Go 语言中是没有 while 语句的,我们可以通过 for 代替
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { k := 1 for { if k <= 10 { fmt.Println("ok~~" , k) } else { break } k++ } }
3、for range(键值循环)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { str := "abc上海" for index, val := range str { fmt.Printf("索引=%d, 值=%c \n" , index, val) } }
2.3 switch case
使用 switch 语句可方便地对大量的值进行条件判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { score := "B" switch score { case "A" : fmt.Println("非常棒" ) case "B" : fmt.Println("优秀" ) case "C" : fmt.Println("及格" ) default : fmt.Println("不及格" ) } }
2.4 break、continue
break跳出循环
continue跳出本次循环
1、break跳出单循环
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { k := 1 for { if k <= 10 { fmt.Println("ok~~" , k) } else { break } k++ } }
2、continue(继续下次循环)
1 2 3 4 5 6 7 8 9 10 package mainfunc main () { for i := 0 ; i < 10 ; i++ { if i%2 == 0 { continue } println (i) } }
三、函数 3.1 函数基础 1、函数定义
函数是组织好的、可重复使用的、用于执行指定任务的代码块。
本文介绍了 Go 语言中函数的相关内容。
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func main () { ret := intSum(1 , 2 ) fmt.Println(ret) } func intSum (x, y int ) int { return x + y }
2、函数返回值
Go 语言中通过 return 关键字向外输出返回值。
函数多返回值,Go 语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹 起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { plus, sub := calc(4 , 5 ) fmt.Println(plus) fmt.Println(sub) } func calc (x, y int ) (int , int ) { sum := x + y sub := x - y return sum, sub }
3.2 函数变量作用域 1、全局变量
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" var num int64 = 10 func main () { fmt.Printf("num=%d\n" , num) }
2、局部变量
局部变量是函数内部定义的变量, 函数内定义的变量无法在该函数外使用
例如下面的示例代码 main 函数中无法使用 testLocalVar 函数中定义的变量 x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { test() } func test () { name := "zhangsan" fmt.Println(name) }
3、for 循环语句中定义的变量
我们之前讲过的 for 循环语句中定义的变量,也是只在 for 语句块中生效
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { testLocalVar3() } func testLocalVar3 () { for i := 0 ; i < 10 ; i++ { fmt.Println(i) } }
四、结构体 1.1 什么是结构体
Go语言中没有”类”的概念,也不支持”类”的继承等面向对象的概念。
Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
1.2 结构体定义 1、基本实例化(法1)
只有当结构体实例化时,才会真正地分配内存,也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 关键字声明结构体类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" type person struct { name string city string age int } func main () { var p1 person p1.name = "张三" p1.city = "北京" p1.age = 18 fmt.Printf("p1=%v\n" , p1) fmt.Printf("p1=%#v\n" , p1) }
2、new实例化(法2)
我们还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址
从打印的结果中我们可以看出 p2 是一个结构体指针。
注意:在 Golang 中支持对结构体指针直接使用.来访问结构体的成员。
p2.name = "张三"
其实在底层是 (*p2).name = "张三"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" type person struct { name string city string age int } func main () { var p2 = new (person) p2.name = "张三" p2.age = 20 p2.city = "北京" fmt.Printf("p2=%#v \n" , p2) }
3、键值对初始化(法3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" type person struct { name string city string age int } func main () { p4 := person{ name: "zhangsan" , city: "北京" , age: 18 , } fmt.Printf("p4=%#v\n" , p4) }
1.3 结构体方法和接收者 1、结构体说明
在 go 语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。
所谓方法就是定义了接收者的函数。
Go语言中的方法(Method)是一种作用于特定类型变量的函数。
这种特定类型变量叫做接收者(Receiver)。
接收者的概念就类似于其他语言中的this或者 self。
方法的定义格式如下:
1 2 3 func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
给结构体 Person 定义一个方法打印 Person 的信息
2、结构体方法和接收者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" type Person struct { name string age int8 } func (p Person) printInfo() { fmt.Printf("姓名:%v 年龄:%v" , p.name, p.age) } func main () { p1 := Person{ name: "小王子" , age: 25 , } p1.printInfo() }
3、值类型和指针类型接收者
实例1:给结构体 Person 定义一个方法打印 Person 的信息
1、值类型的接收者
当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。
在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收 者变量本身。
2、指针类型的接收者
指针类型的接收者由一个结构体的指针组成
由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效 的。
这种方式就十分接近于其他语言中面向对象中的 this 或者 self。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport "fmt" type Person struct { name string age int } func (p Person) printInfo() { fmt.Printf("姓名:%v 年龄:%v\n" , p.name, p.age) } func (p *Person) setInfo(name string , age int ) { p.name = name p.age = age } func main () { p1 := Person{ name: "小王子" , age: 25 , } p1.printInfo() p1.setInfo("张三" , 20 ) p1.printInfo() }
1.4、一个结构体实现多个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport "fmt" type AInterface interface { GetInfo() string } type BInterface interface { SetInfo(string , int ) } type People struct { Name string Age int } func (p People) GetInfo() string { return fmt.Sprintf("姓名:%v 年龄:%d" , p.Name, p.Age) } func (p *People) SetInfo(name string , age int ) { p.Name = name p.Age = age } func main () { var people = &People{ Name: "张三" , Age: 20 , } var p1 AInterface = people var p2 BInterface = people fmt.Println(p1.GetInfo()) p2.SetInfo("李四" , 30 ) fmt.Println(p1.GetInfo()) }
1.5、接口嵌套(继承
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport "fmt" type SayInterface interface { say() } type MoveInterface interface { move() } type Animal interface { SayInterface MoveInterface } type Cat struct { name string } func (c Cat) say() { fmt.Println("喵喵喵" ) } func (c Cat) move() { fmt.Println("猫会动" ) } func main () { var x Animal x = Cat{name: "花花" } x.move() x.say() }
五、面向对象 5.1 Golang接口的定义 1、Golang 中的接口
在Go语言中接口(interface)是一种类型,一种抽象的类型。
接口(interface)定义了一个对象的行为规范,只定义规范不实现
,由具体的对象来实现规范的细 节。
实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。
换句话说,接口就是一个需要实现的方法列表。
2、为什么要使用接口
上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码
如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去
那我们能不能把它们当成“能叫的动物”来处理呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "fmt" ) type Cat struct { Name string } func (c Cat) Say() string { return c.Name + ":喵喵喵" } type Dog struct { Name string } func (d Dog) Say() string { return d.Name + ": 汪汪汪" } func main () { c := Cat{Name: "小白猫" } fmt.Println(c.Say()) d := Dog{"阿黄" } fmt.Println(d.Say()) }
3、定义一个Usber接口
定义一个 Usber 接口让 Phone 和 Camera 结构体实现这个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport "fmt" type Usber interface { start() stop() } type Phone struct { Name string } func (p Phone) start() { fmt.Println(p.Name, "启动" ) } func (p Phone) stop() { fmt.Println(p.Name, "关机" ) } func main () { p := Phone{ Name: "华为手机" , } var p1 Usber p1 = p p1.start() p1.stop() }
4、go中类
5.2 空接口 1、空接口说明
golang中空接口也可以直接当做类型来使用,可以表示任意类型
Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。
空接口表示没有任何约束,因此任何类型变量都可以实现空接口。
空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。
2、空接口作为函数的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func show (a interface {}) { fmt.Printf("值:%v 类型:%T\n" , a, a) } func main () { show(20 ) show("你好golang" ) slice := []int {1 , 2 , 34 , 4 } show(slice) }
3、切片实现空接口
1 2 3 4 5 6 7 package mainimport "fmt" func main () { var slice = []interface {}{"张三" , 20 , true , 32.2 } fmt.Println(slice) }
4、map 的值实现空接口
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func main () { var studentInfo = make (map [string ]interface {}) studentInfo["name" ] = "张三" studentInfo["age" ] = 18 studentInfo["married" ] = false fmt.Println(studentInfo) }
5.3 类型断言
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。
这两部分分别称为接口的动态类型和动态值。
如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言
其语法格式:x.(T)
x : 表示类型为 interface{}的变量
T : 表示断言 x 可能是的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { var x interface {} x = "Hello golnag" v, ok := x.(string ) if ok { fmt.Println(v) }else { fmt.Println("非字符串类型" ) } }
5.4 值接收者和指针接收者 1、值接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。
在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变 量本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport "fmt" type Usb interface { Start() Stop() } type Phone struct { Name string } func (p Phone) Start() { fmt.Println(p.Name, "开始工作" ) } func (p Phone) Stop() { fmt.Println("phone 停止" ) } func main () { phone1 := Phone{ Name: "小米手机" , } var p1 Usb = phone1 p1.Start() phone2 := &Phone{ Name: "苹果手机" , } var p2 Usb = phone2 p2.Start() }
2、指针接收者
指针类型的接收者由一个结构体的指针组成
由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
这种方式就十分接近于其他语言中面向对象中的this 或者self 。
例如我们为Person 添加一个SetAge 方法,来修改实例变量的年龄。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport "fmt" type Usb interface { Start() Stop() } type Phone struct { Name string } func (p *Phone) Start() { fmt.Println(p.Name, "开始工作" ) } func (p *Phone) Stop() { fmt.Println("phone 停止" ) } func main () { phone2 := &Phone{ Name: "苹果手机" , } var p2 Usb = phone2 p2.Start() }
3、值类型接收者使用时机
1、需要修改接收者中的值
2、接收者是拷贝代价比较大的大对象
3、保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
六、并发编程 6.1 并发介绍 1、并发和并行
1 2 A. 多线程程序在一个核的cpu上运行,就是并发。 B. 多线程程序在多个核的cpu上运行,就是并行。
{width=”6.16953302712161in” height=”2.9602318460192474in”}
{width=”3.126453412073491in” height=”1.6674409448818897in”}
协程和线程
6.2 goroutine 1、多线程编程缺点
2、gouroutine
Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。
Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。
Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine 当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。
6.3 协程基本使用 1、启动一个协程
主线程中每个100毫秒打印一次,总共打印2次
另外开启一个协程,打印10次
情况一:打印是交替,证明是并行的
情况二:开启的协程打印两次,就退出了(因为主线程退出了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "fmt" "time" ) func test () { for i := 0 ; i < 10 ; i++ { fmt.Println("test() 你好golang" ) time.Sleep(time.Millisecond * 100 ) } } func main () { go test() for i := 0 ; i < 2 ; i++ { fmt.Println("main() 你好golang" ) time.Sleep(time.Millisecond * 100 ) } }
2、WaitGroup
主线程退出后所有的协程无论有没有执行完毕都会退出
所以我们在主进程中可以通过WaitGroup等待协程执行完毕
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。
例如当我们启动了N 个并发任务时,就将计数器值增加N。
每个任务完成时通过调用Done()方法将计数器减1。
通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
1 2 3 4 var wg sync.WaitGroup // 第一步:定义一个计数器 wg.Add(1) // 第二步:开启一个协程计数器+1 wg.Done() // 第三步:协程执行完毕,计数器-1 wg.Wait() // 第四步:计数器为0时推出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroup func test1 () { for i := 0 ; i < 10 ; i++ { fmt.Println("test1() 你好golang-" , i) time.Sleep(time.Millisecond * 100 ) } wg.Done() } func test2 () { for i := 0 ; i < 2 ; i++ { fmt.Println("test2() 你好golang-" , i) time.Sleep(time.Millisecond * 100 ) } wg.Done() } func main () { wg.Add(1 ) go test1() wg.Add(1 ) go test2() wg.Wait() fmt.Println("主线程退出..." ) }
3、开启多个协程
在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine。
这里使用了 sync.WaitGroup 来实现等待 goroutine 执行完毕
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。
这是因为 10 个 goroutine是并发执行的,而 goroutine 的调度是随机的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" "sync" ) var wg sync.WaitGroupfunc hello (i int ) { defer wg.Done() fmt.Println("Hello Goroutine!" , i) } func main () { for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go hello(i) } wg.Wait() }
6.4.Channel 1.Channel 管道 1.1 Channel说明
共享内存交互数据弊端
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。
为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
channel好处
Go 语言中的通道(channel)是一种特殊的类型。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。
channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
1.2 channel类型
channel 是一种类型,一种引用类型
声明管道类型的格式如下:
1 2 3 4 var 变量 chan 元素类型var ch1 chan int var ch2 chan bool var ch3 chan []int
1.3 创建channel
声明的管道后需要使用 make 函数初始化之后才能使用。
创建 channel 的格式如下:make(chan 元素类型, 容量)
1 2 3 4 5 6 ch1 := make (chan int , 10 ) ch2 := make (chan bool , 4 ) ch3 := make (chan []int , 3 )
2.channel操作
管道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。
现在我们先使用以下语句定义一个管道:
2.1 发送(将数据放在管道内)
2.2 接收(从管道内取值)
2.3 关闭管道
我们通过调用内置的 close 函数来关闭管道: close(ch)
关于关闭管道需要注意的事情是,只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭管道。
管道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的。
关闭后的管道有以下特点:
对一个关闭的管道再发送值就会导致 panic。
对一个关闭的管道进行接收会一直获取值直到管道为空
对一个关闭的并且没有值的管道执行接收操作会得到对应类型的零值。
关闭一个已经关闭的管道会导致 panic。
2.4 管道阻塞 2.4.1 无缓冲的管道
如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道
无缓冲的管道又称为阻塞的管道。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { ch := make (chan int ) ch <- 10 fmt.Println("发送成功" ) }
2.4.2 有缓冲的管道
解决上面问题的方法还有一种就是使用有缓冲区的管道。
我们可以在使用 make 函数初始化管道的时候为其指定管道的容量
只要管道的容量大于零,那么该管道就是有缓冲的管道,管道的容量表示管道中能存放元素的数量。
就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { ch := make (chan int , 5 ) ch <- 10 ch <- 12 fmt.Println("发送成功" ) }
3.从channel取值 3.1 优雅的从channel取值
当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。
当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。
那如何判断一个通道是否被关闭了呢?
for range的方式判断通道关闭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "fmt" ) func f1 (ch1 chan int ) { for i := 0 ; i < 100 ; i++ { ch1 <- i } close (ch1) } func f2 (ch1 chan int , ch2 chan int ) { for { i, ok := <-ch1 if !ok { break } ch2 <- i * i } close (ch2) } func main () { ch1 := make (chan int ) ch2 := make (chan int ) go f1(ch1) go f2(ch1, ch2) for i := range ch2 { fmt.Println(i) } }
3.2 Goroutine结合Channel管道
需求 1 :定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据,要求同步进行。
1、开启一个 fn1 的的协程给向管道 inChan 中写入 100 条数据
2、开启一个 fn2 的协程读取 inChan 中写入的数据
3、注意:fn1 和 fn2 同时操作一个管道
4、主线程必须等待操作完成后才可以退出
注:for range的方式判断通道关闭,推出程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupfunc main () { intChan := make (chan int ,10 ) wg.Add(2 ) go write(intChan) go read(intChan) wg.Wait() fmt.Println("读取完毕..." ) } func write (intChan chan int ) { defer wg.Done() for i:=0 ;i<10 ;i++{ intChan <- i } close (intChan) } func read (intChan chan int ) { defer wg.Done() for v := range intChan { fmt.Println(v) time.Sleep(time.Second) } }
4.单向管道
有的时候我们会将管道作为参数在多个任务函数间传递
很多时候我们在不同的任务函数中使用管道都会对其进行限制
比如限制管道在函数中只能发送或只能接收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" ) func main () { var chan2 chan <- int chan2 = make (chan int , 3 ) chan2<- 20 fmt.Println("chan2=" , chan2) var chan3 <-chan int num2 := <-chan3 fmt.Println("num2" , num2) }
5.Goroutine池
本质上是生产者消费者模型
在工作中我们通常会使用可以指定启动的goroutine数量–worker pool
模式,控制goroutine
的数量,防止goroutine
泄漏和暴涨。
一个简易的work pool
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "time" ) func worker (id int , jobs <-chan int , results chan <- int ) { for j := range jobs { fmt.Printf("worker:%d start job:%d\n" , id, j) time.Sleep(time.Second) fmt.Printf("worker:%d end job:%d\n" , id, j) results <- j * 2 } } func main () { jobs := make (chan int , 100 ) results := make (chan int , 100 ) for w := 1 ; w <= 3 ; w++ { go worker(w, jobs, results) } for j := 1 ; j <= 5 ; j++ { jobs <- j } close (jobs) for a := 1 ; a <= 5 ; a++ { <-results } }
6.5.select select 多路复用
6.1 select说明
传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock,在实际开发中,可能我们不好确定什么关闭该管道。
这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。
为了应对这种场景,Go 内置了 select 关键字,可以同时响应多个管道的操作。
select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。
每个 case 会对应一个管道的通信(接收或发送)过程。
select 会一直等待,直到某个 case 的通信操作完成时,就会执行 case 分支对应的语句。
具体格式如下:
1 2 3 4 5 6 7 8 select {case <-chan1: case chan2 <- 1 : default : }
6.2 select 的使用
使用 select 语句能提高代码的可读性。
可处理一个或多个 channel 的发送/接收操作。
如果多个 case 同时满足,select 会随机选择一个。
对于没有 case 的 select{}会一直等待,可用于阻塞 main 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "time" ) func main () { intChan := make (chan int , 10 ) for i := 0 ; i < 10 ; i++ { intChan <- i } stringChan := make (chan string , 5 ) for i := 0 ; i < 5 ; i++ { stringChan <- "hello" + fmt.Sprintf("%d" , i) } for { select { case v := <-intChan: fmt.Printf("从 intChan 读取的数据%d\n" , v) time.Sleep(time.Millisecond * 50 ) case v := <-stringChan: fmt.Printf("从 stringChan 读取的数据%v\n" , v) time.Sleep(time.Millisecond * 50 ) default : fmt.Printf("数据获取完毕" ) return } } }
6.6.并发安全和锁 1.并发安全与锁 1.1 并发安全
有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。
类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。
下面开启两个协程,对变量x加一操作,分别加5000次,理想结果是10000,实际三次结果都不相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "fmt" "sync" ) var x int64 var wg sync.WaitGroupfunc add () { for i := 0 ; i < 5000 ; i++ { x = x + 1 } wg.Done() } func main () { wg.Add(2 ) go add() go add() wg.Wait() fmt.Println(x) }
1.2 互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine
可以访问共享资源。
Go语言中使用sync
包的Mutex
类型来实现互斥锁。
使用互斥锁来修复上面代码的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "sync" ) var x int64 var wg sync.WaitGroupvar lock sync.Mutexfunc 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) }
1.3 读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的
当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
读写锁在Go语言中使用sync
包中的RWMutex
类型。
读写锁分为两种:读锁和写锁
当一个goroutine获取读锁之后,其他的goroutine
如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
当一个goroutine
获取写锁之后,其他的goroutine
无论是获取读锁还是写锁都会等待。
注意:
是读写锁非常适合读多写少的场景
,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupvar mutex sync.RWMutexfunc write () { mutex.Lock() fmt.Println("执行写操作" ) time.Sleep(time.Second * 2 ) mutex.Unlock() wg.Done() } func read () { mutex.RLock() fmt.Println("---执行读操作" ) time.Sleep(time.Second * 2 ) mutex.RUnlock() wg.Done() } func main () { for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go write() } for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go read() } wg.Wait() }
2.sync其他方法 2.1 sync.WaitGroup
在代码中生硬的使用time.Sleep
肯定是不合适的,Go语言中可以使用sync.WaitGroup
来实现并发任务的同步。
sync.WaitGroup
有以下几个方法:
方法名
功能
(wg * WaitGroup) Add(delta int)
计数器+delta
(wg *WaitGroup) Done()
计数器-1
(wg *WaitGroup) Wait()
阻塞直到计数器变为0
sync.WaitGroup
内部维护着一个计数器,计数器的值可以增加和减少。
例如当我们启动了N 个并发任务时,就将计数器值增加N。
每个任务完成时通过调用Done()方法将计数器减1。
通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
我们利用sync.WaitGroup
将上面的代码优化一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "sync" ) var wg sync.WaitGroupfunc hello () { defer wg.Done() fmt.Println("Hello Goroutine!" ) } func main () { wg.Add(1 ) go hello() fmt.Println("main goroutine done!" ) wg.Wait() }
2.2sync.Once
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync
包中提供了一个针对只执行一次场景的解决方案–sync.Once
。
sync.Once
只有一个Do
方法,其签名如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var icons map [string ]image.Imagevar loadIconsOnce sync.Oncefunc loadIcons () { icons = map [string ]image.Image{ "left" : loadIcon("left.png" ), "up" : loadIcon("up.png" ), "right" : loadIcon("right.png" ), "down" : loadIcon("down.png" ), } } func Icon (name string ) image.Image { loadIconsOnce.Do(loadIcons) return icons[name] }
2.3 sync.Map
Go语言中内置的map不是并发安全的。请看下面的示例
下面的代码开启少量几个goroutine
的时候可能没什么问题
当并发多了之后执行上面的代码就会报fatal error: concurrent map writes
错误。
原因:
因为 map 变量为 指针类型变量,并发写时,多个协程同时操作一个内存
类似于多线程操作同一个资源会发生竞争关系,共享资源会遭到破坏
因此golang 出于安全的考虑,抛出致命错误:fatal error: concurrent map writes。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "strconv" "sync" ) var m = make (map [string ]int )func get (key string ) int { return m[key] } func set (key string , value int ) { m[key] = value } func main () { wg := sync.WaitGroup{} for i := 0 ; i < 20 ; i++ { wg.Add(1 ) go func (n int ) { key := strconv.Itoa(n) set(key, n) fmt.Printf("k=:%v,v:=%v\n" , key, get(key)) wg.Done() }(i) } wg.Wait() }
像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync
包中提供了一个开箱即用的并发安全版map–sync.Map
。
开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。
同时sync.Map
内置了诸如Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" "strconv" "sync" ) 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() }
6.7.goroutine原理 1.设计思路
1.1 设计描述
启动服务之时先初始化一个 Goroutine Pool 池,这个 Pool 维护了一个类似栈的 LIFO 队列 ,里面存放负责处理任务的 Worker
然后在 client 端提交 task 到 Pool 中之后,在 Pool 内部,接收 task 之后的核心操作是
检查当前 Worker 队列中是否有可用的 Worker,如果有,取出执行当前的 task;
没有可用的 Worker,判断当前在运行的 Worker 是否已超过该 Pool 的容量
每个 Worker 执行完任务之后,放回 Pool 的队列中等待
1.2 Pool struct 1 2 3 4 5 6 7 8 9 10 11 12 13 14 type sig struct {}type f func () error type Pool struct { capacity int32 running int32 expiryDuration time.Duration workers []*Worker release chan sig lock sync.Mutex once sync.Once }
1.3 初始化 Pool 并启动定期清理过期 worker 任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func NewPool (size int ) (*Pool, error ) { return NewTimingPool(size, DefaultCleanIntervalTime) } func NewTimingPool (size, expiry int ) (*Pool, error ) { if size <= 0 { return nil , ErrInvalidPoolSize } if expiry <= 0 { return nil , ErrInvalidPoolExpiry } p := &Pool{ capacity: int32 (size), freeSignal: make (chan sig, math.MaxInt32), release: make (chan sig, 1 ), expiryDuration: time.Duration(expiry) * time.Second, } p.monitorAndClear() return p, nil }
1.4 提交任务到 Pool
第一个 if 判断当前 Pool 是否已被关闭,若是则不再接受新任务,否则获取一个 Pool 中可用的 worker,绑定该 task
执行。
1 2 3 4 5 6 7 8 9 func (p *Pool) Submit(task f) error { if len (p.release) > 0 { return ErrPoolClosed } w := p.getWorker() w.task <- task return nil }
1.5 获取可用 worker(核心)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func (p *Pool) getWorker() *Worker { var w *Worker waiting := false p.lock.Lock() idleWorkers := p.workers n := len (idleWorkers) - 1 if n < 0 { waiting = p.Running() >= p.Cap() } else { w = idleWorkers[n] idleWorkers[n] = nil p.workers = idleWorkers[:n] } p.lock.Unlock() if waiting { for { p.lock.Lock() idleWorkers = p.workers l := len (idleWorkers) - 1 if l < 0 { p.lock.Unlock() continue } w = idleWorkers[l] idleWorkers[l] = nil p.workers = idleWorkers[:l] p.lock.Unlock() break } } else if w == nil { w = &Worker{ pool: p, task: make (chan f, 1 ), } w.run() p.incRunning() } return w }
1.6 执行任务
结合前面的 p.Submit(task f)
和 p.getWorker()
,提交任务到 Pool 之后,获取一个可用 worker
每新建一个 worker 实例之时都需要调用 w.run()
启动一个 goroutine 监听 worker 的任务列表 task
,一有任务提交进来就执行;
所以,当调用 worker 的 sendTask(task f)
方法提交任务到 worker 的任务队列之后,马上就可以被接收并执行
当任务执行完之后,会调用 w.pool.putWorker(w *Worker)
方法将这个已经执行完任务的 worker 从当前任务解绑放回 Pool 中,以供下个任务可以使用
至此,一个任务从提交到完成的过程就此结束,Pool 调度将进入下一个循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Worker struct { pool *Pool task chan f recycleTime time.Time } func (w *Worker) run() { go func () { for f := range w.task { if f == nil { w.pool.decRunning() return } f() w.pool.putWorker(w) } }() }
1.7 worker回收(goroutine 复用) 1 2 3 4 5 6 7 8 func (p *Pool) putWorker(worker *Worker) { worker.recycleTime = time.Now() p.lock.Lock() p.workers = append (p.workers, worker) p.lock.Unlock() }
1.8 动态扩容或者缩小池容量 1 2 3 4 5 6 7 8 9 10 11 12 13 func (p *Pool) ReSize(size int ) { if size == p.Cap() { return } atomic.StoreInt32(&p.capacity, int32 (size)) diff := p.Running() - size if diff > 0 { for i := 0 ; i < diff; i++ { p.getWorker().task <- nil } } }
1.9 定期清理过期 Worker
定期检查空闲 worker 队列中是否有已过期的 worker 并清理
因为采用了 LIFO 后进先出队列存放空闲 worker,所以该队列默认已经是按照 worker 的最后运行时间由远及近排序
可以方便地按顺序取出空闲队列中的每个 worker 并判断它们的最后运行时间与当前时间之差是否超过设置的过期时长
若是,则清理掉该 goroutine,释放该 worker,并且将剩下的未过期 worker 重新分配到当前 Pool 的空闲 worker 队列中,进一步节省系统资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func (p *Pool) periodicallyPurge() { heartbeat := time.NewTicker(p.expiryDuration) for range heartbeat.C { currentTime := time.Now() p.lock.Lock() idleWorkers := p.workers if len (idleWorkers) == 0 && p.Running() == 0 && len (p.release) > 0 { p.lock.Unlock() return } n := 0 for i, w := range idleWorkers { if currentTime.Sub(w.recycleTime) <= p.expiryDuration { break } n = i w.task <- nil idleWorkers[i] = nil } n++ if n >= len (idleWorkers) { p.workers = idleWorkers[:0 ] } else { p.workers = idleWorkers[n:] } p.lock.Unlock() } }
2.pool使用 2.1 公共池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" "sync" "time" "github.com/panjf2000/ants/v2" ) func demoFunc () { time.Sleep(10 * time.Millisecond) fmt.Println("Hello World!" ) } func main () { defer ants.Release() var wg sync.WaitGroup syncCalculateSum := func () { demoFunc() wg.Done() } for i := 0 ; i < 1000 ; i++ { wg.Add(1 ) _ = ants.Submit(syncCalculateSum) } wg.Wait() fmt.Printf("running goroutines: %d\n" , ants.Running()) fmt.Printf("finish all tasks.\n" ) }
2.2 方法绑定池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport ( "fmt" "github.com/panjf2000/ants/v2" "sync" ) func myFunc (i interface {}) { fmt.Printf("run with %d\n" , i) } func main () { defer ants.Release() var wg sync.WaitGroup p, _ := ants.NewPoolWithFunc(10 , func (i interface {}) { myFunc(i) wg.Done() }) defer p.Release() for i := 0 ; i < 1000 ; i++ { wg.Add(1 ) _ = p.Invoke(int32 (i)) } wg.Wait() fmt.Printf("running goroutines: %d\n" , p.Running()) }
七、常用模块 一、fmt 1.1 常用占位符
动词
功能
%v
按值的本来值输出
%+v
在 %v 的基础上,对结构体字段名和值进行展开
%#v
输出 Go 语言语法格式的值
%T
输出 Go 语言语法格式的类型和值
%%
输出 %% 本体
%b
整型以二进制方式显示
%o
整型以八进制方式显示
%d
整型以十进制方式显示
%x
整型以 十六进制显示
%X
整型以十六进制、字母大写方式显示
%U
Unicode 字符
%f
浮点数
%p
指针,十六进制方式显示
1.2 Print
Println:
一次输入多个值的时候 Println 中间有空格
Println 会自动换行,Print 不会
Print:
一次输入多个值的时候 Print 没有 中间有空格
Print 不会自动换行
Printf
是格式化输出,在很多场景下比 Println 更方便
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { fmt.Print("zhangsan" , "lisi" , "wangwu" ) fmt.Println("zhangsan" , "lisi" , "wangwu" ) name := "zhangsan" age := 20 fmt.Printf("%s 今年 %d 岁\n" , name, age) fmt.Printf("值:%v --> 类型: %T" , name, name) }
1.3 Sprint
Sprint系列函数会把传入的数据生成并返回一个字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { s1 := fmt.Sprint("枯藤" ) fmt.Println(s1) name := "枯藤" age := 18 s2 := fmt.Sprintf("name:%s,age:%d" , name, age) fmt.Println(s2) s3 := fmt.Sprintln("枯藤" ) fmt.Println(s3) }
二、time 2.1 时间类型
我们可以通过 time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。
注意:%02d 中的 2 表示宽度,如果整数不够 2 列就补上 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" "time" ) func main () { 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()
2.2 时间戳 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" "time" ) func main () { now := time.Now() timestamp1 := now.Unix() timestamp2 := now.UnixNano() fmt.Printf("current timestamp1:%v\n" , timestamp1) timestamp1:1623560753 fmt.Printf("current timestamp2:%v\n" , timestamp2) timestamp2:1623560753965606600 }
使用time.Unix() 函数可以将时间戳转为时间格式
1 2 3 4 5 6 7 8 9 10 11 12 func timestampDemo2 (timestamp int64 ) { timeObj := time.Unix(timestamp, 0 ) fmt.Println(timeObj) year := timeObj.Year() month := timeObj.Month() day := timeObj.Day() hour := timeObj.Hour() minute := timeObj.Minute() second := timeObj.Second() fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n" , year, month, day, hour, minute, second) }
2.3 时间间隔
time.Duration 是time 包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单 位。
time.Duration 表示一段时间间隔,可表示的最长时间段大约290年。
time包中定义的时间间隔类型的常量如下:
1 2 3 4 5 6 7 8 const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute )
2.4 时间格式化
时间类型有一个自带的方法Format 进行格式化
需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S
而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。
补充:如果想格式化为12小时方式,需指定PM 。
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" "time" ) func main () { now := time.Now() fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan" )) 13 :10 :18.143 Sun Jun}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" "time" ) func main () { loc, _ := time.LoadLocation("Asia/Shanghai" ) timeObj, err := time.ParseInLocation("2006-01-02 15:04:05" , "2019-08-04 14:15:20" , loc) if err != nil { fmt.Println(err) return } fmt.Println(timeObj) }
2.5 时间操作函数
Add
我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求
Go 语言的时间对象有提供Add 方法如下
Sub
1 2 3 4 5 6 7 package mainimport ( "fmt" "time" )
三、encoding-json包 3.1 struct与json
比如我们 Golang 要给 App 或者小程序提供 Api 接口数据,这个时候就需要涉及到结构体和Json 之间的相互转换
GolangJSON 序列化是指把结构体数据转化成 JSON 格式的字符串
Golang JSON 的反序列化是指把 JSON 数据转化成 Golang 中的结构体对象
Golang 中 的 序 列 化 和 反 序 列 化 主 要 通 过 "encoding/json" 包 中 的 json.Marshal() 和 json.Unmarshal()方法实现
1、struct转Json字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int Gender string name string Sno string } func main () { var s1 = Student{ ID: 1 , Gender: "男" , name: "李四" , Sno: "s0001" , } fmt.Printf("%#v\n" , s1) Sno:"s0001" } var s, _ = json.Marshal(s1) jsonStr := string (s) fmt.Println(jsonStr) }
2、Json字符串转struct 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int Gender string Name string Sno string } func main () { var jsonStr = `{"ID":1,"Gender":"男","Name":"李四","Sno":"s0001"}` var student Student err := json.Unmarshal([]byte (jsonStr), &student) if err != nil { fmt.Printf("unmarshal err=%v\n" , err) } student.Name=李四 fmt.Printf("反序列化后 student=%#v student.Name=%v \n" , student, student.Name) }
3.2 struct tag 1、Tag标签说明
Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag 在结构体字段的后方定义,由一对反引号包裹起来
具体的格式如下:
1 key1:"value1" key2:"value2"
结构体 tag 由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。
同一个结构体字段可以设置多个键值对 tag,不同的键值对之间使用空格分隔。
注意事项:
为结构体编写 Tag 时,必须严格遵守键值对的规则。
结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误, 通过反射也无法正确取值。
例如不要在 key 和 value 之间添加空格。
2、Tag结构体转化Json字符串 1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int `json:"id"` Gender string `json:"gender"` Name string Sno string
3、Json字符串转成Tag结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int `json:"id"` Gender string `json:"gender"` Name string Sno string } func main () { var s2 Student var str = `{"id":1,"gender":"男","Name":"李四","Sno":"s0001"}` err := json.Unmarshal([]byte (str), &s2) if err != nil { fmt.Println(err) } fmt.Printf("%#v" , s2) }
四、Flag 4.1 Flag
Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。
flag.Parse() 通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。
支持的命令行参数格式有以下几种:
-flag xxx (使用空格,一个-符号)
–flag xxx (使用空格,两个-符号)
-flag=xxx (使用等号,一个-符号)
–flag=xxx (使用等号,两个-符号)
其中,布尔类型的参数必须使用等号的方式指定。
Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–”之后停止。
2、其他函数
flag.Args() ////返回命令行参数后的其他参数,以[]string类型
flag.NArg() //返回命令行参数后的其他参数个数
flag.NFlag() //返回使用的命令行参数个数
4.2 完整示例 1、main.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "flag" "fmt" "time" ) func main () { 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()) }
2、查看帮助 1 2 3 4 5 6 7 8 9 C:\aaa\gin_demo> go run main.go --help -age int 年龄 (default 18 ) -d duration 延迟的时间间隔 -married 婚否 -name string 姓名 (default "张三" )
3、flag参数演示 1 2 3 4 5 C:\aaa\gin_demo> go run main.go -name pprof --age 28 -married=false -d=1 h30m pprof 28 false 1 h30m0s [] 0 4
4、非flag命令行参数 1 2 3 4 5 C:\aaa\gin_demo>go run main.go a b c 张三 18 false 0 s [a b c] 3 0