Skip to content

02 变量的定义与使用

变量

  • 静态语言的习惯:使用变量前,先声明!
  • 变量种类:全局变量,局部变量

全局变量

全局变量是指在函数外部声明并且可以被整个包中的所有函数访问的变量。它们的生命周期贯穿程序的整个运行过程,因此一旦声明,除非程序结束或显式地改变其值,全局变量会一直存在。

  • 声明方式:使用 var 关键字
  • 分类:延迟赋值,立刻赋值
    • 立刻赋值(Immediate Assignment):变量在声明的时候立刻进行初始化赋值。
    • 延迟赋值(Delayed Assignment):变量先声明,稍后在程序运行过程中进行赋值。
go
package main

import "fmt"

// 立刻赋值
var GlobalVar1 int = 100
var GlobalVar2 float32 = 123.456
var GlobalVar3 string = "hello world"

// 立刻赋值 (使用变量块)
var (
	GlobalVar4 int = 100
	GlobalVar5     = float32(123.456) // GlobalVar5 float32 = 123.456
	GlobalVar6     = "hello world"    // GlobalVar6 string = "hello world"
)

// 延迟赋值
var GlobalVar7 int
var GlobalVar8 float32
var GlobalVar9 string

// 延迟赋值 (使用变量块)
var (
	GlobalVar10 int
	GlobalVar11 float32
	GlobalVar12 string
)

func main() {
	// 打印 立刻赋值
	fmt.Println("GlobalVar1 =", GlobalVar1, "\tGlobalVar2 =", GlobalVar2, "\tGlobalVar3 =", GlobalVar3)

	// 打印 立刻赋值 (使用变量块)
	fmt.Printf("GlobalVar4 = %d\t GlobalVar5 = %f\t GlobalVar6 = %s\n", GlobalVar4, GlobalVar5, GlobalVar6)

	// 使用 := 进行赋值
	GlobalVar7 := 100
	GlobalVar8 := 123.456
	GlobalVar9 := "hello world"
	// 打印 延迟赋值
	fmt.Printf("GlobalVar7 = %d\t GlobalVar8 = %f\t GlobalVar9 = %s\n", GlobalVar7, GlobalVar8, GlobalVar9)

	// 使用 := 进行赋值
	GlobalVar10, GlobalVar11, GlobalVar12 := 100, 123.456, "hello world"
	// 打印 延迟赋值 (使用变量块)
	fmt.Printf("GlobalVar10 = %d\t GlobalVar11 = %f\t GlobalVar12 = %s\n", GlobalVar10, GlobalVar11, GlobalVar12)
}
bash
 go run global_variable_definition.go
GlobalVar1 = 100        GlobalVar2 = 123.456    GlobalVar3 = hello world
GlobalVar4 = 100         GlobalVar5 = 123.456001         GlobalVar6 = hello world
GlobalVar7 = 100         GlobalVar8 = 123.456000         GlobalVar9 = hello world
GlobalVar10 = 100        GlobalVar11 = 123.456000        GlobalVar12 = hello world

全局变量的特点

  • 作用域广泛:全局变量的作用域在其声明的包内,包中的所有函数都可以访问它。如果需要跨包访问全局变量,必须通过首字母大写来导出该变量。
  • 生命周期长:全局变量的生命周期从程序启动到结束,它们在程序启动时分配内存,并且在整个程序运行期间保持可用。
  • 可导出:如果全局变量的首字母大写,则可以被其他包访问,这相当于将该变量导出为公共变量。
  • 避免过度使用:虽然全局变量很方便,但滥用可能会导致代码难以维护、调试困难、并发问题等。因此建议在需要时谨慎使用全局变量。

声明全局变量

全局变量的声明通常位于包级别,也就是在所有函数外面。

go
package main

import "fmt"

// 全局变量声明
var (
	x int    = 100
	y string = "Hello"
)

func main() {
	fmt.Println("x in main:", x)
	fmt.Println("y in main:", y)

	modifyVariables()
	fmt.Println("x after modification:", x)
}

func modifyVariables() {
	// 在其他函数中修改全局变量
	x = 200
}
bash
 go run modify_global_variable.go
x in main: 100
y in main: Hello
x after modification: 200
  • 全局变量 xy:这些变量在包级别声明,x 被初始化为 100y 被初始化为 "Hello"
  • 全局变量的访问:在 main 函数中可以直接访问 xy,也可以在 modifyVariables 函数中访问并修改它们的值。
  • 生命周期:全局变量的生命周期贯穿整个程序执行期间。

使用大写字母导出全局变量

如果需要在其他包中访问某个全局变量,可以通过将变量名首字母大写来导出它。

go
// 在包 a 中声明
package a

// 全局变量导出
var GlobalVar = 500
go
// 在包 main 中访问 a 包的全局变量
package main

import (
    "fmt"
    "a"
)

func main() {
    fmt.Println("GlobalVar from package a:", a.GlobalVar)
}
bash
 go run access_package_global_var.go
GlobalVar from package a: 500
  • GlobalVar 是包 a 的全局变量,通过将首字母大写的方式导出,使得其他包可以引用和访问该变量。

全局变量的并发问题

在并发场景下,如果多个 goroutine 同时读写全局变量,可能会引发数据竞争问题。因此,涉及并发访问时,需要使用同步机制,如 sync.Mutexsync.RWMutex 或使用 channel 来确保并发访问的安全性。

并发修改全局变量

go
package main

import (
    "fmt"
    "sync"
)

var counter int
var mutex sync.Mutex

func increment() {
    for i := 0; i < 1000; i++ {
        // 使用互斥锁保护全局变量的修改
        mutex.Lock()
        counter++
        mutex.Unlock()
    }
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        increment()
    }()

    go func() {
        defer wg.Done()
        increment()
    }()

    wg.Wait()
    fmt.Println("Final counter value:", counter)
}
bash
 go run concurrent_modify_global_variable.go
Final counter value: 2000
  • 全局变量 counter:它被两个 goroutine 同时修改。
  • 互斥锁 mutex:通过互斥锁 sync.Mutex 来保护对全局变量 counter 的并发修改,防止数据竞争。
  • 并发:两个 goroutine 并发执行 increment 函数,在每次修改全局变量前加锁,修改后解锁,确保线程安全。

总结

全局变量在 Go 中非常方便,可以在整个包中共享状态,但应尽量避免滥用,尤其是在并发场景下,可能导致数据竞争和复杂的调试问题。对于并发访问全局变量的情况,使用同步机制来确保线程安全。

变量块

  • 变量块是指多个变量在同一个作用域内的批量声明。
  • 通常使用 var 关键字声明变量,而变量块的作用是通过一次 var 关键字来声明多个变量,使代码更加简洁和易读。
go
var (
    var1 type1
    var2 type2
    var3 type3 = value3
)
go
package main

import "fmt"

func main() {
    var (
        a int    // 声明一个 int 类型的变量 a
        b string // 声明一个 string 类型的变量 b
        c = true // 声明并初始化一个 bool 类型的变量 c
    )

    a = 10
    b = "Go 语言"

    fmt.Println("a:", a)
    fmt.Println("b:", b)
    fmt.Println("c:", c)
}
bash
 go run variable_block_declaration.go
a: 10
b: Go 语言
c: true
  • 通过 var 关键字,多个变量 abc 被同时声明。
  • 其中 ab 只进行了声明,c 则在声明时进行了初始化。
  • 之后,我们为变量 ab 赋值。

这种方式适用于在同一个作用域内声明多个变量,特别是当变量类型不同或需要进行不同的初始化时。

局部变量

  • 声明方式:使用 var 关键字、:= 关键字
  • 分类:延迟赋值,立刻赋值

局部变量在 Go 语言中是指只在函数或代码块的作用域内声明和使用的变量。局部变量的生命周期仅限于它所在的函数或代码块,函数执行结束后,这些变量将被销毁,不能在函数之外访问。

局部变量的特点

  • 作用域限制:局部变量的作用域只在函数或代码块内部,无法在外部访问。
  • 生命周期短:局部变量在函数调用时创建,函数执行完毕后销毁。
  • 可自动推断类型:局部变量可以通过简短变量声明方式 := 自动推断类型。
  • 不需要显式初始化:如果局部变量在声明时没有初始化,会自动赋值为其类型的零值(比如整数的零值是 0,布尔值的零值是 false,字符串的零值是 "",指针的零值是 nil)。

声明局部变量

go
package main

import "fmt"

func main() {
	// 声明局部变量
	var a, b int
	a, b = 1, 2
	fmt.Printf("a = %d\t b = %d\t a + b = %d\n", a, b, a+b)

	// 使用简短声明方式,自动推断类型
	m, n := 100, 200
	fmt.Printf("m = %d\t n = %d\t m + n = %d\n", m, n, m+n)

	// 变量块
	var (
		x = int64(100)
		y = int64(200)
	)
	fmt.Printf("x = %d\t y = %d\t x + y = %d\n", x, y, x+y)
}
bash
 go run local_variable_declaration.go
a = 1    b = 2   a + b = 3
m = 100  n = 200         m + n = 300
x = 100  y = 200         x + y = 300

局部变量在函数内部

go
package main

import "fmt"

func main() {
    x := 5
    fmt.Println("x in main:", x)

    // 调用局部函数
    test()
}

func test() {
    // 局部变量,只在 test 函数内有效
    y := 10
    fmt.Println("y in test:", y)
}
bash
 go run local_variable_usage.go
x in main: 5
y in test: 10
  • 变量 xmain 函数的局部变量,只能在 main 函数中使用。
  • 变量 ytest 函数的局部变量,只能在 test 函数中使用。

局部变量的作用域和生命周期

  • 作用域:局部变量只能在声明它们的函数或代码块中访问。例如,如果一个变量在 for 循环、if 语句或一个函数体内声明,它只能在这些代码块内访问。
  • 生命周期:局部变量的生命周期与其所在的函数或代码块绑定,当函数执行完毕后,局部变量被销毁,不再占用内存。

局部变量的作用域

go
package main

import "fmt"

func main() {
    if true {
        z := 50 // z 的作用域在 if 块中
        fmt.Println("z in if:", z)
    }
    // fmt.Println("z outside if:", z) // 这里会报错,z 作用域仅限于 if 块中
}
bash
 go run local_variable_scope.go
z in if: 50
  • 变量 z 的作用域仅限于 if 代码块内,不能在 if 块外访问。

声明方式规范

变量声明有多种方式,包括使用 var 关键字进行声明(可以使用变量块)和使用简短声明 :=。选择使用哪种方式通常取决于变量的作用域、初始化需求以及代码风格。

在 Go 语言中,变量声明有多种方式,包括使用 var 关键字进行声明(可以使用变量块)和使用简短声明 :=。选择使用哪种方式通常取决于变量的作用域、初始化需求以及代码风格。

使用 var 关键字(变量块)

  1. 全局变量声明:全局变量需要用 var 声明,因为 := 只能用于函数内部的局部变量。
  2. 延迟声明(即先声明变量,然后再赋值):适用于需要先声明变量,稍后再初始化的场景。
  3. 明确指定变量类型:当需要明确类型或初始化值不易推断时,使用 var 更合适。例如:var x int
  4. 批量声明多个变量:当有多个变量需要同时声明时,使用 var 变量块更为清晰。
go
// 全局变量声明
var (
    globalVar1 int
    globalVar2 string = "Go"
    globalVar3 float64
)

var x int      // 延迟赋值
var y = 10     // 带有初始化值
var z float64  // 延迟赋值

使用简短声明 :=

  1. 函数内部的局部变量:简短声明只能在函数内部使用,通常用于局部变量的初始化。
  2. 初始化并赋值时:当需要在同一行初始化变量并赋值时,:= 是最简洁的方式。
  3. 类型自动推断:当不想显式指定类型时,:= 能根据赋值的类型自动推断变量类型。
  4. 简化代码:使用 := 可以减少冗余代码,增加可读性和编写速度。
go
func main() {
    a := 10         // 简短声明方式,变量类型自动推断为 int
    b := "Hello"    // 自动推断为 string
    c, d := 30, 40  // 同时声明多个变量
}

如何选择

  1. 全局 vs 局部

    • 如果是全局变量,只能使用 var
    • 如果是局部变量,优先考虑 := 来简化代码。
  2. 是否需要延迟声明

    • 如果需要先声明后赋值,使用 var
    • 如果直接初始化,优先使用 :=
  3. 批量声明变量

    • 当有多个变量需要同时声明时,使用 var 变量块更直观、更易维护。
  4. 代码风格

    • 简短声明 := 可以让代码更简洁,而 var 块更适合大规模的变量管理和类型明确的场景。
go
package main

import "fmt"

// 使用 var 声明全局变量
var (
    globalX int
    globalY string = "Global"
)

func main() {
    // 使用 var 声明局部变量
    var a int = 10
    var b string
    b = "Hello"

    // 使用 := 声明并初始化局部变量
    c := 30
    d, e := "Go", true

    fmt.Println(a, b, c, d, e)
}

总结

  • 全局变量:使用 var
  • 局部变量:优先使用 :=,除非有明确的类型声明或延迟赋值的需求。
  • 批量声明:使用 var 变量块更适合。

选择时根据代码上下文和维护便利性来判断,通常在函数内部尽量使用 := 来保持代码简洁,而 var 更适合全局变量和复杂的声明场景。