变量和常量是编程中必不可少的部分,也是比较好理解的一部分。

标识符#

编程语言中标识符指:由程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等。 Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头且区分大小写。 比如:abc, _123, a123

关键字#

关键字指:编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。Go 语言中类似 C# 的关键字有25个,只能在特定语法结构中使用:

break      default        func     interface    select
case       defer          go       map          struct
chan       else           goto     package      switch
const      fallthrough    if       range        type
continue   for            import   return       var

除此之外Go语言中还有30多个预定义的名字,比如 int 和 ture 等,主要对应内建的常量、类型和函数:

内建常量: true false iota nil
内建类型: int   int8   int16   int32  int64   uint   uint8  uint16   uint32   uint64   uintptr  
         float32   float64   complex128   complex64   bool   byte    rune   string  error
内建函数: make  len  cap   new   append   copy   close   delete   complex   real    imag    panic  recover

通常在 Go 语言编程中推荐的命名方式是驼峰命名。如:ReadAll,不推荐下划线命名。

变量#

变量声明#

变量声明语法如下:

var 变量名字 类型 = 表达式
	// 指定变量类型但不赋值,使用默认值
	var v1 int      // 省略表达式,使用数值类型对应的零值初始化(0)
	var v2 string   // 省略表达式,使用字符串对应的零值初始化(空字符串)
	var v3 [10]int  // 会创建长度为10的数据
	var v4 []int    // 数组切片
	var v5 struct { // {0}
		f int
	}
	var v6 *int            // 指针,因未被初始化所以默认值为nil
	var v7 map[string]int  // map,key为string类型,value为int类型,默认值为map[]
	var v8 func(a int) int // 函数类型,因未被初始化所以默认值为nil,直接调用会抛出空指针异常
	fmt.Println(v1, v2, v3, v4, v5, v6, v7, v8)

注意

  1. Go 语言中变量声明采用类型后置方式,需要先声明变量名称再声明变量类型
  2. 语句最后不需要加;(如果需要一行写多条语句需要,但不推荐这么干)
  3. 如果定义的是局部变量,则必须使用,否则编译报错

批量声明#

	// 多变量声明使用此方式避免重复写var
	var (
		v1 int
		v2 string
	)

或者这样:

	var n1, n2, n3 string
	n1, n2, n3 = "1", "2", "3"

再或者这样:

	var n4, n5, n6 = "1", "2", "3"

变量初始化#

通常情况下 类型 = 表达式 两个部分可以省略其中的一个。如果省略的是类型信息,将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。

  • 数值类型变量对应的零值是 0
  • 布尔类型变量对应的零值是 false
  • 字符串类型对应的零值是 空字符串
  • 接口或引用类型(包括slice、map、chan和函数)变量对应的零值是 nil
  • 数组或结构体等聚合类型对应的零值是每个元素或字段对应类型的零值

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,以保证不存在未初始化的变量。

1:指定类型但不赋值#

指定类型但不赋值,将使用类型的默认零值。

var name string // 省略表达式,使用字符串对应的零值初始化(空字符串)
var age int     // 省略表达式,使用数值类型对应的零值初始化(0)
fmt.Println(name, age)

2:使用类型推断#

var address = "beijing"
fmt.Println(address)

3:简短声明#

简短声明,以名字 := 表达式 形式声明变量,变量的类型根据表达式来自动推导。

sex := "男"
fmt.Println(sex)

4:使用 new 函数#

调用内建的 new 函数创建变量。表达式 new(T)将创建一个 T 类型的匿名变量,初始化为 T 类型的零值,然后返回变量地址,返回的指针类型为*T

p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

new 函数创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,还可以在表达式中使用 new(T)。其实这就是一种语法糖,而不是一个新的基础概念。new 函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活。由于 new 只是一个预定义的函数,它并不是一个关键字,因此可以将 new 名字重新定义为别的类型。例如下面的例子:

func delta(old, new int) int { return new - old }

由于 new 被定义为 int 类型的变量名,因此在 delta 函数内部无法使用内置的 new 函数。

全局变量#

全局声明变量,如果没有使用不会编译报错。

package main

var (
	author  string
	address string
)

func main() {
}

匿名变量#

在使用多重赋值时,如果想要忽略某个值,可以使用 匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,比如:

// 该函数返回两个值
func getName() (firstName, lastName string) {
	return "wangpengliang", "lizimeng"
}

func main() {
	// _ 代表匿名变量,匿名变量将会被忽略
	result, _ := getName()
	fmt.Println(result)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量)

注意:

  1. 函数外的每个语句都必须以关键字开始(var、const、func等)
  2. := 不能使用在函数外
  3. _ 多用于占位,表示忽略值

变量生命周期#

变量的生命周期指的是在程序运行期间变量有效存在的时间段。

  • 对于在包一级声明的变量来说,它们生命周期和整个程序的运行周期一致
  • 在局部变量的声明周期是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收
  • 函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建

这里需要注意一个问题,看以下代码:

var global *int

func f() {
    var x int
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}

f() 里的 x 变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。用Go语言的术语说,这个 x 局部变量从函数 f 中逃逸了。相反,当 g 函数返回时,变量 *y 将是不可达的,也就是说可以马上被回收的。因此:*y 并没有从函数 g 中逃逸,虽然这里用的是 new 方式。编译器会自动选择在栈上还是在堆上分配局部变量的存储空间 而不是用 var 还是 new 声明变量的方式区分。

变量赋值#

使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。

x = 1                       // 命名变量的赋值
*p = true                   // 通过指针间接赋值
person.name = "bob"         // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值

特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为:

count[x] *= scale

这样可以省去对变量表达式的重复计算。数值变量也可以支持 ++ 递增和 -- 递减语句

v := 1
v++    // 等价方式 v = v + 1;v 变成 2
v--    // 等价方式 v = v - 1;v 变成 1

除此之外Go语言还支持多重赋值,在不支持多重赋值的语言中,交换两个变量的内容需要引入一个中间变量,但是Go语言中不需要,比如:

var i int = 10
var j int = 20
i, j = j, i

包和文件#

Go 语言中的包与C#中的 namespace 的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。每个Go文件都只能属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。

包初始化顺序#

如果包中含有多个 .go 源文件,将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将 .go 文件根据文件名排序,然后依次调用编译器编译。

包级别变量处理#

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,如果存在没有初始化表达式的,可以用一个特殊的 init 初始化函数来简化初始化工作。每个文件可以包含多个 init 初始化函数。

func init() { }

init 初始化函数除了不能被调用或引用外,其他行为和普通函数类似。每个文件中的 init 初始化函数,在程序开始执行时按照声明的顺序被自动调用。 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果 a 包 导入了 b 包,那么在 a 包初始化的时候可以认为 b 包必然已经初始化过了。初始化工作是自下而上进行的, main 包最后被初始化。这种方式可以确保在 main 函数执行之前所有依赖的包都已经完成初始化。

导出包#

Go 语言中根据首字母的大小写来确定可以访问的权限。如果首字母大写则可以被其他的包访问;如果首字母小写,则只能在本包中使用。该规则适用于全局变量、全局常量、类型、结构字段、函数、方法等。可以简单的理解成:首字母大写是公有的,首字母小写是私有的。只能访问包导出的名字,未导出的名字不能被包外的代码访问。

导入包#

使用包成员需要使用 import 关键字导入,但不能形成导入循环。

导入系统包:

import "fmt" 

相对路径导入包:导入同一目录下 test 包

import "./test" 

绝对路径导入包:导入 gopath/src/oldboy/python

import "oldboy/python"

导入包并启用别名:导入fmt并启用别名 f2

import f2 "fmt"

fmt 启用别名.可以直接使用内容而不用再添加 fmt 。比如 fmt.Println 可以直接写成 Println

import . "fmt" 

import _

import  _ "fmt"  

这表示只使用该包的 init 函数,并不使用该包的其他内容。这种形式的 import ,在 import时就执行了 fmt 包中的 init 函数。

注意:未使用的导入包,会被编译器视为错误 (不包括 "import _")。实例如下:

package main

import (
    "fmt"
)

func main() {
}

编译错误:

./main.go:4:2: imported and not used: "fmt"

作用域#

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。

作用域和生命周期不能混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如 intlentrue 等是在全局作用域的,因此可以在整个程序中直接使用。任何在函数外部(也就是包级语法域)声明的名字可以在同一个包的任何源文件中访问的。对于导入的包,例如 tempconv 导入的 fmt 包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的 fmt 包,当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句,比如 tempconv.CToF 函数中的变量 c,则是局部作用域的,它只能在函数内部(甚至只能是局部的某些部分)访问。

常量#

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:booleanstring或数字。Go语言中常量指:编译期间就已知且不可改变的值。使用 const 关键字定义常量。

	const PI float64 = 3.14159265358979323846

	// 显式类型定义,常量名称推荐全大写
	const LENGTH int = 10

	// 隐式类型定义,其实也是使用类型推导
	const WIDTH = 20

	// 多重定义
	const (
		size int64 = 1024
		eof        = -1 // 无类型整型常量
	)
	// 或者这样
	const a, b, c = 1, false, "hello world" // 常量的多重赋值

iota#

常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个 const 声明语句中,在第一个声明的常量所在行的 iota 将会被置为 0 ,然后在每一个有常量声明的行加 1。(Go语言没有枚举类型,可以使用 const+iota 模拟)

关键字 iota 定义常量组中从0开始按行计数的自增枚举值。iotaconst 关键字出现时将被重置为0 (const内部的第一行之前),const中每新增一行常量声明将使 iota 计数一次。

普通常量组:如果不指定类型和初始化值,则与上一行非空常量右值相同

const (
   Windows = 0
   Linux
   MaxOS
)
fmt.Println(Windows, Linux, MaxOS) // output: 0 0 0

结合 iota 实现枚举:第一个 iota 等于0,每当 iota 在新的一行被使用,它的值自增1

const (
   Sunday    = iota // 0
   Monday           // 1,通常省略后续行行表达式。
   Tuesday          // 2
   Wednesday        // 3
   Thursday         // 4
   Friday           // 5
   Saturday         // 6
)
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)

iota 插队:

const (
   a1 = iota    // 0
   a2           // 1
   b1 = "hello" // 独立值hello,iota+=1
   b2           // 如不指定类型和初始化值,则与上一行非空常量右值相同,所以是hello;iota+=1
   c1 = iota    // 恢复自增,4
   c2           // 5
)
fmt.Println(a1, a2, b1, b2, c1, c2) // output:0 1 hello hello 4 5

iotl 忽略:使用 _ 忽略某些值

	const (
		d1 = iota
		d2
		_
		d3
	)
	fmt.Println(d1, d2, d3) // output:0 1 3

iota 定义数量级

	const (
		_  = iota             // _表示将0忽略
		KB = 1 << (10 * iota) // 表示1左移十位,转换为十进制就是1024
		MB = 1 << (10 * iota)
		GB = 1 << (10 * iota)
		TB = 1 << (10 * iota)
		PB = 1 << (10 * iota)
	)

同一常量组中,可以提供多个 iota,它们各自增⻓。

	const (
		a, b = iota + 1, iota + 2 // a=iota+1 b=iota+2 => 1,2
		c, d                      // c=iota(1)+1 b=iota(2)+1 => 2,3
	)
	fmt.Println(a, b, c, d)