[Golang]初始化流程分析

本文借助gdb查看Go底层汇编,梳理分析Go程序初始化流程。介绍了gdb调试环境搭建与基本使用,梳理了初始化过程的调用流程和次序。通过案例分析,明确初始化顺序、包依赖关系和编译文件映射,帮助理解初始化与程序入口的次序关系,还提醒慎用相关功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

借助gdb来查看go的底层汇编,借此梳理和分析go程序的初始化流程,看看在初始化阶段go都做了哪些工作,对于理解go的工作机制很有帮助。目前是基于go 1.16.4进行的。

gdb调试

搭建gdb调试go程序 中已经探究并介绍了gdb的环境搭建、基本使用以及如何利用gdb来调试断点查看函数调用次序。

流程调试

在这里插入图片描述
如上图,是go程序初始化流程的整理,由于整个流程调用方法非常多,这里挑选较为核心部分进行梳理和分析,这里仅梳理初始化过程的调用流程和次序,用来熟悉初始化的工作机制,不涉及原理性分析,结合这张图的执行次序来说明,如下:

阶段次序语言环境执行文件执行函数核心逻辑
11.1汇编(.s)$GOROOT/src/runtime/rt0_darwin_amd64.s_rt0_amd64_darwin汇编引导
11.2汇编(.s)$GOROOT/src/runtime/asm_amd64.s_rt0_amd64汇编引导
11.3汇编(.s)$GOROOT/src/runtime/asm_amd64.srt0_go汇编引导
22.1golang(.go)$GOROOT/src/runtime/runtime1.goargs整理命令行参数
22.2golang(.go)$GOROOT/src/runtime/os_darwin.goosinit确定CPU数量
22.3golang(.go)$GOROOT/src/runtime/proc.goschedinit初始化、参数、环境处理
22.4golang(.go)$GOROOT/src/runtime/proc.gonewproc创建主goroutine即runtime.main对应的g
22.5golang(.go)$GOROOT/src/runtime/proc.gomstart启动调度循环
22.6golang(.go)$GOROOT/src/runtime/proc.gomain调用main goroutine运行,但不是用户函数入口的main.main
32.3 => 2.3.1golang(.go)$GOROOT/src/runtime/proc.golockInit(xxx)各类Lock的初始化
32.3 => 2.3.2golang(.go)$GOROOT/src/runtime/proc.gosched.maxmcount = 10000最大线程数
32.3 => 2.3.3golang(.go)$GOROOT/src/runtime/proc.gostackinit()栈初始化
32.3 => 2.3.4golang(.go)$GOROOT/src/runtime/proc.gomallocinit()内存管理器初始化
32.3 => 2.3.5golang(.go)$GOROOT/src/runtime/proc.gomcommoninit()调度器初始化
32.3 => 2.3.6golang(.go)$GOROOT/src/runtime/proc.gogoargs()命令行参数处理
32.3 => 2.3.7golang(.go)$GOROOT/src/runtime/proc.gogoenvs()环境变量处理
32.3 => 2.3.8golang(.go)$GOROOT/src/runtime/proc.goparsedebugvars()调试相关参数处理
32.3 => 2.3.9golang(.go)$GOROOT/src/runtime/proc.gogcinit()垃圾回收器初始化
32.6 => 2.6.1golang(.go)$GOROOT/src/runtime/proc.go(64bit-1G 32bit-250M)Stack栈的最大限制
32.6 => 2.6.2golang(.go)$GOROOT/src/runtime/proc.gosystemstack()启动系统后台监控(垃圾回收,并发调度相关)
32.6 => 2.6.3golang(.go)$GOROOT/src/runtime/proc.goruntime_initruntime包内所有init函数初始化
32.6 => 2.6.4golang(.go)$GOROOT/src/runtime/proc.gogcenable()启动垃圾回收
32.6 => 2.6.5golang(.go)$GOROOT/src/runtime/proc.gomain_init用户包内所有init函数初始化
32.6 => 2.6.6golang(.go)$GOROOT/src/runtime/proc.gomain_main调用用户程序入口执行,由编译器动态生成
32.6 => 2.6.7golang(.go)$GOROOT/src/runtime/proc.goexit(0)执行结束

案例分析

我们创建一个简单的项目,在项目main函数中import一些依赖,查看下用户main_main的方法由编译器动态生成了什么内容。

项目目录及文件结构,大致如下:

% tree
.
├── funcs
│   └── func.go
├── main.go

funcs/func.go文件

package funcs

import "fmt"

func init(){
	fmt.Println(" funcs init")
}

func Add(a int, b int) int  {
	fmt.Println("Add method called.")
	return a + b
}

main.go文件

package main

import (
	//这里导入项目包
	"program/funcs"
	"fmt"
	//这里导入外部包
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

func init()  {
	fmt.Println("main init",funcs.Add(4,5))
}

func main() {
	a, b := 1, 2
	fmt.Println("result => ", funcs.Add(a, b))
}

通过go tool objdump -s “\.init\.0\b” [program]反编译查看该项目来查看编译情况,这里信息比较多,删减保留主要信息如下:

//===== 系统内部初始化开始 =====
TEXT internal/bytealg.init.0(SB) /usr/local/go/src/internal/bytealg/index_amd64.go
//省略...                           

TEXT runtime.init.0(SB) /usr/local/go/src/runtime/cpuflags_amd64.go
//省略...                                                           

TEXT os.init.0(SB) /usr/local/go/src/os/proc.go
//省略...                           
  proc.go:18            0x10eb852               eb8c                    JMP os.init.0(SB)                       
//省略...                              

//===== 项目包内初始化开始 =====

TEXT program/funcs.init.0(SB) /Users/guanjian/workspace/go/program/funcs/func.go
//省略...                   
  func.go:6             0x10facda               e8617fffff              CALL fmt.Println(SB)                    
//省略...     
  func.go:5             0x10facee               e96dffffff              JMP program/funcs.init.0(SB) 
//省略...                                                               

//===== 外部包初始化开始 =====
TEXT github.com/go-sql-driver/mysql.init.0(SB) /Users/guanjian/go/pkg/mod/github.com/go-sql-driver/mysql@v1.5.0/driver.go
//省略...                                                               
  driver.go:83          0x12707e7               eb97                    JMP github.com/go-sql-driver/mysql.init.0(SB)                                                   
//省略...                                                                                        

TEXT main.init.0(SB) /Users/guanjian/workspace/go/program/main.go
 //省略...                    
  main.go:10            0x128cec0               e83bdee6ff              CALL program/funcs.Add(SB)   
 //省略...                   
  main.go:10            0x128cf75               e8c65ce6ff              CALL fmt.Println(SB)                    
//省略...         
  main.go:9             0x128cf96               e9e5feffff              JMP main.init.0(SB)                     
//省略...                    
//省略...  

下面是将以上编译文件主要信息进行了整理,梳理了初始化顺序、包依赖关系、编译文件映射
在这里插入图片描述

初始化顺序

通过编译文件可以梳理得知,go程序的初始化大致可以分为两个部分,分别是Go环境初始化用户环境初始化,Go环境初始化指的是SDK内部执行流程的OS环境识别读取、参数初始化、相关底层支持函数的初始化准备等;用户环境初始化指的是项目中编写的代码逻辑,这里可以再细分为当前项目代码和外部导入包代码。加载顺序是先初始化Go环境,再根据用户代码逻辑从main.main作为入口按序进行初始化。

包依赖关系

包依赖关系的次序与真正的初始化顺序是相反的,在整个依赖链条上最被依赖的包及其init方法是最先被执行初始化的,相反,依赖下游的程序入口main.main的相关初始化操作是最靠后的。

编译文件映射

总结

  • 通过编译文件和gdb的调试可以得知,所有init函数都在同⼀个goroutine 内执⾏的,感兴趣可以参考 https://github.com/golang/go/blob/master/src/runtime/proc.go中doInit的实现
  • 所有init函数结束后才会执⾏main.main函数,也就是说先完成初始化再进入程序入口,这点可以帮助我们更好地理解初始化与程序执行入口两者的次序关系,在编码时避免出现问题
  • import会产生对包的依赖,如果依赖包有init函数则会先执行,而init函数中的内容以及依赖也会有此效果,因此不控制使用init会产生隐藏地、不易察觉的依赖链并产生初始化一系列操作,所以慎用该功能,推荐的是只做当前局部作用域的初始化工作,以免带来不必要的问题隐患

参考

《Go语言学习笔记》 雨痕
搭建gdb调试go程序
golang底层 引导、初始化

### 使用 Golang 对 Kubernetes 组件进行定制化开发 #### 自定义组件开发概述 Kubernetes 提供了强大的可扩展机制,其中 CRD (Custom Resource Definitions)[^2] 和 Operator 是实现自定义功能的核心工具。CRD 允许开发者定义新的资源类型,而 Operator 则通过代码逻辑维护这些资源的状态。 为了简化复杂性的开发工作,社区提供了多种框架支持 CRD 和 Operator 的开发,比如 **Operator SDK** 和 **Kubebuilder**[^1]。这些工具帮助开发者专注于业务逻辑而非底层细节。 --- #### 创建 Custom Resource Definition (CRD) CRD 是一种声明式的资源配置方式,用于定义新类型的 Kubernetes 资源。以下是创建 CRD 的基本流程: 1. 定义一个新的资源对象结构。 2. 编写 YAML 文件描述该资源的字段及其行为模式。 3. 应用到集群中以使 kube-apiserver 认识此资源。 下面是一个简单的 CRD 示例: ```yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: myresources.example.com spec: group: example.com versions: - name: v1alpha1 served: true storage: true scope: Namespaced names: plural: myresources singular: myresource kind: MyResource ``` 上述文件定义了一个名为 `MyResource` 的新资源类型。 --- #### 开发 Operator 来管理 CRD 一旦 CRD 注册成功,就需要编写对应的 Controller 或者 Operator 来监控和响应其生命周期事件。以下是基于 Go 语言使用 **Controller Runtime** 实现的一个简单例子: ##### 初始化项目 假设已经安装好 Operator SDK 工具链,则可以通过以下命令快速搭建环境: ```bash operator-sdk init --domain=example.com --repo=github.com/example/my-operator ``` 接着为特定 CRD 添加控制器支持: ```bash operator-sdk create api --group=mygroup --version=v1alpha1 --kind=MyKind --controller=true ``` 这会生成必要的 scaffold 文件夹结构以及样板代码。 --- ##### 修改 Main 函数启动 Manager 在入口函数 `main.go` 中初始化并运行 Manager 实例负责协调整个操作过程。例如: ```go package main import ( "os" ctrl "sigs.k8s.io/controller-runtime" ) func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: ":8080", Port: 9443, HealthProbeBindAddress: ":8081", }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if err = (&myv1.MyKindReconciler{}).SetupWithManager(mgr); err != nil { setupLog.Error(err, "无法设置 Reconciler") os.Exit(1) } log.Info("开始监听...") if err := mgr.Start(signals.SetupSignalHandler()); err != nil { setupLog.Error(err, "manager stopped") } } ``` 这里设置了多个选项参数控制服务端口、健康检查地址等内容[^3]。 --- ##### 处理核心逻辑——Reconcile 方法 每个 controller 至少要实现一个 reconcile 循环方法处理实际事务。它接收输入请求后决定下一步动作直到达到期望状态为止。如下所示伪代码片段展示了典型场景下的更新策略: ```go // MyKindReconciler reconciles a MyKind object type MyKindReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme } func (r *MyKindReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { instance := &myv1.MyKind{} err := r.Get(context.TODO(), req.NamespacedName, instance) if errors.IsNotFound(err) { return ctrl.Result{}, nil } else if err != nil { r.Log.Error(err, fmt.Sprintf("获取实例失败 %s/%s", req.Namespace, req.Name)) return ctrl.Result{}, err } // 执行具体任务... newPodSpec := corev1.PodSpec{/*...*/} pod := new(corev1.Pod) pod.GenerateName = "mypod-" pod.Spec = newPodSpec if resultErr := r.Create(context.TODO(), pod); client.IgnoreAlreadyExists(resultErr) != nil { r.Log.Error(resultErr, "创建 Pod 错误") return ctrl.Result{}, resultErr } return ctrl.Result{RequeueAfter: time.Minute}, nil } ``` 以上代码片段说明当检测到某个条件满足时触发重新排队等待下一轮循环继续执行。 --- #### 结合案例分析:Spark Operator 作为真实世界中的应用范例之一,Apache Spark 社区贡献出了官方版本的 Spark Operator[^4]。借助于这个插件,用户无需手动调用传统 CLI 方式即可完成批处理或者流计算任务调度至云端平台之上无缝衔接现有基础设施架构优势最大化利用硬件性能指标提升效率降低成本开销等方面均表现出色值得借鉴学习研究价值极高! --- ###
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大摩羯先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值