叩町

叩町

【Go】Go的变量和作用域

go
4
2025-12-19
【Go】Go的变量和作用域

Go包含强大的静态类型系统,包含基本类型、复合类型、指针类型、函数类型、接口类型、通道类型等,Go内置的类型如下:

类型分类类型描述
基本类型
bool布尔类型,值为truefalse
int有符号整数类型
int88位有符号整数
int1616位有符号整数
int3232位有符号整数
int6464位有符号整数
uint无符号整数
uint88位无符号整数
uint1616位无符号整数
uint3232位无符号整数
uint6464位无符号整数
float3232位浮点数
float6464位浮点数
complex6464位复数类型
complex128128位复数类型
复合类型
byte字节类型,等于uint8
runeUnicode码点,int32的别名
string字符串
error错误接口类型,预定义类型
array定长数组
struct结构体类型
slice切片类型
map字典(哈希表)类型
指针类型pointer指针
函数类型function函数
接口类型interface接口
通道类型chan通道

变量声明

标准的变量声明格式如下:

var a int = 10
  • var:修饰变量声明的关键字
  • a:变量名
  • int:变量类型
  • =:赋值操作
  • 10:初始值

Go将变量名放在了类型的签名,这带来了以下好处:

  1. 修正了类型前置可能导致的困惑,使变量声明更加清晰易读。如int* a, b;语句在C中声明了一个int指针类型a,和一个int类型b,而仅通过字面可能会认为ab都是指针类型的变量。Go中的声明方法将更加清晰:var a, b *int
  2. 更容易表达和解析复杂的变量声明。

无论什么类型的变量,都可以使用这种通用格式进行声明,但Go也提供了一系列变体用于声明变量。

未显式赋予初值

如果不为变量指定初始值,声明方式为:

vat a int

在C语言中,如果变量未指定初值,这个变量的值是未定义的,可能是任意值,取决于内存中的垃圾数据或编译期实现,直接使用这样的变量会导致不可预测的行为和错误的结果。

Go对这一“缺陷”进行了修复,如果没有显式为变量赋予初值,编译器会为变量赋予该类型的零值。

内置原生类型默认值
所有整型0
浮点类型0.0
布尔类型false
字符串类型""
指针、接口、切片、通道、字典和函数类型nil

此外,数组、结构体等符合类型变量的零值对应其组成元素均为零值的结果。

变量声明块

除了单独声明每个变量外,Go还支持使用变量声明块的语法,允许将多个变量集中声明于一个var关键字之下:

var (
	a int = 128
	b int8 = 6
	s string = "hello"
	c rune = 'A'
	t bool = true
)

Go还支持在一行同时声明并初始化多个相同类型的变量:

var a, b, c int = 5, 6, 7

同样的多变量声明方法也可用于变量声明块:

var (
    a, b, c int = 5, 6, 7
    c, d, e rune = 'C', 'D', 'E'
) 

省略类型信息的声明

Go允许在声明变量时省略类型信息,格式为:var varName = initExpression

var a = 13

Go编译器会根据右侧的初始值自动推导出变量的类型,并赋予该变量与初值对应的默认类型,整型值默认为int,浮点数值默认为float64,复数默认类型为complex128。如果不希望使用默认类型,而是指定变量的类型,除了采用通用的变量声明外,还可以通过显式类型转换实现:

var b = int32(13)

对于省略类型信息的声明,仅适用于在声明变量的同时为其赋予初始值的情况,因为编译器需要根据初始值自动推导类型,以下写法是不允许的:

var b

结合多变量声明功能,还可以使用以下方式同时声明多个不同类型的变量:

var a, b, c = 12, 'A', "hello"

短变量声明

Go提供了一种更加简化的变量声明形式,格式为:varName := initExpression

a := 12
b := 'A'
c := "hello"
a, b, c := 12, 'A', "hello"

与之前的声明方式相比,去掉了var关键字和类型信息,使用:=操作符,变量的声明更加简洁,但是并非所有情况都适用。

Go的变量分为两类,一类是包级变量(package variable) ,也就是在包级别定义的变量。如果是导出变量(以大写字母开头),那么这个包级变量也可以视为全局变量。另一类是局部变量(local variable) ,也就是在函数或方法内部声明的变量,仅在函数或方法的作用域内有效。

包级变量的声明形式

包级变量只能使用带有var关键字的变量声明,不能使用短变量声明,但在形式细节上可以有一定的灵活性。

声明并显示初始化

对于声明时即进行显式初始化的包级变量,通常采用省略类型信息的格式。如果不接受默认类型,而需要显式指定包级变量类型时,更推荐使用var varName = type(value)的形式:

var a = 13 // 使用默认类型
var b = int32(17) // 显式指定类型
var f = float32(3.14) // 显式指定类型

声明但延迟初始化

对于声明时不立即进行显式初始化的包级变量,可以使用通用的变量声明形式:

var a int32
var f float64

在声明变量时,应注意声明聚类与就近原则的应用。通常会将同一类的变量放在一个var变量的声明块中,而将不同类的变量分别置于不同的var声明块中:

// $GOROOT/src/net/net.go
var (
    netGo  bool 
    netCgo bool 
)
var (
    aLongTimeAgo = time.Unix(1, 0)
    noDeadline = time.Time{}
    noCancel   = (chan struct{})(nil)
)

可以将延迟初始化的变量放在一个var声明块,将显式初始化的变量放在另一个声明块,这种方式称为 “声明聚类”, 可以提升代码可读性。

此外,还有一个最佳实践“就近原则”,即尽可能在靠近第一次使用变量的位置声明变量,这也是对变量作用域最小化的一种实现手段。标准库中可以找到符合这一原则的例子:

// $GOROOT/src/net/http/request.go
var ErrNoCookie = errors.New("http: named cookie not present")
func (r *Request) Cookie(name string) (*Cookie, error) {
    for _, c := range readCookies(r.Header, name) {
        return c, nil
    }
    return nil, ErrNoCookie
}

在示例中,ErrNoCookie变量仅在Cookie方法内部使用,因此被声明在紧邻该方法定义的地方。如果某个包级变量在整个包内被多处使用,则更适合将声明放在源文件的头部。

局部变量声明形式

延迟初始化的局部变量

由于省略类型信息的声明和段变量声明并不支持延迟初始化,因此,对于需要延迟初始化的局部变量,只能采用通用的变量声明形式:

var err error

声明且显示初始化的局部变量

短变量声明形式是局部变量中最常用的声明形式,对于接受默认类型的变量,可以采用以下形式:

a := 17
f :=3.14
s := "hello, gopher!"

对于不接受默认类型的变量,可以通过在:=右侧进行显式类型转换保持声明的一致性:

a := int32(17)
f := float32(3.14)
s := []byte("hello, gopher!")

短变量声明在分支控制语句中也广泛被使用:

if x, y := computeValues(); x < y {
    // 如果x小于y,则执行该代码块
    fmt.Println("x is less than y")
} 
 
for i, j := 0, 0; i < 10; i, j = i+1, j+2 {
    // 执行循环体操作
    fmt.Println(i, j)
}

如果在声明局部变量时遇到了适合聚类应用的场景,更推荐使用var声明块声明多于一个的局部变量,可以参考:

// $GOROOT/src/net/dial.go
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, 
                            addr string, hint Addr) (addrList, error) {
    …
    var (
        tcp      *TCPAddr
        udp      *UDPAddr
        ip       *IPAddr
        wildcard bool
    )
   …
}

变量作用域

在Go中,代码块是指由一对大括号包围的一系列声明和语句。如果一对大括号内不包含任何声明或语句,则称为空代码块。Go支持代码块嵌套,允许在一个代码块内部包含多个层次的子代码块:

func foo() { // 代码块1
    { // 代码块2
        { // 代码块3
            { // 代码块4
            }
        }
    }
}

代码块1到代码块4,由于由成对且可见的大括号包围,可以被称为显式代码块(explicit block)。 与之对应的隐式代码块(implicit block) 没有大括号作为标识,无法通过直接观察大括号来识别。

Go文件代码块.png

  • 宇宙代码块(universe block):涵盖了所有Go源码,可以想象为在所有Go代码的最外层添加大括号
  • 包代码块(package block):每个Go包都有一个对应的隐式包代码块,包含了该包中的所有Go源码,无论这些代码分布在包内的多少个文件内。
  • 文件代码块(file block):每个Go源文件都对应一个文件代码块,因此,如果一个Go包包含多个源文件,则会有多个对应的文件代码块。
  • 控制语句层面的隐式代码块:ifforswitch控制语句都可以视为在自己的隐式代码块内运行,需要注意的是,这里的隐式代码块与使用大括号包围的显示代码块不同,例如在switch控制语句中,隐式代码块在它的显示代码块外部。
  • switchselect语句中子句的代码块:casedefault子句都构成独立的代码块。

一个标识符的作用域指其声明后可以有效使用的源码区域,这是一个编译期的概念,如果在超出标识符作用域的范围使用标识符,在编译阶段会出现错误。标识符的作用域可以使用代码块来划分:声明在外层代码块中的标识符,作用域包括所有内层代码块。 以下为举例说明:

  • 宇宙代码块中的标识符:我们无法手动在宇宙代码块中声明任何自定义标识符,因为这是Go为预定义标识符预留的空间,这些预定义标识符位于包代码块之外,作用域是最大的,可以在源码任何位置使用,但这些标识符不是关键字,因此我们可以在内层代码块中声明同名的标识符。

  • 包代码块级别作用域:在包顶层声明的常量、类型、变量或函数(方法除外)对应的标识符具有包代码块作用域。

  • 文件代码块级别:导入的包名、函数、方法的实现

  • 函数/方法体内:可直接观察配对的大括号界定标识符的作用范围:

    func (t T) M1(x int) (err error) {
    // 代码块1
        m := 13
        // 代码块1是包含m、t、x和err的最内部代码块
        { // 代码块2
     
            // 代码块2是包含类型bar标识符的最内部的包含代码块
            type bar struct {} // 类型bar标识符的作用域始于此
            { // 代码块3
     
                // 代码块3是包含变量a标识符的最内部的包含代码块
                a := 5 // a的作用域始于此
                {  // 代码块4 
                    …
                }
                // a的作用域终止于此
            }
            // 类型bar标识符的作用域终止于此
        }
        // m、t、x和err的作用域终止于此
    }
    
  • 控制语句:即使if else后声明的变量在不同层次的隐式代码块中,实际上每一个else都可以被视为嵌套在上一个代码块中,因此变量在整个if范围内都是有效的,且保持了初始值。

  • 0