Golang中变量声明的探讨 - 函数与包级别的区别
Golang中变量声明的探讨 - 函数与包级别的区别 我正在尝试理解Go编译器背后的设计哲学:为什么在函数内部声明但未使用的变量会引发编译器报错,而在包级别声明的变量却不会?
包级别声明的未使用变量是被静默移除了,还是为了其他场景而保留着?
编译器了解未使用的函数变量。编译器不了解未使用的包变量,链接器了解。
更多关于Golang中变量声明的探讨 - 函数与包级别的区别的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
作为一个新手,我刚刚了解到“链接器”,但这个问题可以修正为“为什么编译过程不会对包级别未使用的已声明变量报错”。
这并没有回答问题:为什么未使用的函数作用域变量会引发错误,而未使用的全局变量则不会。
给 @hollowaykeanho 一个有趣的小知识:另一个非常快速(但也很粗糙)的检查变量是否被包含进二进制文件的方法是,编译 var a = "Lorem Ipsum 12",然后直接在二进制文件上使用 grep 搜索这个字符串。
var a = "Lorem Ipsum 12"
问题是“为什么编译器不报错”,这个问题已经得到了“它无法知道”的回答。
一个可能的后续问题是,为什么链接器不报错。不过这可能只是“没有人想过要实现这个功能”这么简单。
我快速浏览了一下,在问题跟踪器中没有发现关于这个问题的投诉。
hollowaykeanho:
有一个快速但不太规范的方法:编译一个包含未使用包级变量的“Hello World”程序,然后反汇编它,接着分析汇编代码来定位这个变量。不过,我时间有点紧,所以如果有人感兴趣,请尽管去试试(使用
amd64CPU,因为它的数据手册更容易阅读):
未使用的包级变量不会进入二进制文件——它们会被链接器丢弃……
未使用的包级变量不会进入二进制文件——它们会被链接器丢弃
太好了。这为我的假设提供了支持性的证据数据。感谢你的努力。😄
使用
grep直接在二进制文件中搜索该字符串。
哈哈。是的。这是个跳过数据表比较的好技巧。我太习惯于进行数据表比较了。😅
从哲学角度而言,我坚定且强烈地认为,同样的哲学也适用于包级别,特别是看待私有变量时(当然,导出变量是为了供外部使用)。因此,在我看来,当你探索未知领域时,你遇到了一个错误。
再一想,你可以给 golang-nuts 邮件列表发邮件,专门询问第二个问题。这已经符合条件了:
我已经给他们发了邮件。如果这是一个错误,那将是一次彻底的遭遇,我认为至少在哲学层面上是如此。
以防其他人对这个话题感兴趣,Ian Taylor 提供了以下回复:
包作用域的变量可以为其他包(如果变量是导出的)或调试器提供钩子,以改变程序行为。 编译器或链接器没有简单的方法来知道这种情况是否会发生。
特别是编译器(也许不幸地)支持
go:linkname指令,该指令可以访问另一个包并引用包作用域的变量。这通常不是好的做法,但现有的代码确实有这样做。
感谢您的详细阐述,老实说,我对Ian的回答并不完全信服,或许作为一名语言新手,我还不能完全理解它,当然也还没有能力与Ian这样水平的人就此进行辩论。
我粗浅的哲学层面理解是,如果构建过程能够清理已导入但未使用的包,那么它最好也能清理包级别未导出的已声明变量。
如果目前没有相关的代码实现,也许应该将其添加到路线图中,以维护Go关于强制编写简洁、可用代码的承诺。
也许我在“学习”这门语言时偏离了正轨,但我现在正处于这个模糊不清的区域,无法确定自己的理解处于何种水平。要回到这一点并重新思考,肯定还需要一些时间。也许到那时,我至少不会太在意它,毕竟如果它能正常工作,就不要去修复它。
感谢您提供的详细信息,@hollowaykeanho。 对于第一部分,同样的原则应该适用:如果是在函数内部的浪费,那么它就可以作为死代码被移除,类似于函数级别的处理。
不过,我之前并不熟悉编译过程的细节,但我刚刚阅读了以下文档。它们确实提到了移除“死代码”,但没有解释为什么不对此发出警告,或者如何选择性地启用它。
Jad:
我之前对编译过程的细节并不熟悉,但我刚刚阅读了以下文档,它们确实提到了移除“死代码”,但没有解释为什么不对此发出警告,或者如何选择性地启用警告。
根据你的视角,严格来说,编译和链接是两个不同的工作阶段。简而言之,编译是将你编写的人类友好的代码翻译成CPU友好的二进制指令,而链接则是将多个二进制指令“粘合”在一起。
然而,从普通Go用户的角度来看,编译和链接被视为一个整体:构建,因为你没有在其他地方被指示需要手动链接多个Go二进制文件。因此,我期望它会发出警告。
从哲学角度看,我坚定地认为同样的理念也适用于包级别,特别是对于私有变量(当然,导出变量是为了供外部使用)。这就是为什么我认为,当你探索一个未被探索的领域时,你遇到了一个错误。
再一想,你可以给golang-nuts邮件列表发邮件,专门询问第二个问题。这已经符合条件了:
在Go语言中,函数内部和包级别未使用变量的不同处理方式源于其设计哲学中的明确性和作用域差异。
函数级别未使用变量:
函数内部声明的变量若未使用,编译器会报错(declared and not used)。这强制开发者保持代码的简洁性,避免因遗留无用变量而降低可读性或引发潜在错误。
示例:
func example() {
var unused int // 编译器报错:unused declared and not used
used := 1
fmt.Println(used)
}
包级别未使用变量: 包级别未使用的变量是允许的,因为它们可能被其他文件通过导入包的方式使用。编译器无法在单个文件编译时确定这些变量是否会被包外代码引用。
示例:
package main
var globalUnused int // 允许:可能被其他文件使用
func main() {
// globalUnused 未在此使用,但不会报错
}
实际差异: 包级别变量具有包级作用域,其生命周期贯穿程序运行。编译器在编译单个文件时无法判断这些变量是否会被同一包的其他文件使用,因此不会报错。而函数内部变量的作用域仅限于该函数,其使用情况可以完全确定。
这种设计平衡了代码严谨性和包级代码组织的灵活性。

