Golang | 基础 - 3. 变量
3.1 变量
变量是几乎所有编程语言中最基本的组成元素。从根本上说,变量相当于是对一块数据存储空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后可以通过引用变量名来使用这块存储空间。
Go语言中的变量使用方式与C语言接近,但具备更大的灵活性。
3.1.1 变量的声明
Go语言的变量声明方式与C和C++语言有明显的不同。对于纯粹的变量声明,Go语言引入了关键字 var
,而类型信息放在变量名之后,示例如下:
var v1 int
var v2 string
var v3 [10]int // 数组
var v4 []int // 数组切片
var v5 struct {
f int
}
var v6 *int // 指针
var v7 map[string]int // map,key为string类型,value为int类型
var v8 func(a int) int
变量声明语句不需要使用分号作为结束符。与C语言相比,Go语言摒弃了语句必须以分号作为语句结束标记的习惯。
var
关键字的另一种用法是可以将若干个需要声明的变量放置在在一起,免得程序员需要重复写var
关键字,如下所示:
var (
v1 int
v2 string
)
3.1.2 变量初始化
对于声明变量时需要进行初始化的场景, var
关键字可以保留,但不再是必要的元素,如下所示:
var v1 int = 10 // 正确的使用方式1
var v2 = 10 // 正确的使用方式2,编译器可以自动推导出v2的类型
v3 := 10 // 正确的使用方式3,编译器可以自动推导出v3的类型
以上三种用法的效果是完全一样的。与第一种用法相比,第三种用法需要输入的字符数大大减少,是懒程序员和聪明程序员的最佳选择。这里Go语言也引入了另一个C和C++中没有的符号(冒号和等号的组合 := ),用于明确表达同时进行变量声明和初始化的工作。
指定类型已不再是必需的,Go编译器可以从初始化表达式的右值推导出该变量应该声明为哪种类型,这让Go语言看起来有点像动态类型语言,尽管Go语言实际上是不折不扣的强类型语言(静态类型语言)。
当然,出现在 :=
左侧的变量不应该是已经被声明过的,否则会导致编译错误,比如下面这个写法:
var i int
i := 2
会导致类似如下的编译错误:
no new variables on left side of :=
3.1.3 变量赋值
在Go语法中,变量初始化和变量赋值是两个不同的概念。下面为声明一个变量之后的赋值过程:
var v10 int
v10 = 123
Go语言的变量赋值与多数语言一致,但Go语言中提供了C/C++程序员期盼多年的多重赋值功能,比如下面这个交换i
和j
变量的语句:
i, j = j, i
在不支持多重赋值的语言中,交互两个变量的内容需要引入一个中间变量:
t = i;
i = j;
j = t;
多重赋值的特性在Go语言库的实现中也被使用得相当充分,在介绍函数的多重返回值时,将对其进行更加深入的介绍。总而言之,多重赋值功能让Go语言与C/C++语言相比可以非常明显地减少代码行数。
3.1.4 匿名变量
我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没用的变量。在Go中这种情况可以通过结合使用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。
假设 GetName()
函数的定义如下,它返回3个值,分别为firstName
, lastName
和nickName
:
func GetName() (firstName, lastName, nickName string) {
return "May", "Chan", "Chibi Maruko"
}
若只想获得 nickName
,则函数调用语句可以用如下方式编写:
_, _, nickName := GetName()
这种用法可以让代码非常清晰,基本上屏蔽掉了可能混淆代码阅读者视线的内容,从而大幅降低沟通的复杂度和代码维护的难度。
3.2 常量
在Go语言中,常量是指编译期间就已知且不可改变的值。常量可以是数值类型(包括整型、
浮点型和复数类型)、布尔类型、字符串类型等。
3.2.1 字面常量
所谓字面常量(literal),是指程序中硬编码的常量,如:
-12
3.14159265358979323846 // 浮点类型的常量
3.2+12i // 复数类型的常量
true // 布尔类型的常量
"foo" // 字符串常量
在其他语言中,常量通常有特定的类型,比如 -12
在C语言中会认为是一个 int
类型的常量。如果要指定一个值为 -12
的 long
类型常量,需要写成 -12l
,这有点违反人们的直观感觉。Go语言的字面常量更接近我们自然语言中的常量概念,它是无类型的。只要这个常量在相应类型的值域范围内,就可以作为该类型的常量,比如上面的常量 -12
,它可以赋值给 int
、 uint
、 int32
、int64
、 float32
、 float64
、 complex64
、 complex128
等类型的变量。
3.2.2 常量定义
通过 const
关键字,你可以给字面常量指定一个友好的名字:
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // 无类型浮点常量
const (
size int64 = 1024
eof = -1 // 无类型整型常量
)
const u, v float32 = 0, 3 // u = 0.0, v = 3.0,常量的多重赋值
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 无类型整型和字符串常量
Go的常量定义可以限定常量类型,但不是必需的。如果定义常量时没有指定类型,那么它与字面常量一样,是无类型常量。
常量定义的右值也可以是一个在编译期运算的常量表达式,比如
const mask = 1 << 3
由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式,比如试图以如下方式定义常量就会导致编译错误:
const Home = os.GetEnv("HOME")
原因很简单, os.GetEnv()
只有在运行期才能知道返回结果,在编译期并不能确定,所以
无法作为常量定义的右值。
3.2.3 预定义常量
Go语言预定义了这些常量: true
、 false
和 iota
。
iota
比较特殊,可以被认为是一个可被编译器修改的常量,在每一个 const
关键字出现时被重置为0
,然后在下一个 const
出现之前,每出现一次 iota
,其所代表的数字会自动增1
。
从以下的例子可以基本理解 iota
的用法:
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
const (
a = 1 << iota // a == 1 (iota在每个const开头被重设为0)
b = 1 << iota // b == 2
c = 1 << iota // c == 4
)
const (
u = iota * 42 // u == 0
v float64 = iota * 42 // v == 42.0
w = iota * 42 // w == 84
)
const x = iota // x == 0 (因为iota又被重设为0了)
const y = iota // y == 0 (同上)
如果两个 const
的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。因此,上面的前两个 const
语句可简写为:
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 // c1 == 1
c2 // c2 == 2
)
const (
a = 1 <<iota // a == 1 (iota在每个const开头被重设为0)
b // b == 2
c // c == 4
)
3.2.3.1 iota
iota
是 golang
语言的常量计数器,只能在常量的表达式中使用。
使用 iota
能简化定义,在定义枚举时很有用。
举例:
3.2.3.1.1 iota只能在常量的表达式中使用
fmt.Println(iota)
编译错误: undefined: iota
3.2.3.1.2 每次 const 出现时,都会让 iota 初始化为0
const a = iota // a=0
const (
b = iota //b=0
c //c=1
)
3.2.3.1.3 自定义类型
自增长常量经常包含一个自定义枚举类型,允许你依靠编译器完成自增设置。
type Stereotype intconst (
TypicalNoob Stereotype = iota // 0
TypicalHipster // 1
TypicalUnixWizard // 2
TypicalStartupFounder // 3
)
3.2.3.1.4 可跳过的值
设想你在处理消费者的音频输出。音频可能无论什么都没有任何输出,或者它可能是单声道,立体声,或是环绕立体声的。
这可能有些潜在的逻辑定义没有任何输出为 0,单声道为 1,立体声为 2,值是由通道的数量提供。
所以你给 Dolby 5.1 环绕立体声什么值。
一方面,它有6个通道输出,但是另一方面,仅仅 5 个通道是全带宽通道(因此 5.1 称号 - 其中 .1 表示的是低频效果通道)。
不管怎样,我们不想简单的增加到 3。
我们可以使用下划线跳过不想要的值。
type AudioOutput intconst (
OutMute AudioOutput = iota // 0
OutMono // 1
OutStereo // 2
_
_
OutSurround // 5
)
3.2.3.1.5 位掩码表达式
type Allergen int
const (
IgEggs Allergen = 1 << iota // 1 << 0 which is 00000001
IgChocolate // 1 << 1 which is 00000010
IgNuts // 1 << 2 which is 00000100
IgStrawberries // 1 << 3 which is 00001000
IgShellfish // 1 << 4 which is 00010000
)
这个工作是因为当你在一个 const
组中仅仅有一个标示符在一行的时候,它将使用增长的 iota
取得前面的表达式并且再运用它。
在 Go 语言的 spec
中, 这就是所谓的隐性重复最后一个非空的表达式列表。如果你对鸡蛋,巧克力和海鲜过敏,把这些 bits
翻转到 “on” 的位置(从左到右映射 bits)。然后你将得到一个 bit 值 00010011
,它对应十进制的 19
。
fmt.Println(IgEggs | IgChocolate | IgShellfish)
// output:
// 19
3.2.3.1.6 定义数量级
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
PB // 1 << (10*5)
EB // 1 << (10*6)
ZB // 1 << (10*7)
YB // 1 << (10*8)
)
3.2.3.1.7 中间插队
const (
i = iota
j = 3.14
k = iota
l
)
那么打印出来的结果是 i=0,j=3.14,k=2,l=3
3.2.4 枚举
枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们看到可以用在 const 后跟一对圆括号的方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值。Go语言并不支持众多其他语言明确支持的 enum 关键字。
下面是一个常规的枚举表示法,其中定义了一系列整型常量:
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays // 这个常量没有导出
)
同Go语言的其他符号(symbol)一样,以大写字母开头的常量在包外可见。
以上例子中 numberOfDays
为包内私有,其他符号则可被其他包访问。