Skip to main content

基础语法

参考: Go Tour

1 基础

基本程序结构

每个 Go 程序都由 Package 构成. 程序总是从 main 函数开始执行. 按约定包名和导入时候的路径最后一截相同.

通过 Imports 可以引入包, 从而使用包中的公共成员. 如果一个名字以大写开头, 则为 Exported name, 即可以被包外部访问.

Go 中可以使用带名字的返回值, 这样可以在函数体中直接使用它们. 然后单个的 return 即可返回.

基本数据类型

基本数据类型有如下:

bool

string

// 根据平台自己的属性, int 可能是 64 位, 也可能是 32 位的.

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
// represents a Unicode code point

float32 float64

complex64 complex128

如果变量没有赋初值, 则默认值如下:

  • 数值类型初值为: 0
  • 布尔类型初值为: false
  • 字符串类型初值: ""

类型转换写法:

i := 42
f := float64(i)
u := uint(f)

常量定义:

const number = 33

流程控制

循环:

// 一般的 for 循环
for i := 0; i < 10; i++ {
sum += i
}

// 上面的可以简写为如下:
for ; sum < 10; {
sum += sum
}

// while 循环的写法:
for sum < 1000 {
sum += sum
}

// 无限循环的写法:
for {
// ...
}

判断:

if x < 0 {
// ...
}

// 可以在 if 中执行一段语句后再判断:
if v := math.Pow(x, n); v < limit {
return v
}

// switch 语句和 if 类似, 也可以执行一段后再判断:
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}

// 可以通过无条件的 switch 来实现简化多个 if-else 的目的:
t := time.Now()
switch {// 这里类似 switch true
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}

// defer 延迟语句到 return 后执行, 调用的参数会立即计算, 但调用自己会到最后才执行
defer fmt.Println("returned")
// defer 实际是在调用栈中插入对应栈帧, 因此可以有多个 defer, 后进先出
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}

/* 上述的输出为:
counting
done
9
8
7
6
5
4
3
2
1
0
*/

指针

Go 里面有指针, 指针存放值的地址:

// 创建一个指针, 默认值是 nil
var p *int

// 使用 & 可以获取值的指针
i := 42
p = &i

// 使用 * 可以解引用
*p = 21

Struct

Struct 是一系列域的集合:

type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
}

Struct 的指针也是相同的 & 获取, 在指针上可以通过 . 访问成员, 不需要显式解引用.

结构体的字面值, 同时也是创建结构体的办法:

type Vertex struct {
X, Y int
}

var (
v1 = Vertex{1, 2} // 全部赋值, 类型为 Vertex
v2 = Vertex{X: 1} // 部分赋值, 部分默认
v3 = Vertex{} // 全部成员使用默认值
p = &Vertex{1, 2} // 全部赋值, 且 p 类型为 *Vertex
)

Array

数组固定长度的值的集合:

// 声明一个数组, 长度为 10, 里面的值都是 int 类型的, 且默认初始化为全 0
var a [10]int

// 显式的值
names := [4]string{"1", "2", "3", "4"}

// 显式赋值(通过下标)
var a [2]string
a[0] = "Hello"
a[1] = "World"

由于数组的长度也是它类型的一部分, 因此无法扩展长度. 如果要动态长度, 则使用 slice.

Slice

切片是可变长度的, 它是一个固定长度数组的某个区域的视图:

a[low : high] // low..<high

// 切片下标是前闭后开的, 下面这个实际是 a 数组下标从 1 到 3 的值的视图
a[1:4]

切片也可以脱离数组直接存在(实际是隐式地创建一个数组并引用它):

// 普通的数组
a := [3]bool{false, true, false}

// 切片字面量: 隐式地创建数组并引用它(切片)
s := []bool{false, true, false}

如果不指定下标, 则切片引用整个数组:

var a[10]int

// 如下是 a 的切片, 它们等价:
a[:]
a[0:]
a[:10]
a[0:10]

可以获取切片的长度和容量:

var a [10]int
s := a[:]

len(s) // 长度
cap(s) // 容量

可以创建不关联任何数组的切片, 此时它的值是 nil:

var s []int

创建动态长度数组的办法是利用 make 创建切片:

a := make([]int, 5)	// 长度是 5


b := make([]int, 0, 5) // 长度是 0, 容量是 5

可以使用 append 将值附加到切片, 如果原数组太小无法保存, 则会重新分配一个更长的数组并将切片指向新的数组:

s := []int{1, 2, 3}

append(s, 4)
append(s, 5, 6, 7)

同时 append 支持直接在 nil slice 上附加元素

Range

可以在 for 循环中使用 range:

array := [3]int{1, 2, 3}

// for 循环中使用 range
for index, value := range array {
// ...
}

// 可以利用 "_" 忽略 index 或 value
for _, value := range array
for index, _ := range array

Map

Map 是键值对的集合, Map 默认值是 nil, 可以通过 make 创建 Map:

// 声明一个 Map, 其默认值是 nil
var dict map[string]int
// 通过 make 创建 map
dict = make(map[string]int)

// 存放键值对
dict["hello"] = 3

Map 字面量:

// 字面量创建后仍然是可变的
dict := map[string]int{
"hello": 1,
"world": 2,
}

dict["cc"] = 999

Map 的增删改查:

// 增或者改
m[key] = value
// 删除
delete(m, key)
// 读取
val = m[key]
// 读的时候同时判断是否有这个键值对: 如果存在 exists 为 true, 且值对应读出, 否则为 false, 且值为对应类型的默认值.
value, exists := m[key]

函数变量以及闭包

go 函数可能是闭包, 即如果它引用了外部变量则就是:

// 函数作为参数:
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}

// 函数闭包作为返回值(闭包指可以捕捉它上下文变量的函数)
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

2 面向对象

方法(Method)

Go 中没有类的概念, 但可以在类型上定义方法(method):

// 使用 receiver 语法定义某个类型上的方法:
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X * v.X, v.Y * v.Y)
}

// 从功能上说, 上面的方法和下面的函数等价:
func Abs(v *Vertex) float64 {
return math.Sqrt(v.X * v.X, v.Y * v.Y)
}

如果要定义不在当前包中的类型的方法, 则需要使用类型别名, 否则无法进行:

type MyFloat float64

func (f MyFloat) Abs() float64 {
// ...
}

从上面也可发现, 能在非结构体上定义方法.

Receiver 如果是指针, 则可以改变原类型变量中的数据, 否则不能. 在调用时, go 可以自动将目标变量转换为指针进行调用. 同样地, 如果是在变量指针上调用非指针 receiver 的方法, go 也可以自动对指针进行解引用.

关于 Receiver 是否是指针, 考虑如下两个:

  • 是指针, 则可以改变原来的结构内部数据, 否则不能.
  • 使用指针可以避免每次都去复制传值

选择 receiver 类型的指导原则:

  1. 如果 receiver 是 map,func, chan,或是切片并且该方法不重新切片或重新分配, 则使用值 receiver.
  2. 如果该方法需要改变 receiver,则必须是指针.
  3. 如果包含sync.Mutex或类似的同步成员, 则必须是指针
  4. 如果是大型结构或数组,则指针接收器更有效
  5. 如果成员中也是指向可能改变的对象的指针,则更推荐指针(因为它将使读者更清楚地意图)
  6. 如果是一个小数组或结构,则更推荐值类型(例如,类似于 time.Time 类型),没有可变字段和没有指针,或者只是一个简单的基本类型,如 int 或 string,值接收者是有道理的,值接收器可以减少生成的垃圾量;如果将值传递给值方法,则可以使用堆栈上的副本而不是在堆上进行分配。(编译器试图避免这种分配,但它不能总是成功。)不要为此而选择值接收器类型而不先进行分析。
  7. 最后, 如果不满足上述, 则使用指针

接口(interface)

Go 中的接口是一系列行为的集合. 一个类型只要是实现了某接口中的所有方法, 那它就是该接口类型.

有一个要注意的地方是: 如果某类型通过指针 receiver 实现了接口中的方法, 则该类型本身没有实现该接口, 而是它的指针才实现了接口.

在 Go 中, 接口实现是隐式的, 没有任何类似 impl 的关键字, 这样的好处是将接口的实现和接口的声明完全解耦.

考虑一个接口变量 myInterface, 可以将它看成是 (value, type) 这样的结构, 即其中存放了该接口类型, 以及具体类型(concrete type).

如果像下面这样声明接口和接口对应的变量, 在调用时不会出现 nil 异常, 只是将对应具体类型设置为了 nil.

如果对为 nil 的具体类型解引用会出现空指针异常.

如果下方没有 i = t(即接口变量中没有 value 也没有 type), 则直接调用 i.M() 也会出现空指针异常.

可以使用 fmt.Printf("(%v, %T)", i, i) 打印接口的 value 和 type.

func main() {
var i I // 接口变量
var t *T // nil 的具体类型指针
i = t // 接口变量具体类型赋值
describe(i) // 其中对 i 进行了是否是 nil 的判断
i.M() // 在具体类型为 nil 的接口

i = &T{"hello"} // 挂上新的具体类型
describe(i)
i.M()
}

使用 interface {} 可以表示任意类型, 类似 Any, 比如:

var i interface{}

// fmt 的 printf 接收的就是一个任意类型的参数:
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}

如果要对接口进行类型转换(向下), 或者判断接口是某个类型, 可以使用类似 t := i.(T) 的方式:

var i interface{} = "hello"

s := i.(string)
fmt.Println(s)
// 如果 i 是 string 类型的, 则 ok 是 true
s, ok := i.(string)
fmt.Println(s, ok)
// 这里的 ok 是 false
f, ok := i.(float64)
fmt.Println(f, ok)
// 如果没有判断, 直接进行转换且失败的话, 会直接 panic
f = i.(float64) // panic
fmt.Println(f)

如果要对接口类型进行多轮判断, 可以使用 type switch:

func do(i interface{}) {
// 判断传入参数的类型并对应操作
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}

在 Go 中有一个常用的接口是 Stringer, 它在 fmt 包中定义, 如下所示:

type Stringer interface {
String() string
}

基本上所有类型都实现了它, 可以将该类型转换为对应的字符串.

下面的例子是在自定义类型上实现 Stringer:

type IPAddr [4]byte

func (addr IPAddr) String() string {
return fmt.Sprintf("%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3])
}

func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
// 输出为:
// loopback: 127.0.0.1
// googleDNS: 8.8.8.8

错误(Error)

error 是内置的接口, 如下所示:

type error interface {
Error() string
}

在 Go 中的错误处理就是判断 error 是否为 nil 来确认应该进行何种操作.

自定义的错误也只需要实现 Error 方法即可用于返回 error:

// 自定义错误类型
type MyError struct {
When time.Time
What string
}

// 注意这里是在错误类型指针上实现的 error 接口
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}

// 这里返回的是接口指针, 但接口上不需要标记为指针类型
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}

io 相关

在 Go 中定义了 Reader 接口用于表示流式 IO 的读取端, 其中有一个方法:

func (T) Read(b []byte) (n int, err error)

下面是一个简单的 Reader 使用示例, 如果 errorio.EOF 则停止读取:

func main() {
r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}

3 Go 泛型

泛型函数语法如下所示:

func Index[T comparable](s []T, x T) int

可以看到, 泛型参数在 [] 中包裹, 其中可以包含泛型约束.

泛型类型的语法如下:

// any 表示任意类型
type List[T any] struct {
next *List[T]
val T
}

4 并发(使用 Go routine)

Go Routing 是由 Go 运行时管理的轻量级并发工具, 在使用它时, 如果利用共享内存通信, 则必须进行同步, sync 包中有许多这样的工具.

使用 go 关键字启动一个协程:

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

为了解决协程之间的同步问题, Go 提出了一个新的观点: 利用通信来共享内存, 而非共享内存来进行通信, 因此, 有了 channel 这个工具.

Channel

Channel 是有类型的管道, 用于在协程间进行通信, 使用 <- 操作符将数据传入管道, 或将管道中数据提取到变量中, 如下所示:

// 使用 make 创建管道, 其中传递的消息类型是 int 的
ch := make(chan int)

// 将 v 送入管道
ch <- v

// 从管道接收数据并把数据存入到变量
v := <-ch

// 管道作为参数的写法如下, 传入一个名为 c 的 channel 传递 int 消息
func fibonacci(n int, c chan int) {
// ...
}

默认情况下, 不管是 send 还是 receive 时, 管道的一端都会在另外一端没有准备好时处于阻塞状态, 因此通常不需要进行额外的加锁等操作.

为了减少阻塞, 可以使用带缓冲区的管道, 只有当缓冲区满的时候才会阻塞发送端, 只有缓冲区空的时候才会阻塞接收端:

// 带缓冲区的管道
ch := make(chan int, 100)

发送端可以使用 close 对管道进行关闭操作, 表示没有更多数据了:

close(ch)

接收端则可以判断管道的状态, 来决定是否继续接收数据:

// 如果 isOpen 为 false, 则表示管道关闭了
v, isOpen := <-ch

需要注意, 在被关闭了的管道上发送消息, 会触发 panic.

关闭管道都是发送端负责的, 不要在接收端关闭管道.

可以使用 select 等待多个条件, 只有它里面某个条件可用后, 才会解除阻塞:

func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}

func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}

select 可以有 default 块, 如果其他 case 没有, 则会直接执行 default:

func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}

此外, go 提供了共享资源的互斥访问 mutex 等工具.