Administrator
发布于 2024-09-06 / 57 阅读
0
0

Go包级别变量的初始化

包级别变量的初始化

最近在看Go的单例模式的实现时,想到一个问题。包中定义的变量,直接初始化和通过init函数初始化的区别?谁先谁后?

package singleton

type Singleton struct{}

var singletonA *Singleton = &Singleton{}
var singletonB *Singleton

func init() {
	singletonB = &Singleton{}
}

func GetInstance() *Singleton {
	return xxx
}

直接初始化是静态初始化,通过init初始化是动态初始化,静态初始化在编译时确定,在程序启动时立即初始化,比init函数的执行要早。

在 Go 语言中,静态初始化指的是在编译时确定并在程序启动时立即进行的变量初始化。换句话说,静态初始化是对全局变量或包级别变量在程序运行之前(在编译期间)已经确定了的初始化过程。

静态初始化的特点

  1. 在编译时确定静态初始化通常在程序编译阶段就已经确定了初始值,编译器将这些初始化代码直接嵌入生成的二进制文件中。

  2. 程序启动时立即进行:静态初始化发生在程序启动时,在所有代码(包括 main 函数和 init 函数)执行之前。包内的变量初始化顺序是按照它们在源代码中的声明顺序进行的。

  3. 不依赖于运行时逻辑:静态初始化通常用于不需要运行时逻辑的简单值(如常量、数字、布尔值或静态结构体的分配),这些值在编译时就能确定下来。

静态初始化的示例

以下是 Go 中的一些静态初始化的例子:

package main

import "fmt"

// 静态初始化
var a int = 42                // 整数变量的静态初始化
var b string = "Hello, Go!"    // 字符串变量的静态初始化
var c = []int{1, 2, 3}         // 数组切片的静态初始化

type Config struct {
    Port int
    Host string
}

var config = Config{Port: 8080, Host: "localhost"}  // 结构体的静态初始化

func main() {
    fmt.Println(a, b, c, config)
}

在这个示例中,abcconfig 变量的初始化都属于静态初始化。这些初始化在程序启动之前就已经被确定,并且会在程序的 main 函数和任何 init 函数运行之前完成。

静态初始化与动态初始化的区别

特性

静态初始化

动态初始化

初始化时机

在编译时确定,程序启动时立即初始化

在运行时进行(通常在 init 函数或其他函数中)

初始化方式

直接在声明时初始化

通过运行时逻辑或 init 函数初始化

依赖性

不依赖于运行时的任何其他变量或条件

可以依赖运行时的其他变量、资源或外部输入

典型用途

简单的值初始化(如常量、数字、字符串等)

复杂初始化,可能需要进行条件判断或计算

何时使用静态初始化

  • 简单值:当变量的初始值在编译时已经确定并且不会改变时,使用静态初始化是最佳选择。

  • 高效初始化:静态初始化不需要额外的运行时开销,因为它发生在程序开始之前。

总结

静态初始化是一种在编译时就能确定的初始化方式,适用于简单和确定性的值,确保这些变量在程序运行的最早阶段就被正确初始化。它与动态初始化(通常在 init 函数或其他运行时逻辑中进行)相对,后者用于需要依赖其他条件或复杂逻辑的初始化场景。

联想:C++静态变量

联想:在 Go 语言中,没有传统意义上的“静态变量”(如在 C 或 C++ 中的 static 变量)。但是,Go 通过包级别(或全局)变量来实现类似于“静态变量”的功能。

联想:ELF格式

联想:ELF文件的格式和已初始化包级别变量,未初始化包级别变量(通过init或其他函数初始化)的关系

在 Go 语言中,编译出的可执行文件(通常是 ELF 格式在类 Unix 系统中)包含了程序的代码、数据、和其他必要的元信息。包级别变量和其他数据是如何存放在二进制文件中的,可以从以下几个方面来理解:

Go 编译出的可执行文件

  1. 文件格式

  • 在 Linux 上,Go 编译器通常生成 ELF 格式的可执行文件。ELF(Executable and Linkable Format)是一种标准的二进制文件格式,用于存储可执行文件、共享库和核心转储等。

  • 在 Windows 上,Go 编译器生成的是 PE(Portable Executable)格式的文件。

  • 在 macOS 上,Go 编译器生成的是 Mach-O 格式的文件。

  1. 文件结构

  • ELF Header:包含文件的基本信息,如文件类型、目标架构、入口点地址等。

  • Program Header Table:描述程序的内存映射布局,包括各个段的属性和位置。

  • Section Header Table:描述节的属性,如符号表、字符串表、调试信息等。

  • Segments:用于内存映射的段,包括代码段、数据段、BSS 段等。

  • Sections:用于存储额外的信息,如符号表、重定位信息、调试信息等。

包级别变量的存放

包级别变量在 Go 编译出的可执行文件中会被存放在特定的段中,这些段由编译器和链接器生成,通常包括以下内容:

  1. 数据段(.data)

  • 包含已初始化的全局和静态变量。包级别变量如果有初始值(例如 var a = 42)会被存放在数据段中

  • 这个段的内容在程序加载时会被加载到内存中,并且可以被读取和修改。

  1. BSS 段(.bss)

  • 包含未初始化的全局和静态变量。对于没有明确初始化值的包级别变量(例如 var b int),它们会被存放在 BSS 段中。

  • BSS 段的内容在程序启动时会被清零,这样所有未初始化的变量在使用时都将具有零值。

  1. 程序头表和节头表

  • 在 ELF 文件中,程序头表(Program Header Table)包含了各个段的描述信息,指示操作系统如何将这些段映射到进程的虚拟地址空间中。

  • 节头表(Section Header Table)包含了节的信息,帮助链接器和调试工具处理文件中的数据。

示例:如何存放包级别变量

假设我们有以下 Go 代码:

package main

import "fmt"

var a int = 10
var b string = "Hello"

func main() {
    fmt.Println(a, b)
}

在编译成可执行文件后:

  1. 已初始化的变量 ab

  • a(整数)会被放置在数据段(.data)中,因为它有一个初始值 10

  • b(字符串)也会被放置在数据段中,因为它的初始值是 "Hello"

  1. 未初始化的变量

  • 如果有未初始化的全局变量,它们会被放置在 BSS 段(.bss)中。

编译和链接过程

  1. 编译

  • 编译器将 Go 源代码转换为中间的目标文件,其中包含了已初始化的数据、未初始化的数据、符号表、调试信息等。

  1. 链接

  • 链接器将目标文件中的各个部分结合起来,生成最终的可执行文件。它处理了所有的符号解析、重定位和最终的段布局。

  1. 生成的可执行文件

  • 在最终的可执行文件中,已初始化的数据存储在 .data 段中,未初始化的数据存储在 .bss 段中。程序头表和节头表则用于描述如何加载和组织这些数据。

总结

Go 编译出的可执行文件(在类 Unix 系统上通常是 ELF 格式)包含了程序的代码、数据和其他信息。包级别变量根据是否初始化被存放在不同的段中:已初始化的变量存放在 .data 段中,而未初始化的变量存放在 .bss 段中。这些段在程序启动时被加载到内存中,并按照它们的属性进行处理。

测试:包级别变量存放在ELF文件的哪个段?

测试:golang的包变量存放在ELF文件的哪个段?使用下面的代码进行了测试,发现包级别变量可能存在于data段,也可能存在于bss段,有些变量虽然看似在定义的同时进行了初始化,但是还是存放在bss段中,这可能和golang编译器的优化有关,需要更深入的探索。

/*
	用来测试包级别的变量存储在ELF文件的哪个段
*/

package main

import (
	"fmt"
	"math/rand"
)

//var slice []int //只定义,然后在init函数中初始化
//
//func init() {
//	slice = make([]int, 5000)
//}

//var slice = make([]int, 5000000) //定义并初始化

//上面两种方式定义了slice,但是最后编译得到的二进制文件的大小是一样的,说明slice并没有在编译阶段就赋值然后存储到二进制文件中

//如果定义的包级别变量没有被使用到的话,会被编译器优化掉,不会出现在二进制文件中

var sliceMake = make([]int, 5)

var slicePerm = rand.Perm(5)

var sliceInt = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

var globalVar int = 42

var strA string

var strB = "abc"

func main() {
	fmt.Println("Hello World")
	fmt.Println(slicePerm)
	fmt.Println(globalVar)
	fmt.Println(sliceInt)
	fmt.Println(sliceMake)
	fmt.Println(strA)
	fmt.Println(strB)
}

/*

通过nm工具分析go build后生成的二进制文件,看看哪些包级别变量被初始化并写入到了data段中
nm -n package_variable  | grep main

00000000004337a0 T runtime.main
0000000000433ae0 T runtime.main.func2
0000000000458be0 T runtime.main.func1
0000000000483220 T main.main
0000000000483480 T main.init
00000000004bacb8 R runtime.mainPC
000000000051a270 D main.globalVar
000000000051bae0 D main..inittask
000000000052c340 D main.strB
000000000052c4c0 D main.sliceInt
0000000000533420 B runtime.main_init_done
00000000005336a0 B main.strA
0000000000533830 B main.sliceMake
0000000000533850 B main.slicePerm
000000000056138c B runtime.mainStarted

可以看到strB, globalVar和sliceInt前面是D,表示data段,data段存放已经被初始化的静态变量
000000000051a270 D main.globalVar
000000000052c4b0 D main.sliceInt

strA, sliceMake和slicePerm前面是B,表示bss段,bss段存放没有被初始化的静态变量
0000000000533820 B main.sliceMake
0000000000533840 B main.slicePerm

*/

代码仓库地址:

https://github.com/Taichidasheen/LearningGo/blob/master/package_variable/var.go


评论