Go 和其他语言一样,程序是由很多小的基础构件组成的,变量存值,简单的加法和减法运算被组合成复杂的表达式,还有基础类型被聚合为数组或结构体,后面这些都会一一学习到,先从最简单的 命令, 声明, 变量, 赋值类型, 包和文件, 作用域这些基础的概念学习

命名


在 Go 中函数名, 变量名, 常量名, 类型名, 语句标识包名, 遵循的规则为 一个名字必须以一个字母或下划线开头,后面可以跟任意数量的字母,数字或下划线, 严格区分大小写

关键字

Go 的关键字并不是很多,有25个, 关键字不能用于自定义的名字,在特定的语法结构中使用

1
2
3
4
5
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 中这样的预定义名字大约有 30 多个, 主要对应 内建的常量, 类型函数

1
2
3
4
5
6
7
8
9
10
11
// 内建常量:
true false iota nil
// 内建类型:
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
float32 float64 complex128 complex64 uintptr
bool byte rune string error
// 内建函数:
make len cap new append copy close delete
complex real imag
panic recover

命名推荐

名字出现的位置, 影响到这个名字的有效使用范围,和作用域有关系,作用域这个后面会有讲到,名字在函数内部定义, 就只在函数内部有效,在 函数外部定义, 当前包的所有文件中都可以访问, 名字的开头字母的大小决定了包外的可见性, 大写字母开头,是可以导出的, 可以被外部访问, 包名采用小写字母, Go语言名字的风格推荐短小的名字, 驼峰式命名, 多个单词单用大小写分割

声明


Go语言主要有四种类型声明的语句: var,const, typefunc, 分别对应 变量, 常量, 类型, 函数实体对象, 一个 Go 语言程序对应一个或多个.go 为文件后缀名的源文件中,每个源文件以包的声明语句开始,说明该源文件属于那个包

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

import "fmt"

const boilingF = 212.0

func main() {
var f = boilingF
var c = (f - 32) * 8 / 9
fmt.Printf("boiling point = %g°F or %g°C \n", f, c)
}
  • import import语句导入依赖的其它包
  • boilingF 变量是包一级声明语句声明, 可以在整个包对应的每个源文件中被访问
  • fc 变量是函数内部声明, 只能在函数内部被访问

变量


var 声明语句可以创建特定类型的变量, 给变量附加一个名字,并且设置变量的初始值

语法格式

1
var 变量名字  类型 = 表达式
  • 表达式类型 两个可以省略一个, 省略类型信息, 将根据初始化表达式推导变量的类型信息, 省略 表达式, 将用 零值 初始化该变量, 这里要注意的是 接口引用类型(包括 slice, 指针, map chan 和 函数) 变量对应的零值是 nil, 这样做的好处是 可以确保每个声明的变量总有一个良好定义的值, Go语言中不存在未初始化的变量
1
2
var s string
fmt.Println(s) // ""

同时声明多个变量

1
2
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

简短变量

简短变量声明语句的形式可用于声明和初始化局部变量, 形式为 名字 := 表达式, 变量的类型根据表达式来自动推导,多用与局部变量的声明和初始化,var 声明语句多用于需要显示指定变量类型的地方, := 是一个变量声明语句,= 是一个变量赋值操作。

1
2
3
t := 0.0
freq := rand.Float64() * 3.0
i, j := 0, 1

:=是一个变量声明语句

1
i, j = j, i //交换i和j的值

简短变量 声明左边的变量可能并不是全部都是刚刚声明的,如果有一些已经在相同的作用域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为, 简短变量声明语句中至少要声明一个新的变量, 如果不在同一作用域中,那么简短变量将会在当前作用域声明一个新的变量

1
2
3
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables

指针


变量对应一个保存了变量对应类型值的内存空间, 普通变量在声明语句创建时被绑定到一个变量名, 还有很多变量是用表达式引入的, x[i]x.f 变量, 所有这些表达式一般都是读取一个变量的值。一个指针的值是另一个变量的地址, 一个指针对应变量在内存中的存储位置, 并不是每个值都会有一个内存地址,但每一个变量都会有对应的内存地址,通过指针我们可以直接读或更新对应变量的值,而不需要知道该变量的名字

1
2
3
4
5
6
7

x := 1
p := &x
fmt.Println(*p)
*p = 4
fmt.Println(x)
}

声明 x 变量, &x 表达式将产生一个指向该整数变量的指针, 指针对应的类型为 *int, 指针名为p, 那么可以说p指针指向变量 x, *p 表达式对应p指针指向的变量的值,*p表达式读取指针指向变量的值

聚合类型

聚合类型, 比如结构体的每个字段,或者数组的每个元素,也都是对应一个变量,所以可以被取地址,变量有时被称为可寻址的值,即使变量由表达式临时生成,那么表达式也必须能接受 & 取地址操作,任何类型的指针零值都是 nil

1
2
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"

返回局部变量地址

1
2
3
4
5
6
7
var p = f()

func f() *int {
v := 1
return &v
}

Go语言中, 返回函数中局部变量的地址也是安全的,局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。

指针更新变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func incr(p *int) int {
*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
return *p
}

func main() {
fmt.Println(x)
v := 1
incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
}

指针包含一个变量的地址,将指针作为参数调用函数, 可以在函数中通过该指针来更新变量的值, 然后返回更新后的值, 对一个变量取地址,或者复制指针, 都是为原变量创建了新的别名, *p 就是变量 v 的别名, 指针的特别价值在于我们不用名字而访问一个变量

new 函数

在Go中,还有一个创建变量的方法是调用内建new函数, 表达式 new(T) 将创建一个T类型的匿名变量,初始化为 T 类型的零值,返回变量的地址

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

import "fmt"

func main() {

p := new(int)
fmt.Println(*p)
*p = 2
fmt.Println(*p)
}

new 创建变量和普通变量声明语句方式创建变量没有什么区别, new 函数类似一种语法糖,而不是一个新的基础概念, 下面这两个函数具有相同的行为, 多数情况下每次调用 new 函数都是返回一个新的变量的地址, 如果两个类型的大小都是0, 有可能有相同的地址

1
2
3
4
5
6
7
8
9

func newInt1() *int {
return new(int)
}

func newInt2() *int {
var dummy int
return &dummy
}

变量的生命周期

声明周期是指在程序运行期间变量的有效存在的时间间隔

  • 包一级声明的变量: 生命周期和整个程序运行的周期是一致的
  • 局部变量的声明: 声明周期是动态的,每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被收回,函数的参数变量和返回值变量都是局部变量

赋值


赋值语句可以更新一个变量的值, 是将被赋值的变量放在 = 的左边,新值表达式放在 = 的右边

1
2
3
4
x = 1                       // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = "bob" // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值

元祖赋值

元祖赋值是另一种形式的赋值语句, 它允许同时更新多个变量的值,赋值语句右边的所有表达式将会先进行求值,然后再同一更新左边对应变量的值, 元祖复制可以是一系列琐碎的赋值更加紧凑

1
2
x, y = y, x
a[i], a[j] = a[j], a[i]

计算斐波纳契数列的第N个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func fib(n int) int {
x, y := 0, 1
for i := 0; i < n; i++ {
x, y = y, x +y
}
return x
}

func main() {
fmt.Println(fib(100))
}

多返回值

有些表达式会产生多个值,一个函数可能返回多个值, 左边变量的数目必须和表达式返回的值的个数一致, 如果我们不需要的值, 可以使用下划线空白标识符 _ 来丢弃。

可赋值性

赋值语句是显示的赋值形式,在程序中还有很多地方会发生隐式的赋值行为,例如函数调用会隐式将调用参数的值赋值给函数的参数变量,一个返回语句会隐式的将返回操作的值赋值给结果变量, 符合类型的字面量也会产生赋值行为

1
2
3
4
5
medals := []string{"gold", "silver", "bronze"}
medals[0] = "newGold"
medals[1] = "newSilver"
medals[2] = "newBronze"
fmt.Println(medals)

不管是隐式还是显示的赋值,赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型, 只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的,