- Go 入门指南: Go 程序的基本结构和要素
常量
常量使用关键字 const
定义,用于存储不会改变的数据。存储在常量中的数据类型只可以是 布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式为: const identifier [type] = value
,例如:
1 | const Pi = 3.14159 |
在 go 语言中,你可以省略类型说明符 [type]
,因为编译器可以根据常量的值来推断其类型。
- 显式类型定义:
const b string = "abc"
- 隐式类型定义:
const b = "abc"
一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时根据上下文来获得相关类型
1 | var n int |
常量的值必须是能够在编译时就能够确定的;你可以在起赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得:
- 正确的做法:
const c1 = 2/3
- 错误的做法:
const c2 = getNumber()
// 引发构建错误:getNumber() used as value
因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:
len()
数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出,如:
1 | const Ln2= 0.693147180559945309417232121458\ |
根据上面的例子我们可以看到,反斜杠 \
可以在常量表达式中作为多行的连接符使用。
与各种类型的数字型变量相比,无须担心常量之间的类型转换问题,因为它们都是非常理想的数字。不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。另外,常量也允许使用并行赋值的形式
1 | const beef, two, c = "eat", 2, "veg" |
常量还可以用作枚举
1 | const ( |
现在,数字 0、1 和 2 分别代表未知性别、女性和男性。这些枚举值可以用于测试某个变量或常量的实际值,比如使用 switch/case
结构。
在这个例子中,iota 可以被用作枚举值:
1 | const ( |
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
1 | const ( |
iota
也可以用在表达式中,如:iota + 50
。在每遇到一个新的常量块或单个常量声明时,iota
都会重置为 0( 简单地讲,每遇到一次const
关键字,iota
就重置为 0 )。
当然,常量之所以为常量就是恒定不变的量,因此我们无法在程序运行过程中修改它的值;如果你在代码中试图修改常量的值则会引发编译错误。
引用 time
包中的一段代码作为示例:一周中每天的名称。
1 | const ( |
你也可以使用某个类型作为枚举常量的类型:
1 | type Color int |
变量
简介
声明变量的一般形式是使用 var
关键字: var identifier type
。
需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。Go 为什么要选择这么做呢?首先,它是为了避免像 C 语言中那样含糊不清的声明形式。例如: int* a, b
。在这个例子中,只有 a 是指针,而 b 不是。如果你想要这两个变量都是指针,则需要将他们分开书写。
而在 Go 中,则可以很轻松的将它们都声明为指针类型:
1 | var a,b *int |
其次,这种语法能够按照从左至右的顺序阅读,使得代码更加容易理解,示例:
1 | var a int |
你也可以改写成这种形式
1 | var ( |
这种因式分解关键字的写法一般用于声明全局变量。
当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为 空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。
变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如: numShips
和 startDate
。但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写。
一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。像 if 和 for 这些控制结构,而在这些结构中声明的变量的作用域只在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。
尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。
变量可以编译期间就被赋值,赋值给变量使用运算符等号 =
,当然你也可以在运行时对变量进行赋值操作。如下
1 | a = 15 |
一般情况下,当变量 a 和变量 b 之间类型相同时,才能进行如 a = b
的赋值。
声明与赋值(初始化)语句也可以组合起来。如
1 | var identifier [type] = value |
但是 Go 编译器可以根据变量的值来自动推断其类型,这有点像 Ruby 和 Python 这类动态语言,只不过它们是在运行时进行推断,而 Go 是在编译时就已经完成推断过程。因此,你还可以使用下面的这些形式来声明及初始化变量:
1 | var a = 15 |
或
1 | var ( |
不过自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型,例如:
1 | var n int64 = 2 |
然而,var a
这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据。变量的类型也可以在运行时实现自动推断,例如:
1 | var ( |
这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 :=
,例如:
1 | a := 1 |
下面这个例子展示了如何通过 runtime 包在运行时获取所在的操作系统类型,以及如何通过 os 包中的函数 os.Getenv()
来获取环境变量中的值,并保存到 string 类型的局部变量 path 中。
1 | package main |
如果你在 Windows 下运行这段代码,则会输出 The operating system is: windows 以及相应的环境变量的值;如果你在 Linux 下运行这段代码,则会输出 The operating system is: linux 以及相应的的环境变量的值。
值类型和引用类型
程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称之为 “字”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4字节)或 64 位(8字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。
所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值;另外,像数组和结构这些复合类型也是值类型。
当使用 =
将一个变量的值赋给另一个变量时,如:j = i
,实际上是在内存中将 i 的值进行了拷贝。
可以通过 &i
来获取变量 i 的内存地址,例如: 0xf840000040
(每次的地址都可能不一样)。值类型的变量的值存储在栈中。
内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
这个内存地址被称之为指针,这个指针实际上也被存在另外的某一个字中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 r2 = r1
时,只有引用(地址)被复制。如果 r1 的值被改变了,那么这个值的所有引用都会指向被需修改后的内容,在这个例子中,r2 也会受影响。
在 Go 语言中,指针属于引用类型,其它的引用类型还包括 slices,maps 和 channel。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
打印
函数 Printf
可以在 fmt 包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用的格式化字符串作为第一个参数:
1 | func Printf(format string, list of variables to be printed) |
在示例 4.5 中,格式化字符串为:"The operating system is: %s\n"
。
这个格式化字符串可以含有一个或多个的格式化标识符,例如:%..
,其中 ..
可以被不同类型所对应的标识符替换,如 %s
代表字符串标识符、%v
代表使用类型的默认输出格式的标识符。这些标识符所对应的值从格式化字符串后的第一个逗号开始按照相同顺序添加,如果参数超过 1 个则同样需要使用逗号分隔。使用这些占位符可以很好地控制格式化输出的文本。
函数 fmt.Sprintf
与 Printf
的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者,因此你可以在程序中使用包含变量的字符串,具体例子可以参见示例 15.4 simple_tcp_server.go。
函数 fmt.Print 和 fmt.Println 会自动使用格式化标识符 %v 对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。例如:
1 | fmt.Print("Hello:", 23) |
简短形式,使用 :=
赋值操作符
我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,而这个时候再在一个声明语句写上 var 关键字就显得有些多余了,因此我们可以将它们简写为 a := 50
或 b := false
。
a 和 b 的类型(int 和 bool)将由编译器自动推断。
这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 :=
可以高效地创建一个新的变量,称之为初始化声明。
注意事项
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20
就是不被允许的,编译器会提示错误 no new variables on left side of :=
,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。
如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a
。
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a:
1 | func main() { |
尝试编译这段代码将得到错误 a declared and not used
。此外,单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用 fmt.Println(“hello, world”, a) 会移除错误。
但是全局变量是允许声明但不使用。
其他的简短形式为:
同一类型的多个变量可以声明在同一行,如:
1
var a, b, c int
这是将类型写在标识符后面的一个重要原因
多变量可以在同一行进行赋值,如:
1
a, b, c = 5, 7, "abc"
上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用:
1
a, b, c := 5, 7, "abc"
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 “abc”。
这被称为 并行 或 同时 赋值。如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a
。
空白标识符 _
也被用于抛弃值,如值 5 在:_, b = 5, 7
中被抛弃。
_
实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val
和错误 err
是通过调用 Func1 函数同时得到:val, err = Func1(var1)
。
init 函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。如下示例
init.go
1
2
3
4
5
6
7
8
9package trans
import "math"
var Pi float64
func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}在它的 init 函数中计算变量 Pi 的初始值。
示例
user_init.go
中导入了包 trans(需要 init.go 目录为./trans/init.go)并且使用到了变量 Pi:1
2
3
4
5
6
7
8
9
10
11
12package main
import (
"fmt"
"./trans"
)
var twoPi = 2 * trans.Pi
func main() {
fmt.Printf("2*Pi = %g\n", twoPi) // 2*Pi = 6.283185307179586
}init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 backend():
1
2
3
4func init() {
// setup preparations
go backend()
}