Go 语言变量

基本概念

变量是存储特定类型数据的基本单位,在程序运行时分配内存(初始化),并能被修改。

初始化

初始化主要指为变量分配内存并设置初始值。通过变量初始化可以避免程序出现意外行为,优化内存使用效率。在 Go 语言中,初始化会在编译时或运行时进行:

  • 编译时初始化:编译器在编译过程中分析代码,对可在编译时确定值的变量和常量进行初始化。
  • 运行时初始化:如果变量依赖运行时信息或是复杂数据结构,如切片、映射和通道等,初始化会在程序运行时进行。

声明类型、函数和方法和声明变量不同,它们会在编译时被转换成机器指令,没有初始化过程。

初始化步骤

变量初始化可分为以下几个环节:

  • 声明变量:首先在全局或局部声明变量名称和类型。
  • 内存分配:对于需要动态分配的类型如切片、映射等分配内存,一般通过函数 make()new() 来完成。
  • 赋初值:为变量设置初始值,可以是常量或表达式。如未显式赋值,则赋予类型零值。一些复杂的结构体和对象,可能有专门构造函数来进行初始化操作。

初始化方法

new 函数用于为给定的任何类型分配内存,初始化数据为零值,返回指针:

package main

import "fmt"

func main() {
	type Data struct {
		id    int
		valid bool
	}

	nPtr := new(int)   // 分配内存并初始化为 0,类型是 *int
	tPtr := new(Data)  // 初始化值为元素类型零值 {0 false}
	sPtr := new([]int) // 初始化为 nil,类型是 *[]int,代表指向指针的指针,没有意义。

	fmt.Println(nPtr, tPtr, sPtr)
}

make 函数用于给内置引用类型(包括切片、映射和通道)初始化,返回一个立即可用的类型实例:

  • 初始化切片:可以指定切片长度和容量参数。
  • 初始化映射:可以指定映射容量参数。
  • 初始化通道:初始化通道唯一的方式,而通道只有初始化后才能使用。

映射和通道在使用前必须初始化,切片没要求,但最好都使用 make 函数来初始化:

package main

import "fmt"

func main() {
	// 使用 make 来初始化切片,等价于使用零值字面量初始化切片
	s := make([]int, 3)
	r := []int{0, 0, 0}

	// 初始化映射和通道
	m := make(map[string]int)
	c := make(chan int, 5)

	fmt.Println(s, r, m, c)
}

变量声明

声明变量使用 var 关键字,后跟变量名、变量类型和初始值,值和类型都是可选,但两者不能同时省略:

var VariableName VariableType = Expression
  • VariableName:变量名称。

  • VariableType:变量类型,可以是任何有效类型。如果省略,将由编译器自动推断。

  • Expression:变量值或赋值表达式。如果省略,变量值将初始化为其类型零值。

编译器自动推断出的默认类型有下面几种:

  • 整数值int
  • 小数值float64
  • 字符串string
  • 布尔值bool

变量被声明后会立即分配内存空间,局部变量禁止只声明不使用。此外在同一作用域内,不能重复声明变量:

package main

import "fmt"

func main() {
	// 变量声明示范
	var a float32 = 1.1 // 完整声明,用于强调类型
	var b = 1           // 省略类型,自动推断为 int
	var c bool          // 省略值,默认零值 flase
	var (               // 声明变量组
		d = "d"
		e []rune
	)

	// 错误示范
	//var f = 3  // 编译错误:定义后没用上
	//var a = 22 // 语法错误:重复定义

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

简短声明

在函数内部,可以使用 := 语法快速声明并初始化局部变量:

VariableName := Expression
  • VariableName:变量名称。

  • Expression:初始值或赋值表达式,类型由值自动推断得出。

短变量声明等价于 var VariableName = Expression,但还能用于流程控制初始化语句中:

package main

import "fmt"

// 语法错误:在函数外使用简短声明
//a := 1

func main() {
	// 简短变量声明格式只能用于函数内部
	b := 3.0            // 使用字面量赋值
	c := float32(b / 0) // 变量表达式赋值

	// 用于初始化语句中
	for i := 0; i < int(b); i++ {
		fmt.Println(i)
	}

	// 语法错误:无新变量
	//b := 2.2

	fmt.Println(b, c)         // 输出:3 +Inf
	fmt.Printf("%T %T", b, c) // 自动推断为:float64 float32
}

短变量声明依赖一个明确的作用域,通常约定在函数内部使用简短声明,在函数外使用 var 声明变量。

初始化零值

所有变量在 Go 语言中都需要初始化,当变量被声明但没显式初始化时,会被自动赋予一个类型默认值,也被称为零值。常见数据类型零值见下表,对于复合类型如数组或结构体,零值是内部所有元素的零值:

类型 零值
布尔型 false
整数型 0
浮点型 0
复数型 0+0i
字符串 ""
函数、接口、通道、映射、切片、指针 nil

要检查变量是否被赋值,可以将变量值和零值比较:

package main

import (
	"fmt"
)

func main() {
	// 函数类型变量
	var f func(string) int
	fmt.Println(f) // 输出:<nil> 表示不引用任何函数实现

	// 接口类型变量
	var i interface{ DoSomething() }
	fmt.Println(i) // 输出:<nil> 表示没有包含任何值或具体类型

	// 通道类型变量
	var ch chan int
	fmt.Println(ch) // 输出:<nil> 表示未打开,不能用于数据传递

	// 映射类型变量
	var m map[string]int
	fmt.Println(m) // 输出:map[] 表示没有被初始化,且不能直接使用

	// 切片类型变量
	var s []int
	fmt.Println(s) // 输出:[] 表示长度和容量都是 0,且没有底层数组

	// 整型指针变量
	var p *int
	fmt.Println(p) // 输出:<nil> 表示不指向任何内存位置

	// 对指针变量赋值
	v := 10
	p = &v

	// 和零值比较看是否赋值
	if p == nil {
		fmt.Println("p 未被赋值。")
	} else {
		fmt.Printf("p 被赋值,值为 %d\n", *p)
	}
}

零值为 nil 的类型作为函数参数时,可以直接传递 nil 值给函数,减少多余变量定义。同样作为函数返回值时,也能直接返回 nil 代表没有数据:

package main

import "fmt"

func nilCaller(i *int) []byte {
	if i == nil {
		return nil
	}
	return nil
}

func main() {
	// 直接传入 nil
	var f = nilCaller(nil)
	println(f) // 输出:[0/0]0x0
	if f == nil {
		fmt.Println(f) // 输出:[]
	}
}

重声明

声明变量后,相同作用域内只能修改变量值,不能重复声明变量。但有一种特例:当使用简短变量声明进行多重赋值时,如果有引入新变量,能同时对相同作用域内已声明变量进行重声明。

重声明其实是退化成赋值操作,并不是真正的变量声明,因此值要匹配变量类型。重声明常用于处理函数返回值,例如重用 err 错误变量,无需为每次函数调用创建临时变量:

package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	f, err := os.Open("config.ini")
	fmt.Println(err)

	// 为新变量 r 赋值,重用 err 变量
	r, err := http.Get("http://localhost")
	fmt.Println(err)

	// 由于出现错误,所以都为 nil
	fmt.Println(f, r)
}

特别注意重声明条件,只能作用于同个作用域内,外部作用域变量不会参与到重声明:

package main

import "fmt"

var a int

// 在初始化函数中没有触发重声明
func init() {
	// 此处 a 是新的局部变量
	a, b := 1, 2
	fmt.Println("init:", a, b)
}

func main() {
	fmt.Println("main:", a) // 输出:0
}

变量赋值

赋值运算符由等号 = 实现,用于将等号右边表达式的值赋予等号左边的变量,也叫赋值语句(Assignment Statement)。赋值方式有三种,包括简单赋值、多重赋值以及复合赋值。

简单赋值

赋值最基本形式是将一个表达式的值赋给一个变量:

VariableName = Expression
  • VariableName:变量名称。
  • Expression:计算得到值,赋给变量。字面量也算表达式,直接代表自己的值。

赋值时,左侧变量类型必须与右侧结果类型相同,或者右侧类型可以被隐式转为左侧类型:

package main

import "fmt"

func main() {
	var a, b int

	a = 10       // 将 10 赋值给 a
	b = a * 2_0. // 右侧是赋值表达式,可以使用任意二元算术运算符

	fmt.Println(b)
}

多重赋值

多重赋值也称为「并行赋值」或「元组赋值」,是一种同时对多个变量进行赋值的语法。多重赋值中,右侧所有表达式先计算完成,然后再统一赋值给左侧变量,常用于处理函数返回值:

package main

import "fmt"

// caller 返回两个整数
func caller() (int, int) {
	return 7, 13
}

func main() {
	var a, b = 5, 10   // 用于初始化变量
	a, b = caller() // 用于接受返回值
	fmt.Println(a, b)
}

多重赋值还可用于变量值交换,不需要用到中间变量:

package main

import "fmt"

func main() {
	// 直接交换变量值
	x, y := 5, 10
	x, y = y, x
	fmt.Println(x, y) // 输出:10 5

	// 右侧先计算得到 2,3,1 再赋值
	// 完全不同于 a=b;b=c;c=a
	a, b, c := 1, 2, 3
	a, b, c = b, c, a
	fmt.Println(a, b, c) // 输出:2 3 1
}

注意,只有变量类型相同才能进行值交换。

复合赋值

使用复合赋值运算符在赋值同时进行算术或位运算:

package main

import "fmt"

func main() {
	a := 10
	a *= 2 // 等同于 a = a * 2

	fmt.Println(a)
}