变量是存储数据的抽象概念。变量可以理解为执行环境中的数据存储区域,其内部存储了值,并可以根据变量的类型对值进行解析。
在《如何学习Go语言》中,我们介绍了如何从抽象的角度,将变量理解为不同大小和不同类型的瓶子来理解变量的定义。
可以看到变量主要包含了两个重要的方面:
- 变量大小 为变量分配的大小
- 变量类型 如何对变量中的值进行解析,即便变量中存储了相同的值,可能解析出不同的结果
在Go语言中,对于拥有内置数据类型的变量,在编译时即固定了分配的类型和相应的大小。例如int64代表存储的数据是整数,并且拥有64个字节的大小。像字符串这样的类型,以及切片、哈希表这样的类型要相对特殊一些。
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string
不管变量类型如何特殊,一旦在编译时定义好了之后,就无法变化。例如我们可以将之前的HelloWorld程序改写如下,其中s为声明的变量名,string为变量的类型,"Hello World"为初始化赋值到变量中的字符串值。
func main() {
var s string = "Hello World"
fmt.Println(s)
}
如果如下所示,我们尝试将变量s 赋值为整数,则直接在编译时报错。
func main() {
var s string = "Hello World"
s = 123
fmt.Println(s)
}
报错为:
# command-line-arguments
./variables.go:8:4: cannot use 123 (type untyped int) as type string in assignment
这种在确定了变量的类型之后,无法轻易修改变量类型的特性,叫做强类型。强类型的语言包括CC++、java、Go。 而动态类型的语言包括js、Ruby、python。 例如在js中,如下的语法是正常的:
var number = "string"
number = 1123
弱类型的语言虽然带来了灵活性,但是常常会实行隐式转换,或者产生难以意料的结果。例如js中,下例中产生的结果为字符串'12',但是在强类型语言中直接会报错。
number = 1 + "2"
声明变量时需要定义一个名字,例如上例中的变量名s。变量名可以引用任何复杂的值、表达式和函数。一个短小和有意义的名字可以增加程序的可读性,并且是增加抽象,构建复杂程序的基础。
第一种声明变量的方式是通过 var
指定变量名、类型、并分配初始值。其一般形式如下:
var name type = expression
当不需要赋初始值时,= expression
可以被忽略。但是变量会被赋一个初始值。例如对于bool类型初始值为false、strings类型初始值为"", 接口和初始值类型为nil。如下所示,打印出空字符串。
var s string
fmt.Println(s) // ""
当 type
类型被忽略时,变量的类型取决于其赋值的表达式的类型。通过自动类型推断(参考《Go语言底层原理剖析》)的方式,为变量赋值。number 的类型为常量123的类型 int。
var number = 123
fmt.Println(number)
可以在单个声明语句中声明一系列变量。
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
初始化变量的值可以是整数、字符串,甚至是任何表达式,例如下例中,变量根据函数的返回值进行初始化。
var f, err = os.Open(name) // os.Open 返回文件结构体 和错误。
在函数中,有一种更简洁的变量声明方式,其形式为name := expression
,变量name的类型由表达式确定,仍然是一种编译时类型自动推断的方式。如下所示:
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
可以通过如下方式打印出变量的类型:
a := 10
b := "zjx"
c := 3.1415926
d := true
fmt.Printf("%T\n", a) // int
fmt.Printf("%T\n", b) // string
fmt.Printf("%T\n", c) // float64
fmt.Printf("%T\n", d) // bool
由于:=
声明方式的灵活性和简洁性,因此使用的最多。var
声明常常用在需要强制声明变量类型,或者一开始并没有为变量赋初始化的情况。
i := 100 // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
声明只能发生一次。下例是一种特殊情况,第一个语句初始化了变量in和err,第二个语句只初始化了变量out。 而err是对变量的赋值
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
和大多数语言一样,Go变量名以数字、字母或下划线组成。Go对字母和数字的定义比许多语言都要宽泛一些。任何被视为字母或数字的 Unicode 字符都是允许的。下例中的许多特殊变量名都是有效的。
_0 := 0_0
_𝟙 := 20
π := 3
a := "hello" // Unicode U+FF41
fmt.Println(_0)
fmt.Println(_𝟙)
fmt.Println(π)
fmt.Println(a)
虽然上面的代码有效,但不建议这样命名,这些特殊字符令人困惑且难以键入。有些字符即使它们看起来是同一个字符,它们也代表完全不同的变量
func main() {
a := "hello" // Unicode U+FF41
a := "goodbye" // Unicode U+0061
fmt.Println(a)
fmt.Println(a)
}
尽管下划线是变量名中的有效字符,但它很少使用,因为惯用的 Go 不使用蛇形大小写(例如 index_counter 或 number_tries 之类的名称)。相反,当标识符名称由多个单词组成时,Go 使用驼峰式大小写(名称如 indexCounter 或 numberTries 在函数中,倾向于使用短变量名。变量的范围越小,用于它的名称就越短。在 Go 中看到单字母变量名是很常见的。例如,名称 k 和 v(键和值的缩写)用作 for-range 循环中的变量名称。如果您使用标准 for 循环,则 i 和 j 是索引变量的常用名称。 对短变量名,存在围绕变量类型和单字母名称的约定。人们将使用类型的第一个字母作为变量名(例如,i 表示整数,f 表示浮点数,b 表示布尔值) 而对于包级别的变量,则建议使用更具描述性和意义的名称。
变量的生命周期是程序执行时它何时存在。包级变量的生命周期是程序的整个执行过程。相比之下,局部变量具有动态生命周期:每次执行声明语句时都会创建一个新实例,并且变量会一直存在,直到它变得无法访问,此时它的存储可能会被回收。函数参数和结果也是局部变量;每次调用它们的封闭函数时都会创建它们。 例如对于下面的程序,每次 for 循环开始时都会创建变量 t,并在循环的每次迭代中创建新变量 x 和 y。
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
上面循环中创建的局部变量t、x、y 在循环结束后即不可用。而对于函数的入参或返回值,其是否存在取决于是否变成无法访问。 对于无法被访问的变量,其本质上已经无用了,最终会被运行时垃圾回收程序自动清理。
可以引用变量的有效位置称为变量的作用域。如下例所示,在函数f1中声明的变量x只在函数f1中有效,不能够被函数f2调用。
func f1() {
var x input
fmt.Println(x)
}
func f2() {
fmt.Println(x)
}
Go 的词法范围使用花括号{...}作为分割。根据作用域的范围大小,可以将全局作用域,包作用域、文件作用域、函数作用域、Go方法定义了各种情况下的作用域范围
- 预声明标识符的范围是全局作用域 预声明的标识符。所有Go文件中都可使用
Types:
bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr
Constants:
true false iota
Zero value:
nil
Functions:
append cap close complex copy delete imag len make new panic print println real recover
- 在顶层(任何函数之外)声明的常量、类型、变量或函数的标识符的范围是包级别作用域 如下例main包中,f2.go文件中的函数f可以引用f1.go函数中定义的变量x
// f1.go
package main
var x int = 9
// f2.go
package main
import "fmt"
func f() {
fmt.Println(x)
}
func main(){
f()
}
- 导入包的包名范围是文件作用域 下面的代码无效,因为import 是file block 。不能跨文件
// f1.go
package main
import "fmt"
// f2.go 无效
package main
func f() {
fmt.Println("Hello World")
}
- 函数参数或返回值的标识符的范围是函数作用域 //函数体内部的变量是function block,注意前后顺序,同时不能跨函数使用。
func main() {
fmt.Println("Hello World")
x := 5
fmt.Println(x)
}
下面的代码无效:
func main() {
fmt.Println("Hello World")
fmt.Println(x)
x := 5
}
下面的代码无效2:
func main() {
x := 5
}
func test(){
fmt.Println(x)
}
当存在文件级别的变量名与函数级别的变量名同名时,遵循就近原则 var x int=5
func main(){
var x int = 99;
x = 100;
fmt.Println("testx",x) //100
}
- 在函数内声明的常量、变量、类型标识符的范围从标识符声明开始,在包含标识符最内部的花括号结束
//在花括号中声明的变量只在花括号中有效。
func main() {
fmt.Println("Hello World") // x is out of scope
{ // x is out of scope
x := 5 // x is in scope
fmt.Println(x) // x is in scope
} // x is out of scope again
}
/*
下面代码无效:
func main() {
{
x := 5
}
fmt.Println(x)
}
可以使用shadow工具进行探测,安装shadow工具的方式如下:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
shadow xxx.go
运行后,如果有阴影变量能够看到提示:
declaration of "x" shadows declaration at line 6
-
《Learning Go》 Chapter 2. Primitive Types and Declarations
-
《Learning Go》 Chapter 4. Blocks, Shadows, and Control Structures
-
《The Go Programming Language》2.3 Variables
-
《程序员的自我修养》3.5 链接的接口————符号
-
《Structure and Interpretation of Computer Programs》 3.2 The Environment Model of Evaluation sicp:在存在赋值的情况下,不能再将变量视为仅仅是值的名称,例如对于同一个函数的多次调用可能会导致变量结果不同。这种设定带来了复杂性,并导致需要有新的模型,引入了变量所处"环境"这一概念