2025-11-270

相关信息

golang基础学习

Go 程序启动顺序

js
1. 编译期(build) 2. 生成可执行文件 3. 运行时启动(runtime start) 4. 初始化 package 5. 执行 init() 6. 执行 main.main()

init的执行时机

一般可以理解为main执行之前

js
1️. var 变量初始化(初始化的真正执行)(全局变量,也就是package级别的变量) 2️. init() 3️. main()
  • 备注: 局部变量是在函数执行调用时,才会被触发声明

编译时做了那些事情

js
1. 语法检查 2. 类型检查 3. 依赖分析 4. 变量初始化代码生成(决定怎么初始化、按什么顺序初始化) 5. init 调用链生成 6. main 入口标记

字符串的小问题

go
①可以用==比较 ②不可以通过下标的方式改变某个字符,字符串是只读的 ③不能和nil比较

slice 扩容机制

  • Go 1.18 之后(新算法):不再以 1024 为界限,而是引入了更平滑的过渡:
    • 如果期望容量大于当前容量的两倍,直接使用期望容量。
    • 如果当前容量 < 256,容量翻倍(2x)。
    • 如果当前容量 ≥ 256,采用公式:newcap=oldcap+(oldcap+3×256)/4newcap = oldcap + (oldcap + 3 \times 256) / 4
  • 注:随着 oldcap 变大,扩容倍数会从 2x 逐渐降到 1.25x,曲线更平滑,避免了从 2x 突降到 1.25x 的震荡。

数组定义问题

数组是可以通过下标定义的

js
array := [...]int{1,2,3,9:34} 表示array[9]==34len(array)就是10

Go 支持什么形式的类型转换?

  • 支持显示类型的转换

空结构体

  • 不包含任何字段的结构体叫做空结构体 struct{}
  • 定义方式:
js
var et struct{} et := struct{}{} type ets struct {} / et := ets{} / var et ets
  • 特性:
    • 所有的空结构体的地址都是同一地址,都是zerobase的地址,且大小为0
  • 使用场景:
    • 用于保存不重复的元素的集合,Go的map的key是不允许重复的,用空结构体作为value,不占用额外空间。
    • 用于channel中信号传输,当我们不在乎传输的信号的内容的时候,只是说只要用信号过来,通知到了就行的时候,用空结构体作为channel的类型
    • 作为方法的接收者,然后该空结构体内嵌到其他结构体,实现继承

如何停止一个goroutine

  • for - select方法,采用通道,通知协程退出
  • 采用context包

Go 语言中 cap 函数可以作用于哪些内容?

数组、切片、通道

Printf(),Sprintf(),FprintF() 都是格式化输出,有什么区别

  • Print() 标准输出,但是无格式,打印多个参数时,参数输出之间自动增加一个空格
  • Printf()是标准输出,一般用于打印。
  • Sprintf()把格式化字符串输出到字符串,并返回
  • FprintF()把格式化字符串输出到实现了io.witer方法的类型,比如文件

golang中new和make的区别

  • 共同点:都会分配内存空间(堆上)
  • 不同点:
  • 作用变量不同,
    • new 可以为任意类型分配内存
    • make 只能给切片、map、chan分配内存
  • 返回类型不同
    • new返回的是指向变量的指针
    • make返回的是上边三种变量类型本身

一句话总结:new是初始化为对应类型的零值;make初始化零值的同时,也初始化分配结构,比如在设置长度、容量的时候

数据和切片的区别

  • 数组是值类型,slice 是引用语义;数组赋值会拷贝数据,而 slice 赋值会共享底层数组。
  • 数组长度是类型的一部分,比如 [3]int 和 [4]int 是不同类型,而 slice 长度是动态的,可以通过 append 扩容。
  • slice 本质是一个结构体,包含指向底层数组的指针、长度和容量,而数组就是一段连续内存。
  • 在函数传参时,数组会发生完整拷贝,而 slice 只会拷贝结构体,但底层数据是共享的。

golang中所有传参都是值传递

  • 是Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝
  • 拷贝的内容为非引用类型(int、string、struct)时,在函数中无法修改原内容数据
  • 拷贝的内容为引用类型(指针、map、slice、chan)时,会修改原内容数据

context的使用场景

  • 作用:
    • context可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。
      • 最重要的是它是并发安全的。
    • 可以存储键值对,供上下文(协程间)读取【建议不要使用】
    • 优雅的主动取消协程(Cancel)。主动取消子协程运行,用不到子协程了,回收资源。比如一个http请求,客户端突然断开了,就直接cancel,停止后续的操作;
    • 超时退出协程(Timeout),比如如果三秒之内没有执行结束,直接退出该协程;
    • 截止时间退出协程(Deadline),如果一个业务,2点到4点为业务活动期,4点截止结束任务(协程)

rune学习

  • golang中string底层是通过byte数组实现的。
    • 中文字符在unicode下占2个字节,在utf-8编码下占3个字节,
    • golang默认编码正好是utf-8。
  • 所以len计算的时候,一个中文字符占用3个字节。
  • 而转换为rune计算,就刚好能得到字符的实际个数,比如我们想取出‘界’,rune的方式就很友好
js
import ( "fmt" "unicode/utf8" ) func main() { var str = "hello 世界" //golang中string底层是通过byte数组实现的,直接求len 实际是在按字节长度计算 所以一个汉字占3个字节算了3个长度 fmt.Println("len(str):", len(str)) //以下两种都可以得到str的字符串长度 //golang中的unicode/utf8包提供了用utf-8获取长度的方法 fmt.Println("RuneCountInString:", utf8.RuneCountInString(str)) //通过rune类型处理unicode字符 fmt.Println("rune:", len([]rune(str))) } // 结果为 12 8 8 fmt.Println(string([]rune(str)[7:])) //就能取出‘界’

逃逸问题

一般我们给一个引用类对象中的引用类成员进行赋值,可能出现逃逸现象。可以理解为访问一个引用对象实际上底层就是通过一个指针来间接的访问了,但如果再访问里面的引用成员就会有第二次间接访问,这样操作这部分对象的话,极大可能会出现逃逸的现象。

make or new

对于引用类型的变量,我们不光要声明它,还要为它分配内容空间,否则我们的值放在哪里去呢

  • new 用来开分配内存,同时返回一个指向该类型内存地址的指针。 同时请注意,它同时把分配的内存置为零,也就是类型的零值
  • make 用来为map chan slice分配内存,但是返回的是类型本身,而不是指针类型, 因为他们本身就已经是引用类型,不需要再进行指针返回了

相同点

  • 堆空间分配 不同点
  • make: 只用于slice、map以及channel的初始化, 无可替代
  • new: 用于类型内存分配(初始化值为0), 不常用

(https://www.caoziang.com/static/img/76c7a8bf10da15d974ced47f6aa346d2.image.webp)

中间件

  • 采用的是装饰器模式

golang内存泄漏的场景

  • 从一个大的slice中截取一小段
    • 问题: 只要这部分小的slice一直在使用,那么这个大的slice就一直无法被gc
    • 解决: 通过copy(newSlice, large[0:1])
  • goroutine 泄漏
    • channel阻塞卡死
    • channel没有关闭

如何避免channel被重复关闭

  • 通过sync.once
  • 遵循“由发送者关闭”原则

grpc是什么

js
gRPC 是一个基于 HTTP/2Protobuf 的高性能 RPC 框架, 支持多语言和流式通信, 主要用于微服务之间的高性能调用。 相比 REST,它序列化效率更高、连接复用能力更强, 但可读性较差, 更适合内部服务通信。

开闭原则

简单的说就是在修改需求的时候,应该尽量通过扩展来实现变化,而不是通过修改已有代码来实现变化

虚拟内容的作用

  • 物理内存无法被最大化利用。
  • 程序逻辑内存空间使用独立。
  • 内存不够,继续虚拟磁盘空间。

nil

nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值” 但是如果不特别指定的话,Go 语言不能识别类型

常量不能取值

  • 因为常量时编译器就替换为值,是没有地址的,所以不可以通过&常量名的方式获取地址的,只能print值

defer 的使用场景

场景描述
文件操作defer file.Close() 避免忘记关闭文件
锁操作defer mu.Unlock() 确保释放锁
数据库事务defer tx.Rollback()Commit()
捕获异常搭配 recover() 使用来捕获 panic

defer 的执行顺序

  • 先进后出
golang
func test() { defer fmt.Println("A") defer fmt.Println("B") defer fmt.Println("C") }

执行结果是:

golang
C B A

defer 的常见陷阱

go
for i := 0; i < 3; i++ { defer fmt.Println(i) } // 输出:2 2 2(不是 2 1 0)

原因: defer 延迟执行,i 的值在循环结束时为 2,所有 defer 引用的是同一个 i 正确的写法

go
for i := 0; i < 3; i++ { v := i defer fmt.Println(v) } // 输出:2 1 0

重要:每个goroutine都需要独立的recover机制,否则panic会导致整个程序崩溃。

go
func safeGoRoutine() { defer func() { if r := recover(); r != nil { fmt.Println("Goroutine recovered:", r) } }() // goroutine的业务逻辑 panic("goroutine panic") } func main() { go safeGoRoutine() time.Sleep(time.Second) }

gc触发机制

  • 主动触发
    • runtime.GC()
  • 被动触发堆内存达到上次gc后的堆内存的2倍
    • 距离上次gc超过2分钟
    • 堆内存达到上次gc后的堆内存的2倍

goroutine如何动态的保活

  • 子Goroutine
    • 可以通过给一个被监控的Goroutine添加一个defer ,然后recover() 捕获到当前Goroutine的异常状态,最后给主Goroutine发送一个死亡信号,通过Channel。
  • 主Goroutine
    • 在主Goroutine上,从这个Channel读取内容,当读到内容时,就重启这个子Goroutine,当然主Goroutine需要记录子Goroutine的ID,这样可以针对性的启动了
  • 代码实现
js
type WorkerManager struct { //用来监控Worker是否已经死亡的缓冲Channel workerChan chan *worker // 一共要监控的worker数量 nWorkers int } //创建一个WorkerManager对象 func NewWorkerManager(nworkers int) *WorkerManager { return &WorkerManager{ nWorkers:nworkers, workerChan: make(chan *worker, nworkers), } } //启动worker池,并为每个Worker分配一个ID,让每个Worker进行工作 func (wm *WorkerManager)StartWorkerPool() { //开启一定数量的Worker for i := 0; i < wm.nWorkers; i++ { i := i wk := &worker{id: i} go wk.work(wm.workerChan) } //启动保活监控 wm.KeepLiveWorkers() } //保活监控workers func (wm *WorkerManager) KeepLiveWorkers() { //如果有worker已经死亡 workChan会得到具体死亡的worker然后 打出异常,然后重启 for wk := range wm.workerChan { // log the error fmt.Printf("Worker %d stopped with err: [%v] \n", wk.id, wk.err) // reset err wk.err = nil // 当前这个wk已经死亡了,需要重新启动他的业务 go wk.work(wm.workerChan) } }

本文作者:曹子昂

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!