刚学Go的同学一定想过Go程序的启动过程。关于这个问题,可以阅读饶达的文章HowGoprogramsrun。今天我们将问题缩小,学习Go程序如何加载启动参数以及如何解析参数。C参数解析学过C语言的童鞋们一定对argc和argv不陌生。C程序总是从主函数main开始执行,在带参数的主函数中,按照惯例,argc和argv的名字会作为主函数的参数。其中argc(argumentcount)表示命令行参数的个数,argv(argumentvalue)是参数指针数组。#includeintmain(intargc,char*argv[]){printf("argc=%d\n",argc);printf("argv[0]=%s,argv[1]=%s,argv[2]=%s\n",argv[0],argv[1],argv[2]);return0;}编译执行以上C代码,输出如下$gccc_main.c-omain$./mainfobarsssdddargc=5argv[0]=./main,argv[1]=foo,argv[2]=bar那么在Go语言中,如何获取命令行参数呢?os.Args和C一样加载,Go程序也是从main加载main函数开始(用户层)执行,但是main函数中没有定义argc和argv。我们可以通过os.Args函数获取命令行参数。packagemainimport("fmt""os")funcmain(){fori,v:=rangeos.Args{fmt.Printf("arg[%d]:%v\n",i,v)}}编译执行Go函数$gobuildmain.go$./maininfoobarsssdddarg[0]:./mainarg[1]:fooarg[2]:bararg[3]:sssarg[4]:ddd和C一样,第一个参数也代表可执行文件。加载实现下面我们需要展示一些Go汇编代码。为了方便读者理解,我们先通过两张图来理解Go汇编语言对CPU的重新抽象。X86/AMD64ArchitectureGo伪寄存器Go汇编为了简化汇编代码的编译,引入了四个伪寄存器PC、FP、SP、SB。四个伪寄存器加上其他通用寄存器,是Go汇编语言对CPU的重新抽象。当然,这种抽象结构也适用于其他非X86类型的架构。回到正题,命令行参数的解析过程是程序启动的一部分。以linuxamd64系统为例,Go程序的执行入口位于runtime/rt0_linux_amd64.s。TEXT_rt0_amd64_linux(SB),NOSPLIT,$-8JMP_rt0_amd64(SB)_rt0_amd64函数在runtime/asm_amd64.sTEXT_rt0_amd64(SB),NOSPLIT,$-8MOVQ0(SP),DI//argcLEAQ8(SP),SI//argvJMPruntime·rt0_go(SB)看到argc和argv了吗?在这里,它们分别从堆栈内存加载到DI和SI寄存器。rt0_go函数完成了runtime的所有初始化工作,但我们这里只关注argc和argv的处理。TEXTruntime·rt0_go(SB),NOSPLIT|TOPFRAME,$0//copyargumentsforwardonanevenstackMOVQDI,AX//argcMOVQSI,BX//argvSUBQ$(4*8+7),SP//2args2autoANDQ$~15,SPMOVQAX,16(SP)MOVQBX,24(SP)...MOVL16(SP),AX//copyargcMOVLAX,0(SP)MOVQ24(SP),AX//copyargvMOVQAX,8(SP)CALLruntime·args(SB)CALLruntime·osinit(SB)CALLruntime·schedinit(SB)...经过一系列操作后,argc和argv返回到栈内存0(SP)和8(SP)。args函数位于runtime/runtime1.govar(argcint32argv**byte)funcargs(cint32,v**byte){argc=cargv=vsysargs(c,v)}这里argc和argv保存到变量runtime分别。argc和runtime.argv。在rt0_go函数中调用并执行args函数后,schedinit也会被执行。funcschedinit(){...goargs()...goargs在runtime/runtime1.govarargslice[]stringfuncgoargs(){ifGOOS=="windows"{return}argslice=make([]string,argc)fori:=int32(0);i