1.文件名、关键字与标识符
Go 的源文件以 .go
为后缀名存储在计算机中,这些文件名均由小写字母组成,如 scanner.go
。如果文件名由多个部分组成,则使用下划线 _
对它们进行分隔,如 scanner_test.go
。文件名不包含空格或其他特殊字符。
_
本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。
Go 代码中会使用到的 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 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
程序的代码通过语句来实现结构化。每个语句不需要像 C 家族中的其它语言一样以分号 ;
结尾,因为这些工作都将由 Go 编译器自动完成。
如果你打算将多个语句写在同一行,它们则必须使用 ;
人为区分,但在实际开发中我们并不鼓励这种做法。
2.Go 程序的基本结构和要素
2.1包的概念、导入与可见性
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go
为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main
。package main
表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main
的包。
另外要注意的是,所有的包名都应该使用小写字母。
如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。
属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。
如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。
Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o
的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。
如果 A.go
依赖 B.go
,而 B.go
又依赖 C.go
:
- 编译
C.go
,B.go
, 然后是A.go
. - 为了编译
A.go
, 编译器读取的是B.o
而不是C.o
.
这种机制对于编译大型的项目时可以显著地提升编译速度。
可见性规则
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如:import fm "fmt"
。下面的代码展示了如何使用包的别名:
package main
import fm "fmt" // alias3
func main() {
fm.Println("hello, world")
}
2.2 函数
这是定义一个函数最简单的格式:
func functionName()
你可以在括号 ()
中写入 0 个或多个函数的参数(使用逗号 ,
分隔),每个参数的名称后面必须紧跟着该参数的类型。(注意,要是多个参数的类型是一样的,可以在最后一个相同类型参数加类型就 ok了)。
2.3 注释
单行注释是最常见的注释形式,你可以在任何地方使用以 //
开头的单行注释。多行注释也叫块注释,均已以 /*
开头,并以 */
结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
package main
import "fmt" // Package implementing formatted I/O.
func main() {
/* 多行
注释
*/
fmt.Printf("Καλημέρα κόσμε; or こんにちは 世界\n")
}
2.4 类型
类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。
结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 语言中不存在类型继承。
2.5 Go 程序的一般结构
下面的程序可以被顺利编译但什么都做不了,不过这很好地展示了一个 Go 程序的首选结构。这种结构并没有被强制要求,编译器也不关心 main 函数在前还是变量的声明在前,但使用统一的结构能够在从上至下阅读 Go 代码时有更好的体验。
所有的结构将在这一章或接下来的章节中进一步地解释说明,但总体思路如下:
- 在完成包的 import 之后,开始对常量、变量和类型的定义或声明。
- 如果存在 init 函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。
- 如果当前包是 main 包,则定义 main 函数。
- 然后定义其余的函数,首先是类型的方
-
package main import ( "fmt" ) const c = "C" var v int = 5 type T struct{} func init() { // initialization of package } func main() { var a int Func1() // ... fmt.Println(a) } func (t T) Method1() { //... } func Func1() { // exported function Func1 //... }
Go 程序的执行(程序启动)顺序如下:
- 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
- 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
- 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init 函数的话,则调用该函数。
- 在完成这一切之后,main 也执行同样的过程,最后调用 main 函数开始执行程序。
2.6 类型转换
在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数)
valueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值)
a := 5.0
b := int(a)
但这只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(例如将 int16 转换为 int32)。当从一个取值范围较大的转换到取值范围较小的类型时(例如将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。当编译器捕捉到非法的类型转换时会引发编译时错误,否则将引发运行时错误。
2.7 Go 命名规范
干净、可读的代码和简洁性是 Go 追求的主要目标。通过 gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 Get...
之类的字符,如果是用于修改某个对象,则使用 SetName
。有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使用下划线来分割多个名称。
3 常量
因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。
常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
- 正确的做法:
const c1 = 2/3
- 错误的做法:
const c2 = getNumber()
// 引发构建错误:getNumber() used as value
iota
可以被用作枚举值:
const (
a = iota
b = iota
c = iota
)
第一个 iota
等于 0,每当 iota
在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2
可以简写为如下形式:
const (
a = iota
b
c
)
4 变量
4.1 基本
声明变量的一般形式是使用 var
关键字:var identifier type
。
声明与赋值(初始化)语句也可以组合起来。
var identifier [type] = value
var a int = 15
var i = 5
var b bool = false
var str string = "Go says hello to the world!"
4.2 值类型和引用类型
所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:
当使用等号 =
将一个变量的值赋值给另一个变量时,如:j = i
,实际上是在内存中将 i 的值进行了拷贝:
你可以通过 &i 来获取变量 i 的内存地址,例如:0xf840000040(每次的地址都可能不一样)。值类型的变量的值存储在栈中。
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
这个内存地址被称之为指针(你可以从上图中很清晰地看到,第 4.9 节将会详细说明),这个指针实际上也被存在另外的某一个字中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 r2 = r1
时,只有引用(地址)被复制。
如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。
在 Go 语言中,指针(第 4.9 节)属于引用类型,其它的引用类型还包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
4.3 init 函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性
package trans
import "math"
var Pi float64
func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}
init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 backend()
:
func init() {
// setup preparations
go backend()
}
4.4 类型
Go 也有基于架构的类型,例如:int、uint 和 uintptr。
这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
int
和uint
在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。uintptr
的长度被设定为足够存放一个指针即可。
整数:
- int8(-128 -> 127)
- int16(-32768 -> 32767)
- int32(-2,147,483,648 -> 2,147,483,647)
- int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)
int 型是计算最快的一种类型。
整型的零值为 0,浮点型的零值为 0.0。
float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。由于精确度的缘故,你在使用 ==
或者 !=
来比较浮点数时应当非常小心。你最好在正式使用前测试对于精确度要求较高的运算。
你应该尽可能地使用 float64,因为 math
包中所有有关数学运算的函数都会要求接收这个类型。
格式化说明符
在格式化字符串里,%d
用于格式化整数(%x
和 %X
用于格式化 16 进制表示的数字),%g
用于格式化浮点型(%f
输出浮点数,%e
输出科学计数表示法),%0nd
用于规定输出长度为n的整数,其中开头的数字 0 是必须的。
%n.mg
用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串 %5.2e
来输出 3.4 的结果为 3.40e+00
。
当进行类似 a32bitInt = int32(a32Float)
的转换时,小数点后的数字将被丢弃。这种情况一般发生当从取值范围较大的类型转换为取值范围较小的类型时,或者你可以写一个专门用于处理类型转换的函数来确保没有发生精度的丢失。
随机数
一些像游戏或者统计学类的应用需要用到随机数。rand
包实现了伪随机数的生成。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
a := rand.Int()
fmt.Printf("%d / ", a)
}
for i := 0; i < 5; i++ {
r := rand.Intn(8)
fmt.Printf("%d / ", r)
}
fmt.Println()
timens := int64(time.Now().Nanosecond())
rand.Seed(timens)
for i := 0; i < 10; i++ {
fmt.Printf("%2.2f / ", 100*rand.Float32())
}
}
4.5 strings 和 strconv 包
作为一种基本数据结构,每种语言都有一些对于字符串的预定义处理函数。Go 中使用 strings
包来完成对字符串的主要操作。
前缀和后缀
HasPrefix
判断字符串 s
是否以 prefix
开头:
strings.HasPrefix(s, prefix string) bool
HasSuffix
判断字符串 s
是否以 suffix
结尾:
strings.HasSuffix(s, suffix string) bool
字符串包含关系
Contains
判断字符串 s
是否包含 substr
:
strings.Contains(s, substr string) bool
判断子字符串或字符在父字符串中出现的位置(索引)
Index
返回字符串 str
在字符串 s
中的索引(str
的第一个字符的索引),-1 表示字符串 s
不包含字符串 str
strings.Index(s, str string) int
修改字符串大小写
ToLower
将字符串中的 Unicode 字符全部转换为相应的小写字符:
strings.ToLower(s) string
ToUpper
将字符串中的 Unicode 字符全部转换为相应的大写字符:
strings.ToUpper(s) string
修剪字符串
你可以使用 strings.TrimSpace(s)
来剔除字符串开头和结尾的空白符号;如果你想要剔除指定字符,则可以使用 strings.Trim(s, "cut")
来将开头和结尾的 cut
去除掉。该函数的第二个参数可以包含任何字符,如果你只想剔除开头或者结尾的字符串,则可以使用 TrimLeft
或者 TrimRight
来实现。
分割字符串
strings.Fields(s)
将会利用 1 个或多个空白符号来作为动态长度的分隔符将字符串分割成若干小块,并返回一个 slice,如果字符串只包含空白符号,则返回一个长度为 0 的 slice。
strings.Split(s, sep)
用于自定义分割符号来对指定字符串进行分割,同样返回 slice。
拼接 slice 到字符串
Join
用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串:
strings.Join(sl []string, sep string) string
字符串与其它类型的转换
与字符串相关的类型转换都是通过 strconv
包实现的。
该包包含了一些变量用于获取程序运行的操作系统平台下 int 类型所占的位数,如:strconv.IntSize
。
任何类型 T 转换为字符串总是成功的。
针对从数字类型转换到字符串,Go 提供了以下函数:
strconv.Itoa(i int) string
返回数字 i 所表示的字符串类型的十进制数。strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string
将 64 位浮点型的数字转换为字符串,其中fmt
表示格式(其值可以是'b'
、'e'
、'f'
或'g'
),prec
表示精度,bitSize
则使用 32 表示 float32,用 64 表示 float64。
将字符串转换为其它类型 tp 并不总是可能的,可能会在运行时抛出错误 parsing "…": invalid argument
。
针对从字符串类型转换为数字类型,Go 提供了以下函数:
strconv.Atoi(s string) (i int, err error)
将字符串转换为 int 型。strconv.ParseFloat(s string, bitSize int) (f float64, err error)
将字符串转换为 float64 型。
指针
不像 Java 和 .NET,Go 语言为程序员提供了控制数据结构的指针的能力;但是,你不能进行指针运算。通过给予程序员基本内存布局,Go 语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这些对构建运行良好的系统是非常重要的:指针对于性能的影响是不言而喻的,而如果你想要做的是系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。
Go 语言的取地址符是 &
,放到一个变量前使用就会返回相应变量的内存地址。