Skip to content

Latest commit

 

History

History
310 lines (262 loc) · 10.9 KB

7.variables.md

File metadata and controls

310 lines (262 loc) · 10.9 KB

变量与类型

变量是存储数据的抽象概念。变量可以理解为执行环境中的数据存储区域,其内部存储了值,并可以根据变量的类型对值进行解析。

在《如何学习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 指定变量名、类型、并分配初始值。其一般形式如下:

 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:在存在赋值的情况下,不能再将变量视为仅仅是值的名称,例如对于同一个函数的多次调用可能会导致变量结果不同。这种设定带来了复杂性,并导致需要有新的模型,引入了变量所处"环境"这一概念

  • https://go.dev/ref/spec#Declarations_and_scope

  • https://go.dev/ref/spec#VarSpec

  • https://www.golang-book.com/books/web/01-02