GoLangStudy:day8&9
GO day8&9
golang基础语法详解(Tag实际应用,goroutine详解)
1.TAG标签在JSON中的实际应用
package main
import (
//encoding包中有常见编码格式,这里我们调用json包
"encoding/json"
"fmt"
)
type Movie struct {
//为每个字段
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actor []string `json:"actor"`
}
func main() {
movie := Movie{"汉密尔顿", 2010, 56, []string{"lmm", "lmm2"}}
//编码的过程:结构体--json
//将movie通过json包的Marshal方法转换成json格式,若转换出错则退出程序
//json将使用上面声明的json标签输出各字段
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error", err)
return
}
//输出转换后的movie
//%s会把字节数组转换成可读JSON字符串,若使用%v,则会输出ASCII字节数组,可读性差
fmt.Printf("jsonStr=%s\n", jsonStr)
//解码的过程:json--结构体
//Unmarshal方法第一个参数传入需要进行解码的变量,第二个参数用于接收解码后的值
//由于内部传参方式默认为值拷贝,所以第二个参数要取地址,函数才能直接操作myMovie
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal error", err)
return
}
fmt.Printf("myMovie=%v\n", myMovie)
}
输出结果:
jsonStr={"title":"汉密尔顿","year":2010,"rmb":56,"actor":["lmm","lmm2"]}
myMovie={汉密尔顿 2010 56 [lmm lmm2]}
2.goroutine基本模型与调度
知识点1:goroutine基本模型
多线程:短时间内按顺序在各个线程中来回切换,使得它宏观上看起来像是在同时运行
弊端:具有切换成本,线程越多,切换成本越高
图解:

操作系统将线程切成用户线程(协程)和内核线程两部分
golang改良后对线程的处理:
1.将协程co-routine(用户线程)改为goroutine:只需几kb内存,更加轻量化,可以更大批量进行
2.灵活调度:
golang老调度器缺点:(g:goroutine, m:内核线程)

改良后:(p用来处理g,p中有所有的g资源,比如堆栈等等)


一个p同一时间只能处理一个g,所以同一时间内最多能同时处理GOMAXPROCS个g(GOMAXPROCS是个宏)
知识点2:调度器设计策略
复用线程: workstealing机制(作用对象是单个G,即一个个具体的Goroutine),handoff机制(作用对象是P及其绑定的整个本地G队列)
利用并行: GOMAXPROCS的个数最好设置为CPU核数/2
抢占:多个g共用一个CPU,每个g最多使用10ms,下一个g便可以抢占过来继续使用
全局G队列:属于调度器本身(sched结构体),所有P共享;访问全局队列必须加锁,意味着有性能开销;当本地队列塞满了,新创建的G会被放到全局队列;另外,部分长时间阻塞后苏醒的G也会丢到这里。
调度器如何“照顾”全局队列?(核心规则)
为了防止全局队列里的G饿死(一直没机会执行),Go调度器采用了一种优先拿本地,定期看全局的公平策略:
- 每调度61次,破例一次:P在不停从本地队列取G执行时,每过61次调度,就会强行去全局队列里取一个G来执行。
- 闲下来才批量拿:只有当P的本地队列完全空了,并且去别的P那“偷”(Work Stealing)也偷不到时,它才会去全局队列一次性拿一半的G过来,填满自己的本地队列再执行。
简单总结:全局队列是“后备粮仓”,优先级低于本地队列,主要用来兜底和保证公平性。日常调度主要依赖高性能的无锁本地队列。
3.创建goroutine
在golang中,main本身就是一个协程(主协程),程序启动时操作系统创建一个OS线程,go运行时在上面启动main线程,执行main()函数
而go newTask()则是在go运行时的调度模型中新建了一个子协程,他并不依附于主协程,而是都交给go调度器去排队调度
运行时,调度器会轮流执行goroutine,每隔一定时间就切换协程。这就是下文交替输出的由来
package main
import (
"fmt"
"time"
)
// 子协程
func newTask() {
i := 0
for {
i++
fmt.Printf("new task i=%d\n", i)
time.Sleep(1 * time.Second)//goroutine遇到这类语句会主动让出处理器切换协程
}
}
// 主协程
func main() {
//创建一个g去执行newTask
go newTask()
i := 0
for {
i++
fmt.Printf("main i=%d\n", i)
time.Sleep(1 * time.Second)
}
}
输出结果:
main i=1
new task i=1
main i=2
new task i=2
new task i=3
main i=3
main i=4
new task i=4
new task i=5
main i=5
main i=6
new task i=6
new task i=7
main i=7
那为什么主协程停止,所有子协程也会停止呢?
我们把上文中main的循环删除后再运行,会发现我们创建的子协程根本不会执行
这是go的硬性规则:当main主协程执行完毕退出的时候,整个go程序直接终止,不会等待子协程的执行,也就是说,进程的生命周期是由主协程决定的。
注意:在main函数中,go newTask()这句执行过后的效果就是创建一个子协程,然后main函数便会接着向下执行,而不是等待子协程执行完再向下执行
直接用go承载协程:
package main
import (
"fmt"
"time"
)
// 主协程
func main() {
//用go承载一个形参为空,返回值为空的函数并自行调用
go func() {
defer fmt.Println("A defer")
//再套一个子函数
func() {
defer fmt.Println("B defer")
//通过内线程退出外线程的语句:
//runtime.Goexit()
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
time.Sleep(1 * time.Second)
}
}
输出结果:
B
B defer
A
A defer
//若没有注释掉runtime.Goexit()
B defer
A defer
若go承载的函数有返回值,在协程中我们也不能直接通过赋值来直接获得返回值,即不会将返回值抛给函数的上一层,而是要使用我们接下来要学习的channel(管道)来实现
浙公网安备 33010602011771号