Bootstrap

go学习杂记

一些学习时候留下的杂技,单纯用来记录,想要系统学习的话还是要看书籍哈

2025/1/21
面向对象原则
依赖倒置原则:高层模块依赖于抽象,而不是具体实现。(高层不依赖底层,而是依赖抽象接口。这样随时可以切换选择底层接口)
里氏替换原则:子类可以无缝替换父类,且不破坏系统的正确性。
接口隔离原则:客户端不应依赖于它们不使用的接口,接口应尽可能小且具体。
这些原则旨在提高代码的可维护性、可扩展性和可复用性

interface将程序各个部分组合在一起的方法,笔者称之为“水平组合“
类型嵌入为类型提供垂直扩展能力

构建和运行:go build/go run
依赖包查看与获取:go list | go get/go mod xx
编辑辅助格式化:go fmt/gofmt [代码缩进格式自动统一]
文档查看:go doc/godoc [在go.mod 下执行 开启一个本地网页,http://localhost:6060/ ,打开可以查阅文档 |可选端口号 godoc -http=:6061]
godoc -src sync.WaitGroup.Add # 可以直接查看源代码
单元测试/基准测试/测试覆盖率:go test
代码静态分析:go vet ./… [不加./… 只会检查当前目录 直接提示代码中 错误、不一致、可以的代码片段。 加-v打印更清晰 go vet -v ./… | 加-json 结果以json输出| -c=N,提示错误代码 上下N行]
性能剖析与跟踪结果查看:go tool pprof/go tool trace
trace如何使用
在main 函数开头加上下面的代码就行
f, err := os.Create(“trace.out”) // 创建一个文件
if err != nil {
return
}
defer f.Close()
trace.Start(f) // 开始监测
defer trace.Stop()
运行完成后会生成 trace.out 文件, 执行 go tool trace trace.out 即可 打开分析网页
主要分析两大块 Goroutine analysis 和 View trace by xxx
View trace by xxx 有 proc 和 thread 就是线程和cpu的执行情况
Goroutine analysis 主要是各个函数包含的 Goroutines 的运行时间,点进去可以查看函数所有的Goroutines的占用时间

升级到新Go版本API的辅助工具:go tool fix
报告Go语言bug:go bug
值得重点提及的是gofmt统一了Go语言的编码风格,在其他语言开发者还在为代码风格争论不休的时候,Go开发者可以更加专注于领域业务。
同时,相同的代码风格让以往困扰开发者的代码阅读、理解和评审工作变得容易了很多,至少Go开发者再也不会有那种因代码风格的不同而产生的陌生感。

go初始化默认0值
所有整型类型:0
浮点类型:0.0
布尔类型:false
字符串类型:“”
指针、interface、切片(slice)、channel、map、function:nil
Go的零值初始是递归的,即数组、结构体等类型的零值初始化就是对其组成元素逐一进行零值初始化。

0值可用,体现在0值的用这个类型的方法,但不能涉及到内存存储
案例:
var mu sync.Mutex // 这里已经初始化了,但是值是0 【如果是c++语言,声明后还要初始话为0;比如 pthread_mutex_init(&mu, NULL);】
mu Lock()
mu.UnLock()
// mu 不要复制,不然就是一个新的结构体,应该用指针 mu1 := &mu

还有一个案例,指定类型的空指针是可以访问这个类型的函数的(这个相当于 类静态函数的 语法糖)
type TCPAdd struct {
state int
}
func(a *TCPAdd) String()string {
if a == nil {
return “”
}
}
var p *TCPAdd
fmt.Printf(“%s\n”, p.String()) // 打印

还有就是很常见的 切片
var s []int
s[0] = 1 // 这个是错误的,因为s是0值切片
s = append(s,1) //用append 切入

还有map
var m map[string]int // 这种写法没有,因为插入不了
m[“go”] = 1 //报错
正确实现
m1 := make(map[string]int)
m1[“go”]=1

// 并发
var a chan int
go func(){
for v := range a{ // a关闭自动结束

}

for {
	v,ok := <-a
	if !ok{
		break
	}
	...
}

// 以上两种 都可以 循环读chan数据, for range 比较方便
}
}()
// select 使用于有多个 chan需要监听的情况
select {
case msg1 := <-chan1:
// 当从chan1成功接收到数据时执行
fmt.Println(“Received”, msg1)
case chan2 <- msg2:
// 当成功向chan2发送数据时执行
fmt.Println(“Sent”, msg2)
default:
// 如果没有case准备好,则执行default
fmt.Println(“No communication”)
}

单向通道
// 常见于 函数参数,函数返回值,以及并发模式(固定chan发送或者接受)
//只发送()
var ch1 <-chan int
//只接受(常见于 函数返回值)
var ch1 chan<- int

抢占式调度: 可以中断当前执行的线程,转而执行其他线程的方式
go groutinue 协作式抢占调度:
这个抢占式调度的原理是在每个函数或方法的入口加上一段额外的代码,让运行时有机会检查是否需要执行抢占调度。
这种协作式抢占调度的解决方案只是局部解决了“饿死”问题,对于没有函数调用而是纯算法循环计算的G,goroutine调度器依然无法抢占[2]。
//纯算法循环计算 的意思是, 一个groutinue 内没有调用其他函数,只是进行for, while,这种groutinue 就没有切入点去被抢占

GPM模型
G:代表goroutine,存储了goroutine的执行栈信息、goroutine状态及goroutine的任务函数等。另外G对象是可以重用的。
P:代表逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理CPU核数>=P的数量)P中最有用的是其拥有的各种G对象队列、链表、一些缓存和状态。
M:M代表着真正的执行计算资源。在绑定有效的P后,进入一个调度循环;而调度循环的机制大致是从各种队列、P的本地运行队列中获取G,
切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到M。如此反复。M并不保留G状态,这是G可以跨M调度的基础。

抢占逻辑:
在Go程序启动时,运行时会启动一个名为sysmon的M(一般称为监控线程),该M的特殊之处在于它无须绑定P即可运行(以g0这个G的形式)
sysmon每20us~10ms启动一次
如果一个G任务执行超过10ms,sysmon会认为运行太久了,就会启动抢占式调度请求,实际就是给G的抢占标志位设true.
这样,一旦G进行下一次的调用函数方法,就会检查出抢占标志位为true,然后就可以将G抢占并移出运行状态,放入 P的本地运行队列中,(或者全局队列)
注意,这对于死循环或者大循环无效,但是也有方法,循环的时候可以在固定节点加上 runtime.Gosched() 主动让出执行权,比如
for i := 0; i < 1000000; i++ {
// 模拟计算密集型任务
_ = i * i
if i%1000 == 0 {
runtime.Gosched() // 主动让出 CPU
}
}

go调度器调式信息查看
GODEBUG=schedtrace=1000,scheddtail=1 godoc -http=:6060
参数:
调度器:
schedtrace=x,每个xms输出一次
scheddetail=1,调度器会输出更详细的多行信息,包括每个处理器(P)、线程(M)和 Goroutine(G)的状态。
垃圾回收:
gctrace=1 启动垃圾回收器的跟踪输出,每次垃圾回收时会向标准错误输出一行信息,概述收集的总内存大小和暂停时间。
gctrace=2:除了上述信息外,还会重复输出每次收集的详细数据。
gcdead=1:垃圾收集器会销毁它认为已经死亡的堆栈

goroutine 调度实例简要分析
func deadloop(){
for{}
}
func main(){
go deadloop()
for {
time.Sleep(time.Second 1)
fmt.Println(“…0000”)
}
}
这个程序有两个 goroutine, 其中 main goroutine 不会因另一个死循环而卡死。
因为 他们有多个 P 调度器。P一般是cpu核心
每个核心的线程数。他们之间调度不会收到影响。【但是deadloop 确实会一直死循环,因为没有 切入点可以让其被抢占】
可以实验:
1. 用云服务器搞一个一核的处理器。
2. main函数执行开始,加上一句代码: runtime.GOMAXPROCS(1)
这句话会让 多个 goroutine 共用一个 P调度器。
这个时候会让所有 g 死循环
如果不想 所有g死循环, 可以修改 deadloop
func deadloop(){
for {
add(3,5) // 这就是切入点, 抢占标志位=true,会触发被抢占
}
}
但还是不行, 这句话会被 优化成内联函数!!!
可以在编译代码的时候禁用优化。
go build -gcflags “-N -l” -o test test.go
// -N 禁用优化
// -l 禁用内联
go tool objdump -S test > test.s
直接在 idea打开test.s 就可以查看 具体函数

	再次执行就会发现不会死循环了。

go 在 build 的时候提示依赖有问题,查看版本是不是落后的。

;