命名
保留字
一个名字必须以一个字母( Unicode
字母) 或下划线开头, 后面可以跟任意数量的字母、 数字或下划线。 大写字母和小写字母是不同的 Go
语言中类似if
和switch
的保留字有**25**
个; 关键字不能用于自定义名字, 只能在特定语法结构中使用 30
多个预定义的名字, 比如int
和true
等, 主要对应内建的常量、 类型和函数
风格
- 名字的开头字母的大小写决定了名字在包外的可见性。 如果一个名字是大写字母开头的( 译注: 必须是在函数外部定义的包级名字; 包级函数名本身也是包级名字) , 那么它将是导出的, 也就是说可以被外部的包访问, 例如
fmt
包的Printf
函数就是导出的, 可以在fmt
包外部访问。 包本身的名字一般总是用小写字母 - 名字的长度没有逻辑限制, 但是
Go
语言的风格是尽量使用短小的名字, 如果一个名字的作用域比较大, 生命周期也比较长, 那么用长的名字将会更有意义 Go
语言程序员推荐使用 驼峰式 命名, 当名字有几个单词组成的时优先使用大小写分隔, 而不是优先用下划线分隔声明
声明语句定义了程序的各种实体对象以及部分或全部的属性,Go
语言主要有四种类型的声明
var
:变量const
:常量type
:类型func
:函数
一个Go
语言编写的程序对应一个或多个以.go
为文件后缀名的源文件中。 每个源文件以包的声明语句开始, 说明该源文件是属于哪个包。 包声明语句之后是import
语句导入依赖的其它包,然后是包一级的类型、 变量、 常量、 函数的声明语句, 包一级的各种类型的声明语句的顺序无关紧要,函数内部必须先声明在使用
一个函数的声明由一个函数名字、 参数列表( 由函数的调用者提供参数变量的具体值) 、 一个可选的返回值列表和包含函数定义的函数体组成。 如果函数没有返回值, 那么返回值列表是省略的。 执行函数从函数的第一个语句开始, 依次顺序执行直到遇到renturn
返回语句, 如果没有返回语句则是执行到函数末尾, 然后返回到函数调用者。
func fToC(f float64) float64 {
return (f - 32) * 5 / 9
}
变量
一般语法
“类型”或“= 表达式”两个部分可以省略其中的一个。
省略的是类型信息, 那么将根据初始化表达式来推导变量的类型信息。 如果初始化表达式被省略, 那么将用零值初始化该变量。
- 数值类型变量对应的零值是0,
- 布尔类型变量对应的零值是false,
- 字符串类型对应的零值是空字符串,
- 接口或引用类型( 包括
slice
、map
、chan
和函数) 变量对应的零值是nil
。 - 数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
多个变量声明
在一个声明语句中同时声明一组变量, 或用一组初始化表达式声明并初始化一组变量。 如果省略每个变量的类型, 将可以声明多个类型不同的变量( 类型由初始化表达式推导)
也可以通过函数调用的返回值进行初始化
短变量声明
函数内部有一种:=
变量声明的方法,其变量的类型根据表达式来自动推导,如,
简短变量声明语句也可以用来声明和初始化一组变量i, j := 1, 2
**:=**
是一个变量声明语句, 而**=**
是一个变量赋值操作。 也不要混淆多个变量的声明和元组的多重赋值 , 后者是将右边各个的表达式值赋值给左边对应位置的各个变量:
同样可以使用函数返回值定义// 交换值
i, j = j, i
简短变量声明左边的变量可能并不是全部都是刚刚声明的。 如果有一些已经在相同的词法域声明过了, 那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了f, err := os.Open(name)
并且,简短变量声明语句中必须至少要声明一个新的变量, 下面的代码将会编译失败,解决的方法是第二个简短变量声明语句改用普通的多重赋值语言in, err := os.Open(infile)
// err 只有赋值行为
out, err := os.Create(outfile)
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables
指针
一个变量对应一个保存了变量对应类型值的内存空间。 普通变量在声明语句创建时被绑定到一个变量名, 比如叫x
的变量, 但是还有很多变量始终以表达式方式引入, 例如x[i]
或x.f
变量。
一个指针的值是另一个变量的地址。 一个指针对应变量在内存中的存储位置。 并不是每一个值都会有一个内存地址, 但是对于每一个变量必然有对应的内存地址。 通过指针, 可以直接读或更新对应变量的值, 而不需要知道该变量的名字, 对于聚合类型每个成员——比如结构体的每个字段、 或者是数组的每个元素——也都是对应一个变量, 因此可以被取地址
例如,声明一个变量x
```go package main
import “fmt”
func main() { x := 99 p := &x fmt.Println(p, *p) } // out 0xc0000140d8 99
1. `&x`表达式( 取`x`变量的内存地址) 将产生一个**指向该整数变量的指针**, 指针对应的数据类型是` *int` , 指针被称之为**“指向int类型的指针” **
1. 如果指针名字为`p`, 那么可以说“`**p**`**指针指向变量**`**x**`”, 或者说“`p`指针保存了`x`变量的内存地址”
1. `*p` 表达式对应p指针指向的变量的值。 一般 `*p` 表达式读取指针指向的变量的值, 这里为`int`类型的值, 同时因为 `*p` 对应一个变量, 所以该表达式也可以出现在赋值语句的左边, 表示更新指针所指向的变量的值
1. 变量有时候被称为可寻址的值。 即使变量由表达式临时生成, 那么表达式也必须能接受 `&` 取地址操作
```go
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
任何类型的指针的零值都是nil
。 如果p != nil
测试为真, 那么p
是指向某个有效变量。 指针之间也是可以进行相等测试的, 只有当它们指向同一个变量或全部是nil时才相等
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
在Go
语言中, 返回函数中局部变量的地址也是安全的。 例如下面的代码, 调用f函数时创建局部变量v
, 在局部变量地址被返回之后依然有效, 因为指针p
依然引用这个变量, 但是每次调用的结果都是不一致的,
package main
import "fmt"
func test() *int {
x := 99
return &x
}
func main(){
p1 := test()
p2 := test()
fmt.Println(p1, p2)
fmt.Println(p1 == p2)
}
// out: 0xc0000140d8 0xc0000140e0
// false
指针包含了一个变量的地址, 因此如果将指针作为参数调用函数, 那将可以在函数中通过该指针来更新变量的值。 例如下面这个例子就是通过指针来更新变量的值, 然后返回更新后的值,下面的例子并未改变指针,只是改变了指针对应的值
package main
import "fmt"
func test(p *int) int {
*p++
return *p
}
func main(){
x := 100
fmt.Println(&x)
test(&x)
fmt.Println(*&x)
fmt.Println(&x)
}
// out:
// 0xc0000140d8
// 101
// 0xc0000140d8
每次我们对一个变量取地址, 或者复制指针, 我们都是为原变量创建了新的别名。 例如, *p
就是是 变量v
的别名。 指针特别有价值的地方在于我们可以不用名字而访问一个变量, 但是这是一把双刃剑: 要找到一个变量的所有访问者并不容易, 我们必须知道变量全部的别名, 不仅仅是指针会创建别名, 很多其他引用类型也会创建别名, 例如slice
、 map
和chan
, 甚至结构体、 数组和接口都会创建所引用变量的别名
flag包
指针是实现标准库中flag包的关键技术, 它使用命令行参数来设置对应变量的值, 而这些对应命令行标志参数的变量可能会零散分布在整个程序中。
package main
import (
"fmt"
"flag"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Println(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
调用flag.Bool
函数会创建一个新的对应布尔型标志参数的变量。 它有三个属性
- 命令行标志参数的名字
n
- 然后是该标志参数的默认值
- 该标志参数对应的描述信息。 如果用户在命令行输入了一个无效的标志参数, 或者输入
-h
或-help
参数, 那么将打印所有标志参数的名字、 默认值和描述信息
调用flag.String
函数将于创建一个对应字符串类型的标志参数变量, 同样包含命令行标志参数对应的参数名、 默认值、 和描述信息。 程序中的 **sep **
和 **n**
变量分别是指向对应命令行标志参数变量的指针, 因此
必须用 ***sep **
和***n**
形式的指针语法间接引用它们。
运行过程
当程序运行时, 必须在使用标志参数对应的变量之前调用先flag.Parse
函数, 用于更新每个标志参数对应变量的值( 之前是默认值) 。 对于非标志参数的普通命令行参数可以通过调用flag.Args()
函数来访问, 返回值对应对应一个字符串类型的**slice**
。 如果在flag.Parse
函数解析命令行参数时遇到错误, 默认将打印相关的提示信息, 然后调用os.Exit(2)
终止程序。
package main
import (
"fmt"
"flag"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Println(*sep)
fmt.Println(flag.Args())
fmt.Println(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
// go run src/po.go -s /*-*/ -n a b c d
// out
// /*-*/
// [a b c d]
// a/*-*/b/*-*/c/*-*/d
new函数
表达式new(T)
将创建一个T
类型的匿名变量, 初始化为T
类型的零值, 然后返回变量地址, 返回的指针类型为 *T
...
func main() {
p := new(int)
fmt.Println(*p)
*p = 123
fmt.Println(*p)
}
// out
// 0
// 123
用new创建变量和普通变量声明语句方式创建变量没有什么区别, 除了不需要声明一个临时变量的名字外, 我们还可以在表达式中使用new(T)
。 换言之, **new**
函数类似是一种语法糖, 而不是一个新的基础概念
生命周期
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。 对于在包一级声明的变量来说, 它们的生命周期和整个程序的运行周期是一致的。 而相比之下, 在局部变量的声明周期则是动态的: 从每次创建一个新变量的声明语句开始, 直到该变量不再被引用为止, 然后变量的存储空间可能被回收。
赋值
使用赋值语句可以更新一个变量的值, 最简单的赋值语句是将要被赋值的变量放在=
的左边,新值的表达式放在=
的右边
第四个也可写成如下形式
count[x] *= scale
数值变量也可以支持 ++
递增和--
递减语句( 译注: 自增和自减是语句, 而不是表达式, 因此** x = i++ **
之类的表达式是错误的)
元组赋值
元组赋值是另一种形式的赋值语句, 它允许同时更新多个变量的值。 在赋值之前, 赋值语句右边的所有表达式将会先进行求值, 然后再统一更新左边对应变量的值。 如交换两个值
x, y = y, x
a[i], a[j] = a[j], a[i]
i, j, k = 2, 3, 5
// 计算公约数
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
有些表达式会产生多个值, 比如调用一个有多个返回值的函数。 当这样一个函数调用出现在元组赋值右边的表达式中时 , 左边变量的数目必须和右边一致,和变量声明一样, 我们可以用下划线空白标识符 _
来丢弃不需要的值
f, err = os.Open("foo.txt")
_, err = os.Open("foo.txt")
可赋值性
赋值语句是显式的赋值形式, 但是程序中还有很多地方会发生隐式的赋值行为: 函数调用会隐式地将调用参数的值赋值给函数的参数变量, 一个返回语句将隐式地将返回操作的值赋值给结果变量, 一个复合类型的字面量 也会产生赋值行为。
medals := []string{"gold", "silver", "bronze"}
// 相当于
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
不管是隐式还是显式地赋值, 在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。 更直白地说, 只有右边的值对于左边的变量是可赋值的, 赋值语句才是允许的
类型
变量或表达式的类型定义了对应存储值的属性特征,是否支持一些操作符, 以及它们自己关联的方法
集等
在任何程序中都会存在一些变量有着相同的内部结构, 但是却表示完全不同的概念。 例如,
- 一个int类型的变量可以用来表示一个循环的迭代索引、 或者一个时间戳、 或者一个文件描述符、 或者一个月份;
- 一个float64类型的变量可以用来表示每秒移动几米的速度、 或者是不同温度单位下的温度;
- 一个字符串可以用来表示一个密码或者一个颜色的名称
一个类型声明语句创建了一个新的类型名称, 和现有类型具有相同的底层结构。 新命名的类型提供了一个方法,用来分隔不同概念的类型, 这样即使它们底层类型相同也是不兼容的
类型声明语句一般出现在包一级, 因此如果新创建的类型名字的首字符大写, 则在外部包也可以使用
例如,将不同温度单位分别定义为不同的类型
package tempconv
import "fmt"
type Celsius float64
type Farenheit float64
func CToF(c Celsius) Farenheit { return Farenheit(c * 9/9 + 32) }
func FToC(f Farenheit) Farenheit { return Celsius(c * 9/9 + 32) }
在这个包声明了两种类型: Celsius
和Fahrenheit
分别对应不同的温度单位。 它们虽然有着相同的底层类型float64
, 但是它们是不同的数据类型, 因此它们不可以被相互比较或混在一个表达式运算。 刻意区分类型, 可以避免一些像无意中使用不同单位的温度混合计算导致的错误; 因此需要一个类似Celsius(t)
或Fahrenheit(t)
形式的显式转型操作才能将**float64**
转为对应的类型。 Celsius(t)
和Fahrenheit(t)
是类型转换操作, 它们并不是函数调用。 类型转换不会改变值本身, 但是会使它们的语义发生变化。 另一方面, CToF
和FToC
两个函数则是对不同温度单位下的温度进行换算, 它们会返回不同的值。
一般来说,
- 对于每一个类型
T
, 都有一个对应的类型转换操作T(x)
, 用于将x
转为T
类型 - 如果T是指针类型, 可能会需要用小括弧包装T, 比如
(*int)(0)
- 只有当两个类型的底层基础类型相同时, 才允许这种转型操作, 或者是两者都是指向相同底层结构的指针类型, 这些转换只改变类型而不会影响值本身。
比较运算符 ==
和 <
也可以用来比较一个命名类型的变量和另一个有相同类型的变量, 或有着相同底层类型的未命名类型的值之间做比较。 但是如果两个值有着不同的类型, 则不能直接进行比较
命名类型还可以为该类型的值定义新的行为。 这些行为表示为一组关联到该类型的函数集合, 我们称为类型的方法集。写法如下,
下面的声明语句, Celsius
类型的参数c
出现在了函数名的前面, 表示声明的是Celsius
类型的一个叫名叫String
的方法, 该方法返回该类型对象c
带着°C
温度单位的字符串:
func (c Celsius) String() string {return fmt.Sprintf("%g°C", c)}
一个距离转换小程序
package main
import (
"fmt"
"flag"
)
type M float64
type FT float64
type KM float64
const (
KFT M = 0.3048
KKM M = 1000
)
var m = flag.Float64("m", 1.0, "m")
func MToFT(m M) FT { return FT(m / KFT) }
func MToKM(m M) KM { return KM(m / KKM) }
func main() {
flag.Parse()
ft := MToFT(M(*m))
km := MToKM(M(*m))
fmt.Printf("%f(m)\t%f(ft)\t%f(km)\n", *m, ft, km)
}
// out
// go run src/po.go -m 1.80
// 1.800000(m) 5.905512(ft) 0.001800(km)
包和文件
Go
语言中的包和其他语言的库或模块的概念类似, 目的都是为了支持模块化、 封装、 单独编译和代码重用。一个包的源代码保存在一个或多个以.go
为文件后缀名的源文件中, 通常一个包所在目录路径的后缀是包的导入路径。每个包都对应一个独立的名字空间,引用包内函数必须显式引用
// 包 gopl.io/ch1/helloworld
// 路径 $GOPATH/src/gopl.io/ch1/helloworld
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息:
- 如果一个名字是大写字母开头的, 那么该名字是导出的
导入包
每个包还有一个包名, 包名一般是短小的名字( 并不要求包名是唯一的)包名在包的声明处指定。 按照惯例, 一个包的名字和包的导入路径的最后一个字段相同, 例如gopl.io/ch2/tempconv
包的名字一般是tempconv
,
例如,io
包
内部源文件的包名multi.go
pipe.go
工具
可以使用golang.org/x/tools/cmd/goimports
导入工具, 它可以根据需要自动添加或删除导入的包; 许多编辑器都可以集成goimports
工具, 然后在保存文件的时候自动运行。 类似的还有gofmt
工具, 可以用来格式化Go
源文件