Golang QA2

一、Go 语言核心语法与陷阱#

Q1:defer 的执行顺序是什么?如果 defer 后面跟着 panic,执行过程是怎样的?

defer 是 LIFO(后进先出) 的栈结构,最后注册的 defer 最先执行。panic 触发后,程序不会立即退出,而是逆序执行当前 goroutine 已注册的所有 defer;如果某个 defer 中调用了 recover(),panic 会被捕获并停止向上传播;否则所有 defer 执行完毕后,panic 继续向上层 goroutine 传播。

Q2:map 是并发安全的吗?并发读写会怎样?如何实现并发安全?

map 不是并发安全的。Go 1.6 之后,运行时检测到并发读写会直接抛出 fatal error: concurrent map read and map write,无法通过 recover 捕获。原因是 map 底层涉及哈希桶、扩容搬迁等复杂操作,加全局锁会严重拖累性能,因此 Go 把同步责任交给开发者。并发安全的替代方案:sync.RWMutex + 原生 map(读多写少)或 sync.Map(读多写少、key 相对稳定的场景)。

Q3:Go 的 map 扩容机制是怎样的?

有两种扩容策略:翻倍扩容(装载因子超过 6.5,桶数翻倍)和 等量扩容(overflow 链表过长但元素总数不多,重新排列使数据紧凑)。扩容是 渐进式 的,不是一次性搬完,每次写操作搬迁少量 bucket,避免大数据量下造成长时间卡顿。

Q4:闭包在循环中有什么常见陷阱?Go 1.22 做了什么改变?

经典陷阱:在 for 循环中启动 goroutine 或创建闭包时,如果直接捕获循环变量,所有闭包可能共享同一个变量,最终都看到最后一次迭代的值。Go 1.22 之前需要 i := i 显式拷贝。Go 1.22 起,循环变量每次迭代都是新的实例,该陷阱在语言层面被修复。

Golang QA1

在 Go 语言中,Goroutine 的回收是由 Go 运行时(runtime)自动管理的。Go 的运行时系统会跟踪和调度 Goroutine 的生命周期,并在 Goroutine 不再需要时进行回收。以下是关于 Goroutine 回收的详细分析:

1. Goroutine 的生命周期#

Goroutine 是 Go 的轻量级线程,由 Go 运行时系统创建和调度。每个 Goroutine 都有自己的栈和上下文。当 Goroutine 不再执行代码时,它进入了终止状态,具体的生命周期包括:

  • 创建:Goroutine 被创建并开始执行指定的函数。
  • 运行:Goroutine 执行任务并可能会被调度到 CPU 上运行。
  • 阻塞:Goroutine 可能因为等待 I/O 操作、同步原语等原因被阻塞。
  • 终止:Goroutine 完成执行或被取消,进入终止状态。

2. Goroutine 的回收机制#

Goroutine 的终止#

当 Goroutine 完成其执行(函数返回或调用了 runtime.Goexit())时,它会被标记为终止状态。此时,Goroutine 的资源(如栈内存)将会被回收。Go 运行时系统会做以下几件事来处理终止的 Goroutine:

  • 自动回收:Go 运行时系统会自动回收那些已终止的 Goroutine 的内存。这个过程由垃圾回收器(GC)负责,GC 会在适当的时候释放 Goroutine 使用的内存资源。
  • 垃圾回收:Goroutine 的栈内存会被回收,并且 Goroutine 的局部变量和其他资源将被清理。Go 的垃圾回收器会检测到 Goroutine 不再被引用时自动进行回收。

Go 运行时的内部实现#

Go 运行时系统中涉及到 Goroutine 的回收机制的几个关键组件包括:

把Golang当脚本语言用

支持#

Go 软件包:

  • github.com/fatih/color[8] 是用于输出对应编码颜色的包。

  • github.com/schollz/progressbar[9] 是用于为执行时间过久的任务创建进度条的包。

  • github.com/jimlawless/whereami[10] 是用于捕获源代码的文件名、行号、函数等信息的包,这对于改正错误信息十分有用!

  • github.com/spf13/cobra[11] 是用于更轻松地创建带有输入选项和相关文档的复杂脚本的包。

常用命令

build#

编译个后端服务,解决有些包依赖问题

go build -ldflags '-w -s' -tags netgo -insta
llsuffix netgo -o xxxxx

清理#

  • 清理GOPATH:
go clean -modcache
# 设置大磁盘的新路径
export GOPATH=/mnt/data/go/gopath

Golang进阶笔记

Golang进阶笔记#

golang

性能分析#

import (
    "net/http"
    _ "net/http/pprof"
)

go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

将以上代码插入程序中 启动之后执行

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

具体使用方法可以help 如果使用top的话就可以查看方法占用了

(pprof) top
Showing nodes accounting for 29.59s, 98.60% of 30.01s total Dropped 35 nodes (cum <= 0.15s)
flat flat% sum% cum cum%
28.98s 96.57% 96.57% 28.98s 96.57% time.Now
0.24s 0.8% 97.37% 0.51s 1.70% runtime.selectgo
0.16s 0.53% 97.90% 0.16s 0.53% memeqbody
0.13s 0.43% 98.33% 0.29s 0.97% runtime.mapaccess2_faststr 
0.08s 0.27% 98.60% 29.95s 99.80% example.com//pts/services/monitor.Run

如以上就是说monitor.Run方法中使用了select, 然后其中的time.Now被疯狂执行.

路径问题#

test_test.go#

package main

import "testing"

func TestHelloWorld(t *testing.T) {
	// t.Fatal("not implemented")
	path := getCurrentPath()
	t.Log("getCurrentPath: ", path)
}

test.go#

package main

import (
	"fmt"
	"log"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
)

func main() {
	fmt.Println("getTmpDir(当前系统临时目录) = ", getTmpDir())
	fmt.Println("getCurrentAbPathByExecutable(仅支持go build) = ", getCurrentAbPathByExecutable())
	fmt.Println("getCurrentAbPathByCaller(仅支持go run) = ", getCurrentAbPathByCaller())
	fmt.Println("getCurrentAbPath(最终方案-全兼容) = ", getCurrentAbPath())
	fmt.Println("getCurrentPath(runtime.Caller1) = ", getCurrentPath())
}

// 最终方案-全兼容
func getCurrentAbPath() string {
	dir := getCurrentAbPathByExecutable()
	if strings.Contains(dir, getTmpDir()) {
		return getCurrentAbPathByCaller()
	}
	return dir
}

func getCurrentPath() string {
	_, filename, _, _ := runtime.Caller(1)

	return path.Dir(filename)
}

// 获取系统临时目录,兼容go run
func getTmpDir() string {
	dir := os.Getenv("TEMP")
	if dir == "" {
		dir = os.Getenv("TMP")
	}
	res, _ := filepath.EvalSymlinks(dir)
	return res
}

// 获取当前执行文件绝对路径
func getCurrentAbPathByExecutable() string {
	exePath, err := os.Executable()
	if err != nil {
		log.Fatal(err)
	}
	res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
	return res
}

// 获取当前执行文件绝对路径(go run)
func getCurrentAbPathByCaller() string {
	var abPath string
	_, filename, _, ok := runtime.Caller(0)
	if ok {
		abPath = path.Dir(filename)
	}
	return abPath
}

输出#

ian@ianDebian:~$ ./test
getTmpDir(当前系统临时目录) =  .
getCurrentAbPathByExecutable(仅支持go build) =  /home/ian
getCurrentAbPathByCaller(仅支持go run) =  /home/ian
getCurrentAbPath(最终方案-全兼容) =  /home/ian
getCurrentPath(runtime.Caller1) =  /home/ian
ian@ianDebian:~$
ian@ianDebian:~$ go run test.go
getTmpDir(当前系统临时目录) =  .
getCurrentAbPathByExecutable(仅支持go build) =  /tmp/go-build3048077768/b001/exe
getCurrentAbPathByCaller(仅支持go run) =  /home/ian
getCurrentAbPath(最终方案-全兼容) =  /tmp/go-build3048077768/b001/exe
getCurrentPath(runtime.Caller1) =  /home/ian
ian@ianDebian:~$
ian@ianDebian:~$ go test test_test.go test.go -v
=== RUN   TestHelloWorld
    test_test.go:8: getCurrentPath:  /home/ian
--- PASS: TestHelloWorld (0.00s)
PASS
ok  	command-line-arguments	0.002s

Golang 私有仓库#

xxx替换为具体地址

Golang笔记

Golang笔记#

golang 先贴一个客观的教程文档网站http://www.runoob.com/go/go-slice.html

垃圾回收#

常见的回收算法#

  1. 引用计数
    • 优点:对象可以被很快回收
    • 缺点:不太好处理循环引用
  2. 标记-清除
    • 优点:解决了引用计数的缺点
    • 缺点:需要 STW(Stop The World),暂时停止程序运行
  3. 分代收集(按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,段的放入新生代,不同代有不同的回收算法和回收频率)
    • 优点:回收性能好
    • 缺点:算法复杂
  4. 三色标记法(初始状态:白色、从根节点开始遍历,遍历到的变成灰色,遍历灰色,将灰色引用的标记灰色,遍历过的灰色对象变为黑色。循环遍历灰色对象。通过写屏障检测对象的变化,重复。收集所有白色对象(垃圾))

GPM调度和CSP模型#

CSP 模型?#

CSP 模型是“以通信的方式来共享内存”,不同于传统的多线程通 过共享内存来通信。用于描述两个独立的并发实体通过共享的通 讯 channel (管道)进行通信的并发模型。

GPM 分别是什么、分别有多少数量?#

• G(Goroutine): 即Go协程,每个go关键字都会创建一个协 程。 • M(Machine):工作线程,在Go中称为Machine,数量对应真 实的CPU数(真正干活的对象)。 • P(Processor): 处理器(Go中定义的一个摡念,非CPU), 包含运行Go代码的必要资源,用来调度 G 和 M 之间的关联关 系,其数量可通过 GOMAXPROCS() 来设置,默认为核心数。 M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队 列,P可以调度G交由M执行。

Goroutine调度策略#

• 队列轮转:P 会周期性的将G调度到M中执行,执行一段时间 后,保存上下文,将G放到队列尾部,然后从队列中再取出一个 G进行调度。除此之外,P还会周期性的查看全局队列是否有G等 待调度到M中执行。 • 系统调用:当G0即将进入系统调用时,M0将释放P,进而某个空 闲的M1获取P,继续执行P队列中剩下的G。M1的来源有可能是 M的缓存池,也可能是新建的。 当G0系统调用结束后,如果有空闲的P,则获取一个P,继续执 行G0。如果没有,则将G0放入全局队列,等待被其他的P调度。 然后M0将进入缓存池睡眠。 ![[goroutine.png]]

CHAN 原理#

结构体#

type hchan struct {
	qcount uint // 队列中的总元素个数
	dataqsiz uint // 环形队列大小,即可存放元素的个数
	buf unsafe.Pointer // 环形队列指针
	elemsize uint16 //每个元素的大小
	closed uint32 //标识关闭状态
	elemtype *_type // 元素类型
	sendx uint // 发送索引,元素写入时存放到队列中的位置
	recvx uint // 接收索引,元素从队列的该位置读出
	recvq waitq // 等待读消息的goroutine队列
	sendq waitq // 等待写消息的goroutine队列
	lock mutex //互斥锁,chan不允许并发读写
}

读写流程#

向 channel 写数据:#

  1. 若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
  2. 若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
  3. 若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入sendq ,进入睡眠,等待被读 goroutine 唤醒。

从 channel 读数据#

  1. 若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。
  2. 如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。
  4. 将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine唤醒。

关闭 channel#

  1. 关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会panic。
panic 出现的场景还有:#
  • 关闭值为 nil 的 channel
  • 关闭已经关闭的 channel
  • 向已经关闭的 channel 中写数据

无缓冲 Chan 的发送和接收是否同步?#

// 无缓冲的channel由于没有缓冲发送和接收需要同步
ch := make(chan int)
//有缓冲channel不要求发送和接收操作同步
ch := make(chan int, 2)

channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读 到数据;channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接 收阻塞。