Golang中新建变量时使用`:=`的一些思考
Golang中新建变量时使用:=的一些思考
背景
昨天,我编写了项目的数据库驱动部分,代码如下:
var (
DBdriver = "mysql"
DBinfo = "xxx"
DBengine *xorm.Engine
)
func init() {
DBengine, err := xorm.NewEngine(DBdriver, DBinfo)
...
}
我原以为 DBengine 变量已在全局声明,这里即使 err 是新变量,编译器也会将返回结果赋值给全局变量中的 DBengine。但实际上,由于使用了 :=,DBengine 也被视为一个局部变量进行创建和赋值。
我最终将代码修改为:
func init() {
var err error
DBengine, err = xorm.NewEngine(DBdriver, DBinfo)
...
}
内容
但我在想,为什么编译器被设计成在这种模棱两可的情况下将 DBengine 创建为局部变量,而不是优先检查是否已存在同名变量并赋值给它。我猜,从代码的编写方式来看,这只会让编码者如果需要将 DBengine 作为局部变量,就在此行之前创建它,这与我当前的解决方案没有太大区别;从代码逻辑来看,这样设计更符合逻辑且对初学者更友好。
有人能解释为什么编译器要这样设计吗?以及我是否有更好的编码方式?
更多关于Golang中新建变量时使用`:=`的一些思考的实战教程也可以访问 https://www.itying.com/category-94-b0.html
考虑到全局声明可能位于函数代码上方数十行,甚至可能在一个完全不同的文件中。
这正是核心所在。谢谢!
所以编译器只会搜索局部变量,如果存在同名变量就赋值,如果尚未声明则创建一个新变量。我写了一些代码来确认这一点。代码如下:
package main
import (
"fmt"
)
func main() {
test()
}
func test() {
var foo int
fmt.Println(&foo)
foo, err := bar()
if err != nil {
fmt.Println(err)
}
fmt.Println(&foo)
}
func bar() (int, error) {
return 1, nil
}
正如我所预期的,它们打印出了相同的地址。😄
更多关于Golang中新建变量时使用`:=`的一些思考的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
操作符
:=总是创建一个新变量。
@christophberger 没有触及的一点是,它可以在局部作用域中重用变量,按照惯例,这通常是最后一个变量,例如 err 或 ok:
fooResult, err := foo()
if err!=nil {
log.Fatalf(err)
}
barResult, err := bar()
...
请看这里,它打印了被重用变量的地址,并且这些地址没有改变。Go Playground - The Go Programming Language
如果你将 fooResult 的声明移到全局作用域,那么它会打印两个不同的 fooResult 地址。
我相信 @christophberger 知道所有这些,但为了向提问者澄清。
你好 @Augists,
这种行为是明确定义的。这里涉及两个方面:
:=操作符总是创建一个新变量。在任何情况下,它都不会尝试在外部作用域中查找已存在的变量并将值赋给那个变量。- 局部变量总是会遮蔽在外部作用域(包括全局作用域)中声明的同名变量。
如果不是这样,代码将更难阅读,也更容易出错。
原因如下:假设 := 有时会创建一个新的局部变量,而有时又会给一个全局变量赋值。如果你只查看包含 := 操作的函数,你将无法判断 := 是创建了一个新的局部变量,还是给一个已存在的全局变量赋值。(想象一下,全局变量的声明可能在函数代码上方几十行,甚至在一个完全不同的文件中。)意外地给全局变量赋值而不是创建局部变量,这种情况就太容易发生了。
在Go语言中,:=操作符被设计为短变量声明,它的核心行为是:为左侧所有变量创建新的局部变量。这是语言规范明确规定的行为,而不是编译器的自由裁量。
为什么这样设计?
1. 明确性和一致性
:=的语义非常明确:声明并初始化新变量。如果它有时创建新变量,有时复用已有变量,会导致:
- 代码可读性下降
- 难以预测的行为
- 隐藏的bug
var x int = 10
func example() {
// 如果这里复用全局x,会修改全局状态
// 如果创建新x,则是局部变量
x, y := 20, 30 // 明确:创建新的局部x和y
fmt.Println(x, y)
}
2. 避免意外修改
如果:=可以复用外部作用域的变量,很容易意外修改全局或外层变量:
var globalErr error
func process() error {
data, globalErr := parseInput() // 如果复用globalErr,会意外修改全局变量
if globalErr != nil {
return globalErr
}
// ... 使用data
}
3. 作用域清晰
Go强调明确的作用域。每个变量都应该在最小的必要作用域内声明:
func goodExample() {
// 明确:err是局部变量
result, err := someOperation()
if err != nil {
// 处理错误
}
// 明确:复用result,新声明err2
finalResult, err2 := anotherOperation(result)
if err2 != nil {
// 处理错误
}
}
更好的编码方式
方案1:显式声明错误变量(你当前的方案)
func init() {
var err error
DBengine, err = xorm.NewEngine(DBdriver, DBinfo)
if err != nil {
log.Fatal("数据库连接失败:", err)
}
}
方案2:使用函数返回值
func initDB() (*xorm.Engine, error) {
return xorm.NewEngine(DBdriver, DBinfo)
}
func init() {
var err error
DBengine, err = initDB()
if err != nil {
log.Fatal("数据库连接失败:", err)
}
}
方案3:使用辅助函数处理错误
func mustInitEngine(driver, info string) *xorm.Engine {
engine, err := xorm.NewEngine(driver, info)
if err != nil {
log.Fatal("数据库连接失败:", err)
}
return engine
}
func init() {
DBengine = mustInitEngine(DBdriver, DBinfo)
}
方案4:使用闭包(适用于复杂初始化)
func init() {
var err error
DBengine, err = func() (*xorm.Engine, error) {
engine, err := xorm.NewEngine(DBdriver, DBinfo)
if err != nil {
return nil, err
}
// 可以进行其他初始化配置
engine.SetMaxOpenConns(100)
return engine, nil
}()
if err != nil {
log.Fatal("数据库初始化失败:", err)
}
}
关键点总结
:=总是创建新变量,这是语言设计的选择,不是编译器实现细节- 这种设计避免了作用域混淆和意外修改
- 当需要赋值给已存在的变量时,使用
=而不是:= - 错误处理应该显式、明确,这是Go语言的哲学
你的修改方案(方案1)是最常见和推荐的做法,因为它:
- 明确区分了变量声明和赋值
- 保持了代码的清晰性
- 符合Go语言的惯用法

