golang语言-2-go基本语法
文件名、关键字与标识符Go 的源文件以 一个源文件可以包含任意多行的代码,Go 本身没有对源文件的大小进行限制。 你会发现在 Go 代码中的几乎所有东西都有一个名称或标识符。另外,Go 语言也是区分大小写的,这与 C 家族中的其它语言相同。有效的标识符必须以字符(可以使用任何 UTF-8 编码的字符或 以下是无效的标识符:
在编码过程中,你可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。 下面列举了 Go 代码中会使用到的 25 个关键字或保留字:
之所以刻意地将 Go 代码中的关键字保持的这么少,是为了简化在编译过程第一步中的代码解析。和其它语言一样,关键字不能够作标识符使用。 除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数(第 6.5 节),它们的作用都将在接下来的章节中进行进一步地讲解。
程序一般由关键字、常量、变量、运算符、类型和函数组成。 程序中可能会使用到这些分隔符:括号 程序中可能会使用到这些标点符号: 程序的代码通过语句来实现结构化。每个语句不需要像 C 家族中的其它语言一样以分号 如果你打算将多个语句写在同一行,它们则必须使用
Go 程序的基本结构和要素示例 4.1 hello_world.go package main
import "fmt"
func main() {
fmt.Println("hello,world")
}
包的概念、导入与可见性包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。 如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 你必须在源文件中非注释的第一行指明这个文件属于哪个包,如: 一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 标准库 在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。在 Windows 下,标准库的位置在 Go 根目录下的子目录 Go 的标准库包含了大量的包(如:fmt 和 os),但是你也可以创建自己的包(第 8 章)。 如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。 属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。 如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。 Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 如果
这种机制对于编译大型的项目时可以显著地提升编译速度。 每一段代码只会被编译一次 一个 Go 程序是通过
如果需要多个包,它们可以被分别导入: import "fmt"
import "os"
或: import "fmt"; import "os"
但是还有更短且更优雅的方法(被称为因式分解关键字,该方法同样适用于 const、var 和 type 的声明或定义): import (
"fmt"
"os"
)
它甚至还可以更短的形式,但使用 gofmt 后将会被强制换行: import ("fmt"; "os")
当你导入多个包时,最好按照字母顺序排列包名,这样做更加清晰易读。 如果包名不是以 导入包即等同于包含了这个包的所有的代码对象。 除了符号 包通过下面这个被编译器强制执行的规则来决定是否将自身的代码对象暴露给外部文件: 可见性规则 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。 (大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。 因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。 假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用: 因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如: 示例 4.2 alias.go package main
import fm "fmt" // alias3
func main() {
fm.Println("hello,world")
}
注意事项 如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 包的分级声明和初始化 你可以在使用 函数这是定义一个函数最简单的格式: func functionName()
你可以在括号 main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误 func main must have no arguments and no return values results.
在程序开始执行并完成初始化后,第一个调用(程序的入口点)的函数是 函数里的代码(函数体)使用大括号 左大括号 `build-error: syntax error: unexpected semicolon or newline before {`
(这是因为编译器会产生 Go 语言虽然看起来不使用分号作为语句的结束,但实际上这一过程是由编译器自动完成,因此才会引发像上面这样的错误 右大括号 func Sum(a,b int) int { return a + b }
对于大括号 因此符合规范的函数一般写成如下的形式: func functionName(parameter_list) (return_value_list) {
…
}
其中:
只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。 下面这一行调用了 fmt.Println("hello,world")
使用
单纯地打印一个字符串或变量甚至可以使用预定义的方法来实现,如: 这些函数只可以用于调试阶段,在部署程序的时候务必将它们替换成 当被调用函数的代码执行到结束符 程序正常退出的代码为 0 即 注释示例 4.2 hello_world2.go package main
import "fmt" // Package implementing formatted I/O.
func main() {
fmt.Printf("Καλημ?ρα κ?σμε; or こんにちは 世界n")
}
上面这个例子通过打印 注释不会被编译,但可以通过 godoc 来使用(第 3.6 节)。 单行注释是最常见的注释形式,你可以在任何地方使用以 每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明,其中应该提供一些相关信息并对整体功能做简要的介绍。一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。当开发人员需要了解包的一些情况时,自然会用 godoc 来显示包的文档说明,在首行的简要注释之后可以用成段的注释来进行更详细的说明,而不必拥挤在一起。另外,在多段注释之间应以空行分隔加以区分。 示例: // Package superman implements methods for saving the world.
//
// Experience has shown that a small number of procedures can prove
// helpful when attempting to save the world.
package superman
几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 示例: // enterOrbit causes Superman to fly into low Earth orbit,a position
// that presents several possibilities for planet salvation.
func enterOrbit() error {
...
}
godoc 工具(第 3.6 节)会收集这些注释并产生一个技术文档。 类型可以包含数据的变量(或常量),可以使用不同的数据类型或类型来保存数据。使用 var 声明的变量的值会自动初始化为该类型的零值。类型定义了某个变量的值的集合与可对其进行操作的集合。 类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。 结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 语言中不存在类型继承。 函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后,例如: func FunctionName (a typea,b typeb) typeFunc
你可以在函数体中的某处返回使用类型为 typeFunc 的变量 var: return var
一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 func FunctionName (a typea,b typeb) (t1 type1,t2 type2)
示例: 函数 Atoi (第 4.7 节): 返回的形式: return var1,var2
这种多返回值一般用于判断某个函数是否执行成功(true/false)或与其它返回值一同返回错误消息(详见之后的并行赋值)。 使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体(第 10 章),但是也可以定义一个已经存在的类型的别名,如: type IZ int
这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。 然后我们可以使用下面的方式声明变量: var a IZ = 5
这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能(第 4.2.6 节)。 如果你有多个类型需要定义,可以使用因式分解关键字的方式,例如: type (
IZ int
FZ float64
STR string
)
每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。 程序的一般结构下面的程序可以被顺利编译但什么都做不了,不过这很好地展示了一个 Go 程序的首选结构。这种结构并没有被强制要求,编译器也不关心 main 函数在前还是变量的声明在前,但使用统一的结构能够在从上至下阅读 Go 代码时有更好的体验。 所有的结构将在这一章或接下来的章节中进一步地解释说明,但总体思路如下:
示例 4.4 gotemplate.go 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 程序的执行(程序启动)顺序如下:
类型转换在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数): valueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值) 示例: a := 5.0
b := int(a)
但这只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(例如将 int16 转换为 int32)。当从一个取值范围较大的转换到取值范围较小的类型时(例如将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。当编译器捕捉到非法的类型转换时会引发编译时错误,否则将引发运行时错误。 具有相同底层类型的变量之间可以相互转换: var a IZ = 5
c := int(a)
d := IZ(c)
Go 命名规范干净、可读的代码和简洁性是 Go 追求的主要目标。通过 gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 常量常量使用关键字 存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。 常量的定义格式: const Pi = 3.14159
在 Go 语言中,你可以省略类型说明符
一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时刻根据上下文来获得相关类型。 var n int
f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 int
常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。 数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出: const Ln2= 0.693147180559945309417232121458
176568075500134360255254120680009
const Log2E= 1/Ln2 // this is a precise reciprocal
const Billion = 1e9 // float constant
const hardEight = (1 << 100) >> 97
根据上面的例子我们可以看到,反斜杠 与各种类型的数字型变量相比,你无需担心常量之间的类型转换问题,因为它们都是非常理想的数字。 不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。另外,常量也允许使用并行赋值的形式: const beef,two,c = "eat",2,"veg"
const Monday,Tuesday,Wednesday,Thursday,Friday,Saturday = 1,3,4,5,6
const (
Monday,Wednesday = 1,3
Thursday,Saturday = 4,6
)
常量还可以用作枚举: const (
Unknown = 0
Female = 1
Male = 2
)
现在,数字 0、1 和 2 分别代表未知性别、女性和男性。这些枚举值可以用于测试某个变量或常量的实际值,比如使用 switch/case 结构 (第 5.3 节). 在这个例子中, const (
a = iota
b = iota
c = iota
)
第一个 const (
a = iota
b
c
)
( 译者注:关于 iota 的使用涉及到非常复杂多样的情况,这里作者解释的并不清晰,因为很难对 iota 的用法进行直观的文字描述。如希望进一步了解,请观看视频教程 《Go编程基础》 第四课:常量与运算符 )
当然,常量之所以为常量就是恒定不变的量,因此我们无法在程序运行过程中修改它的值;如果你在代码中试图修改常量的值则会引发编译错误。 引用 const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
你也可以使用某个类型作为枚举常量的类型: type Color int
const (
RED Color = iota // 0
ORANGE // 1
YELLOW // 2
GREEN // ..
BLUE
INDIGO
VIOLET // 6
)
变量简介声明变量的一般形式是使用 需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。Go 为什么要选择这么做呢? 首先,它是为了避免像 C 语言中那样含糊不清的声明形式,例如: 而在 Go 中,则可以很轻松地将它们都声明为指针类型: var a,b *int
其次,这种语法能够按照从左至右的顺序阅读,使得代码更加容易理解。 示例: var a int
var b bool
var str string
你也可以改写成这种形式: var (
a int
b bool
str string
)
这种因式分解关键字的写法一般用于声明全局变量。 当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。 变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如: 但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写(第 4.2 节:可见性规则)。 一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。 在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。在第 5 章,我们将会学习到像 if 和 for 这些控制结构,而在这些结构中声明的变量的作用域只在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。 尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。 变量可以编译期间就被赋值,赋值给变量使用运算符等号 示例: a = 15
b = false
一般情况下,当变量a和变量b之间类型相同时,才能进行如 声明与赋值(初始化)语句也可以组合起来。 示例: var identifier [type] = value
var a int = 15
var i = 5
var b bool = false
var str string = "Go says hello to the world!"
但是 Go 编译器的智商已经高到可以根据变量的值来自动推断其类型,这有点像 Ruby 和 Python 这类动态语言,只不过它们是在运行时进行推断,而 Go 是在编译时就已经完成推断过程。因此,你还可以使用下面的这些形式来声明及初始化变量: var a = 15
var b = false
var str = "Go says hello to the world!"
或: var (
a = 15
b = false
str = "Go says hello to the world!"
numShips = 50
city string
)
不过自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型,例如: var n int64 = 2
然而, var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)
这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 a := 1
下面这个例子展示了如何通过 示例 4.5 goos.go package main
import (
"fmt"
"runtime"
"os"
)
func main() {
var goos string = runtime.GOOS
fmt.Printf("The operating system is: %sn",goos)
path := os.Getenv("PATH")
fmt.Printf("Path is %sn",path)
}
如果你在 Windows 下运行这段代码,则会输出 这里用到了 值类型和引用类型程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为 “ 字 ”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4 字节)或 64 位(8 字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。 所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值: 另外,像数组(第 7 章)和结构(第 10 章)这些复合类型也是值类型。 当使用等号 你可以通过 &i 来获取变量 i 的内存地址(第 4.9 节),例如:0xf840000040(每次的地址都可能不一样)。值类型的变量的值存储在栈中。 内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。 更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。 这个内存地址被称之为指针(你可以从上图中很清晰地看到,第 4.9 节将会详细说明),这个指针实际上也被存在另外的某一个字中。 同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。 当使用赋值语句 如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。 在 Go 语言中,指针(第 4.9 节)属于引用类型,其它的引用类型还包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。 打印函数 func Printf(format string,list of variables to be printed)
在示例 4.5 中,格式化字符串为: 这个格式化字符串可以含有一个或多个的格式化标识符,例如: 函数 函数 fmt.Print("Hello:",23)
将输出: 简短形式,使用 := 赋值操作符我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,而这个时候再在 Example 4.4.1 的最后一个声明语句写上 a 和 b 的类型(int 和 bool)将由编译器自动推断。 这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 注意事项 如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如: 如果你在定义变量 a 之前使用它,则会得到编译错误 如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a: func main() {
var a string = "abc"
fmt.Println("hello,world")
}
尝试编译这段代码将得到错误 此外,单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用 但是全局变量是允许声明但不使用。 其他的简短形式为: 同一类型的多个变量可以声明在同一行,如: var a,b,c int
(这是将类型写在标识符后面的一个重要原因) 多变量可以在同一行进行赋值,如: a,b,c = 5,7,"abc"
上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用: a,c := 5,"abc"
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 这被称为 并行 或 同时 赋值。 如果你想要交换两个变量的值,则可以简单地使用 (在 Go 语言中,这样省去了使用交换函数的必要) 空白标识符
并行赋值也被用于当一个函数返回多个返回值时,比如这里的 init 函数变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。 每一个源文件都可以包含一个或多个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。 一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。 示例 4.6 init.go: package trans
import "math"
var Pi float64
func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}
在它的 init 函数中计算变量 Pi 的初始值。 示例 4.7 user_init.go 中导入了包 trans(在相同的路径中)并且使用到了变量 Pi: package main
import (
"fmt"
"./trans"
)
var twoPi = 2 * trans.Pi
func main() {
fmt.Printf("2*Pi = %gn",twoPi) // 2*Pi = 6.283185307179586
}
init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 func init() {
// setup preparations
go backend()
}
练习 推断以下程序的输出,并解释你的答案,然后编译并执行它们。 练习 4.1 local_scope.go: package main
var a = "G"
func main() {
n()
m()
n()
}
func n() { print(a) }
func m() {
a := "O"
print(a)
}
练习 4.2 global_scope.go: package main
var a = "G"
func main() {
n()
m()
n()
}
func n() {
print(a)
}
func m() {
a = "O"
print(a)
}
练习 4.3 function_calls_function.go package main
var a string
func main() {
a = "G"
print(a)
f1()
}
func f1() {
a := "O"
print(a)
f2()
}
func f2() {
print(a)
}
基本类型和运算符我们将在这个部分讲解有关布尔型、数字型和字符型的相关知识。 表达式是一种特定的类型的值,它可以由其它的值以及运算符组合而成。每个类型都定义了可以和自己结合的运算符集合,如果你使用了不在这个集合中的运算符,则会在编译时获得编译错误。 一元运算符只可以用于一个值的操作(作为后缀),而二元运算符则可以和两个值或者操作数结合(作为中缀)。 只有两个类型相同的值才可以和二元运算符结合,另外要注意的是,Go 是强类型语言,因此不会进行隐式转换,任何不同类型之间的转换都必须显式说明(第 4.2 节)。Go 不存在像 C 和 Java 那样的运算符重载,表达式的解析顺序是从左至右。 你可以在第 4.5.3 节找到有关运算符优先级的相关信息,优先级越高的运算符在条件相同的情况下将被优先执行。但是你可以通过使用括号将其中的表达式括起来,以人为地提升某个表达式的运算优先级。 4.5.1 布尔类型 bool一个简单的例子: 布尔型的值只可以是常量 true 或者 false。 两个类型相同的值可以使用相等 当相等运算符两边的值是完全相同的值的时候会返回 true,否则返回 false,并且只有在两个的值的类型相同的情况下才可以使用。 示例: var aVar = 10
aVar == 5 -> false
aVar == 10 -> true
当不等运算符两边的值是不同的时候会返回 true,否则返回 false。 示例: var aVar = 10
aVar != 5 -> true
aVar != 10 -> false
Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口(interface,第 11 章),它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的。如果以上条件都不满足,则其中一个值的类型必须在被转换为和另外一个值的类型相同之后才可以进行比较。 布尔型的常量和变量也可以通过和逻辑运算符(非 逻辑值可以被用于条件结构中的条件语句(第 5 章),以便测试某个条件是否满足。另外,和 Go 语言中包含以下逻辑运算符: 非运算符: !T -> false
!F -> true
非运算符用于取得和布尔值相反的结果。 和运算符: T && T -> true
T && F -> false
F && T -> false
F && F -> false
只有当两边的值都为 true 的时候,和运算符的结果才是 true。 或运算符: T || T -> true
T || F -> true
F || T -> true
F || F -> false
只有当两边的值都为 false 的时候,或运算符的结果才是 false,其中任意一边的值为 true 就能够使得该表达式的结果为 true。 在 Go 语言中,&& 和 || 是具有快捷性质的运算符,当运算符左边表达式的值已经能够决定整个表达式的值的时候(&& 左边的值为 false,|| 左边的值为 true),运算符右边的表达式将不会被执行。利用这个性质,如果你有多个条件判断,应当将计算过程较为复杂的表达式放在运算符的右侧以减少不必要的运算。 利用括号同样可以升级某个表达式的运算优先级。 在格式化输出时,你可以使用 布尔值(以及任何结果为布尔值的表达式)最常用在条件结构的条件语句中,例如:if、for 和 switch 结构(第 5 章)。 对于布尔值的好的命名能够很好地提升代码的可读性,例如以 4.5.2 数字类型4.5.2.1 整型 int 和浮点型 floatGo 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码(详情参见 二的补码 页面)。 Go 也有基于架构的类型,例如:int、uint 和 uintptr。 这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
Go 语言中没有 float 类型。 与操作系统架构无关的类型都有固定的大小,并在类型的名称中就可以看出来: 整数:
无符号整数:
浮点型(IEEE-754 标准):
int 型是计算最快的一种类型。 整型的零值为 0,浮点型的零值为 0.0。 float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。由于精确度的缘故,你在使用 你应该尽可能地使用 float64,因为 你可以通过增加前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。 你可以使用 Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用,下面这个程序很好地解释了这个现象(该程序无法通过编译): 示例 4.8 type_mixing.go package main
func main() {
var a int
var b int32
a = 15
b = a + a // 编译错误
b = b + 5 // 因为 5 是常量,所以可以通过编译
}
如果你尝试编译该程序,则将得到编译错误 同样地,int16 也不能够被隐式转换为 int32。 下面这个程序展示了通过显式转换来避免这个问题(第 4.2 节)。 示例 4.9 casting.go package main
import "fmt"
func main() {
var n int16 = 34
var m int32
// compiler error: cannot use n (type int16) as type int32 in assignment
//m = n
m = int32(n)
fmt.Printf("32 bit int is: %dn",m)
fmt.Printf("16 bit int is: %dn",n)
}
输出: 32 bit int is: 34
16 bit int is: 34
格式化说明符 在格式化字符串里,
数字值转换 当进行类似 func Uint8FromInt(n int) (uint8,error) {
if 0 <= n && n <= math.MaxUint8 { // conversion is safe
return uint8(n),nil
}
return 0,fmt.Errorf("%d is out of the uint8 range",n)
}
或者安全地从 float64 转换为 int: func IntFromFloat64(x float64) int {
if math.MinInt32 <= x && x <= math.MaxInt32 { // x lies in the integer range
whole,fraction := math.Modf(x)
if fraction >= 0.5 {
whole++
}
return int(whole)
}
panic(fmt.Sprintf("%g is out of the int32 range",x))
}
不过如果你实际存的数字超出你要转换到的类型的取值范围的话,则会引发 panic(第 13.2 节)。 问题 4.1 int 和 int64 是相同的类型吗? 4.5.2.2 复数Go 拥有以下复数类型: complex64 (32 位实数和虚数)
complex128 (64 位实数和虚数)
复数使用 示例: var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v",c1)
// 输出: 5 + 10i
如果 c = complex(re,im)
函数 在使用格式化说明符时,可以使用 复数支持和其它数字类型一样的运算。当你使用等号 4.5.2.3 位运算位运算只能用于整数类型的变量,且需当它们拥有等长位模式时。
二元运算符
一元运算符
当希望把结果赋值给第一个操作数时,可以简写为 位左移常见实现存储单位的用例 使用位左移与 iota 计数配合可优雅地实现存储单位的常量枚举: type ByteSize float64
const (
_ = iota // 通过赋值给空白标识符来忽略值
KB ByteSize = 1<<(10*iota)
MB
GB
TB
PB
EB
ZB
YB
)
在通讯中使用位左移表示标识的用例 type BitFlag int
const (
Active BitFlag = 1 << iota // 1 << 0 == 1
Send // 1 << 1 == 2
Receive // 1 << 2 == 4
)
flag := Active | Send // == 3
4.5.2.4 逻辑运算符Go 中拥有以下逻辑运算符: 它们之所以被称为逻辑运算符是因为它们的运算结果总是为布尔值 b3:= 10 > 5 // b3 is true
4.5.2.5 算术运算符常见可用于整数和浮点数的二元运算符有 (相对于一般规则而言,Go 在进行字符串拼接时允许使用对运算符
取余运算符只能作用于整数: 整数除以 0 可能导致程序崩溃,将会导致运行时的恐慌状态(如果除以 0 的行为在编译时就能被捕捉到,则会引发编译错误);第 13 章将会详细讲解如何正确地处理此类情况。 浮点数除以 0.0 会返回一个无穷尽的结果,使用 练习 4.4 尝试编译 divby0.go。 你可以将语句 对于整数和浮点数,你可以使用一元运算符 i++ -> i += 1 -> i = i + 1
i-- -> i -= 1 -> i = i - 1
同时,带有 在运算时 溢出 不会产生错误,Go 会简单地将超出位数抛弃。如果你需要范围无限大的整数或者有理数(意味着只被限制于计算机内存),你可以使用标准库中的 4.5.2.6 随机数一些像游戏或者统计学类的应用需要用到随机数。 示例 4.10 random.go 演示了如何生成 10 个非负随机数: 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())
}
}
可能的输出: 816681689 / 1325201247 / 623951027 / 478285186 / 1654146165 /
1951252986 / 2029250107 / 762911244 / 1372544545 / 591415086 / / 3 / 0 / 6 / 4 / 2 /22.10
/ 65.77 / 65.89 / 16.85 / 75.56 / 46.90 / 55.24 / 55.95 / 25.58 / 70.61 /
函数 你可以使用 4.5.3 运算符与优先级有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低: 优先级 运算符
7 ^ !
6 * / % << >> & &^
5 + - | ^
4 == != < <= >= >
3 <-
2 &&
1 ||
当然,你可以通过使用括号来临时提升某个表达式的整体运算优先级。 4.5.4 类型别名当你在使用某个类型时,你可以给它起另一个名字,然后你就可以在你的代码中使用新的名字(用于简化名称或解决名称冲突)。 在 示例 4.11 type.go package main
import "fmt"
type TZ int
func main() {
var a,b TZ = 3,4
c := a + b
fmt.Printf("c has the value: %d",c) // 输出:c has the value: 7
}
实际上,类型别名得到的新类型并非和原类型完全相同,新类型不会拥有原类型所附带的方法(第 10 章);TZ 可以自定义一个方法用来输出更加人性化的时区信息。 练习 4.5 定义一个 4.5.5 字符类型严格来说,这并不是 Go 语言的一个类型,字符只是整数的特殊用例。 在 ASCII 码表中,A 的值是 65,而使用 16 进制表示则为 41,所以下面的写法是等效的: var ch byte = 65 或 var ch byte = 'x41'
( 另外一种可能的写法是 不过 Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。其实 在书写 Unicode 字符时,需要在 16 进制数之前加上前缀 因为 Unicode 至少占用 2 个字节,所以我们使用 示例 4.12 char.go var ch int = 'u0041'
var ch2 int = 'u03B2'
var ch3 int = 'U00101234'
fmt.Printf("%d - %d - %dn",ch,ch2,ch3) // integer
fmt.Printf("%c - %c - %cn",ch3) // character
fmt.Printf("%X - %X - %Xn",ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U",ch3) // UTF-8 code point
输出: 65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234
格式化说明符 包
这些函数返回一个布尔值。包 字符串字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。UTF-8 是被广泛使用的编码格式,是文本文件的标准编码,其它包括 XML 和 JSON 在内,也都使用该编码。由于该编码对占用字节长度的不定性,Go 中的字符串也可能根据需要占用 1 至 4 个字节(示例见第 4.6 节),这与其它语言如 C++、Java 或者 Python 不同(Java 始终使用 2 个字节)。Go 这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。 字符串是一种值类型,且值不可变,即创建某个文本后你无法再次修改这个文本的内容;更深入地讲,字符串是字节的定长数组。 Go 支持以下 2 种形式的字面值:
和 C/C++不一样,Go 中的字符串是根据长度限定,而非特殊字符 |