Go系列:Go 语法

这篇文章主要讲述 Go 语言的基本语法。

语句

Go 语言不需要在语句或声明后面使用分号结尾,除非有多个语句或声明出现在同一行,如下所示:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello world!")
}

常量

常量是一个表达式,其值在编译时确定,它本质上是基本类型:布尔型、字符串或数字。定义如下:

1
const pi = 3.14159

常量声明可以同时指定类型和值,如果没有显式指定类型,则类型根据右侧的表达式进行推断。

1
const pi float32 = 3.14159

同时声明一组常量,除了第一项之外,其它项在等号右侧的表达式都可以省略,它会复用前面一项的表达式及类型。如下例:a=1,b=1,c=2,d=2

1
2
3
4
5
6
const (
a = 1
b
c = 2
d
)

常量的声明可以使用常量生成器iota,它创建相关的一系列值,不需要逐个值写出。常量声明中,iota从0开始取值,逐项加1.

1
2
3
4
5
6
7
8
9
10
11
type Weekday int

const (
Sunday Weekday iota
Monday
Tuesday
Wednesday
Thursday
Friday
Satutday
)

上述的声明中,Satutday 的值为0, Monday 的值为 1,其它以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GO 格式化字符:
格式 描述
%v 按值的本来值输出
%+v 在 %v 基础上,对结构体字段名和值进行展开
%#v 输出 Go 语言语法格式的值
%T 输出 Go 语言语法格式的类型和值
%% 输出 % 本体
%b 整型以二进制方式显示
%o 整型以八进制方式显示
%d 整型以十进制方式显示
%x 整型以十六进制方式显示
%X 整型以十六进制、字母大写方式显示
%U Unicode 字符
%f 浮点数
%p 指针,十六进制方式显示
1
2
3
4
5
6
7
8
9
10
11
12
13

func WaitChannel(conn <-chan string) bool {
timer := time.NewTimer(3 * time.Second)

select {
case <- conn:
timer.Stop()
return true
case <- timer.C: //超时
fmt.Println("WaitChannel timeout")
return false
}
}

可见性

如果一个实体在函数中声明,它只在函数局部有效。如果声明在函数外,它将对包里面的所有源文件可见。实体第一个字母的大小写决定其可见性是否跨包。如果名称以大写字母开关,这是导出的,意味着它对包外是可见和可访问的,可以被自己包之外的其它程序所心引用,像fmt包中的Printf。

声明

4个主要的声明:变量(var),常量(const),类型(type)及函数(func).

变量声明

1
var name type = expression

类型和表达式可以省略一个,但不能全省略:
1)省略类型,它的类型将由初始化表达式确定,如 var i = 10
2)省略表达式,其初始值对应于类型的零值,数字是0,布尔是false,字符串是空串(“”),对于接口和引用类型(slice,指针,map,通道,函数)是nil. 数组或结构体这样的复杂类型,零值是其所有元素或成员的零值。

短变量声明

在函数中,可使用”短变量声明”的方式来声明和初始化局部变量,如下所示:

1
name := expression

name 的类型由 expression 的类型决定。

说明:一次声明多个变量,至少有一个是新变量。

指针

指针的值是一个变量的地址,如下所示:

1
2
3
x := 1
p := &x // 取地址
*x = 2 // 赋值

函数返回局部变量的地址是允许的,如下所示:

1
2
3
4
5
6
func f() *int {
v := 1
return &v
}

var p = f()

将指针作为参数传入函数也是合法的。

new函数

可以使用 new(T) 生成指定类型的指针变量,初始化值是其类型对应的零值。形式如下:

1
p := new(int)

创建一个int类型的指针,指针指向的地址存储了0值。

变量的生命周期

生命周期是指在程序执行过程中变量存在的时间段。包级别变量的生命周期是整个程序的执行时间。相反,局部变量有一个动态的生命周期:每次执行声明语句时创建一个新的实体,变量一直生存到它变得不可访问,这是它占用的存储空间被回收。函数的参数和返回值也是局部变量,它们在其闭包函数补调用的时候创建。
变量的生命周期是通过它的可达性来确定的,基本思路是以每一个包级别的变量(或每一个当前执行函数的局部变量),作为路径的源头,通过指针和其它方式的引用可以找到该变量,则该变量是可以访问的,不会被回收,若变量的路径不存在,则表示该变量不可访问,可以进行回收。

多重赋值

多重赋值允许几个变量一次性被赋值。在实际更新变量前,右侧所有的表达式都会提前计算,变量可以出现在赋值符两侧,如下所示:

1
x, y = y, x

通过多重赋值,可以交换两个变量的值。

函数可以返回多个值,使用多重赋值接收返回结果时,左边的变量个数需要和函数的返回值一样多,如下所示:

1
f, err = os.Open("foo.txt"// 函数返回两个值

通常函数使用额外的返回值来指示一些错误情况,如os.Open返回error类型。或者返回一个通常叫ok的bool类型变量,有三个操作符有类似的行为,如map查询,类型断言和通道接收动作,如下所示:

1
2
3
v, ok = m[key] // map 查询
v, ok = x.(T) // 类型断言
v, ok = <-ch // 通道接收

另外,可以将不需要的值赋给空标识符:

1
2
_, err = io.Copy(dst, src)  // 丢弃字节个数
-, ok = x.(T) // 检查类型但丢弃结果

命名类型

type 声明定义一个新的命名类型,它和某个已有类型使用同样的底层类型。命名类型提供了一个类型的别名,这个别名可以具有业务的含义。

1
type name underlying-type

类型的声明通常出现在包级别,这里命名的类型在整个包中可见,如果名字是导出的(开头使用大写字母),其它的包也可以访问它。

1
2
type Celsius float64       // 摄氏温度
type Fahrenheit float64 // 华氏温度

这里定义了两个新的类型,即使使用的相同的底层类型 float64, 它们也不是相同的类型。
从 float64 转换为 Celsius(t) 或 Fahrenheit(t) 需要显式类型转换,其中Celsius(t) 或 Fahrenheit(t) 是类型转换,不是函数调用。
在表达式中,Celsius 或 Fahrenheit 类型隐式转换为 float64,不用显式转换,如 c*9/5 + 32,其中c为Celsius类型。

对于每一类型T, 都有一个对应的类型转换操作 T(x) 将值 x 转换为类型T。如果两个类型具有相同的底层类型或二者都是指向相同底层类型变量的未命名指针类型,则二者是可以相互转换的。

命名类型的底层类型决定了它的结构和表达方式,以及它支持的内部操作集合,这些内部操作与直接使用底层类型的情况相同。这意味着,对于 Celsius 和 Fahrenheit 类型而言,它可以使用与 float64 相同的算术操作符。

通过 == 和 < 之类的比较操作符,命名类型的值可以与相同类型的值或者底层类型的值相比较。但是不同命令类型的值不能直接比较:

1
2
3
4
5
6
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true", 与底层类型进行比较
fmt.Println(f >= 0) // "true", 与底层类型进行比较
fmt.Println(c == f) // 编译错误:类型不匹配
fmt.Println(c == Celsius(f)) // "true", 与相同类型可以比较

包用于支持模块化、封装、编译隔离和重用。一个包的源代码保存在一个或多个以.go结尾的文件中,它所在目录路径就是包的导入路径。

在GO程序里,每一个包通过称为导入路径(import path)的唯一字符串来标识。如:import gopl.io/ch2/tempconv,一个导入路径标注一个目录,目录中包含构成包的一个或多个GO源文件。除了导入路径之外,每个包还有一个包名,它以短名字的形式(且不必是唯一的)出现在包的声明中。按约定,包名匹配导入路径的最后一段,这样可以方便地预测gopl.io/ch2/tempconv的包名是tempconv.

说明:包名不需要与导入路径中的最后一个目录同名,如gopl.io/ch2/tempconv路径下,GO源文件中的包名可以不是tempconv,可以改为newtempconv.但不建议这么做,为了让程序更好的可读性及方便维护,需要按照约定:导入路径的最后一个目录即是包名。

包的初始化

包的初始化从初始化包级别的变量开始,这些变量按照声明顺序初始化,有依赖关系的情况下,根据依赖的顺序执行。
除了初始化变量,对于复杂的操作,可以使用init函数,如 func init() {}, 这个init 函数不能被调用和引用,在每一个文件里,当程序高启动的时候,init函数按照它们声明的顺序自动执行。
包的初始化按照在程序中导入的顺序来进行,依赖顺序优先,每次初始化一个包。因此,如果包p导入了包q,可以确保q在p之前已经完全初始化。初始化过程是自下向上的,main包最后初始化。在这种方式下,在程序的main函数开始执行前,所有的包已经初始化完毕。

range语法

The range form of the for loop iterates over a slice or map.
When ranging over a slice, two values are returned for each iteration. The first is the index, and the second is a copy of the element at that index.

实例:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}