Go 语言快速入门

已经了解Go语言的读者跳过这章。其他编程语言的读者可以了解Go语言主要功能,方便阅读书中的代码。

语法

变量

// 声明变量类型
var a int       // 默认是 0
var b string    // 默认是 ""
// 赋值
a = 123
b = "hi"
// 声明变量并初始化
var c int = 123
d := "hi"       // 声明时自动推导类型
// 多个变量同时赋值
a, c = c, a

变量没初始化,结果一定是用0字节表示的值(包括整数0,false,空字符串,nil 等)。

语句

// if-else 没有括号
if a != c {
    // ...
} else {
    // ...
}
// switch 不会像C一样自动跳到下一个 case
switch a {
case 123:
    // ...
case 456:
    // ...
default:
    // ...
}
// 在 if 里声明变量并使用
if err := f(); err != nil {
    // ...
}
// 循环
for {
    if xxx {
        break
    }
    if yyy {
        continue
    }
}
for i := 0; i < N; i++ {
    // ...
}
for zzz {
    // ...
}

函数

func f(a int, b string) error {
    return nil
}
// 多个返回值
func g() (string, bool) {
    return "hi", false
}
// 多个返回值赋值
s, b := g()
// 返回值可以有变量名
func h() (s string, b bool) {
    s = "hi"
    b = false
    return
}
// 如果要返回错误,`error` 放在最后
func foo() (string, error) {
    // ...
}

result, err := foo()
if err != nil {
    // handle error
} else {
    // use `result`
}

数据类型

数值类型

// 固定长度
bool, byte
int8, int16, int32, int64
uint8, uint16, uint32, uint64
float32, float64
// 长度与系统相关:32位系统32位整数,64位系统64位整数
int
uint
// 类型转换
a := 1  // int
var b int64
b = a           // 编译不过
b = int64(a)    // 类型转换

指针/引用

a := 1
var ptr *int    // 声明指针类型
ptr = &a        // 获取地址
b := *ptr       // 读取指针数据(dereference)

因为Go有GC,所以指针使用有限制。比如不能像C一样随便对指针做运算,不能将指针以其他类型储存,等等。不过只要不用 unsafe 就不会遇到这些问题。

struct, method

type C struct {
    foo string,
    bar int,
}
// struct 初始化
c := C{foo: "hi", bar: 123}
// 内容
c.bar = 456
bar1 := c.bar
// 指针
pc := &c
bar2 := (*pc).bar
bar3 := pc.bar      // 不需要 dereference 的语法
// 普通函数
func foo1(c *C) {
    c.foo = "hi"
}
// 方法(method),自身以指针传递
func (c *C) foo2() {
    c.foo = "hi"
}
// 方法,自身参数传值(value)
func (c C) bar() {
    println(c.bar)
}
// 调用普通函数
foo1(&c)
// 调用方法
c.foo2()    // 传指针
c.bar()     // 传值

固定大小数组

var a [2]byte       // 2个元素的数组,初始化为0
var b [2][4]byte    // 2个元素,每个元素是4个元素的数组
// 赋值
b = [2][4]byte{
    {0, 0, 0, 0},
    {0, 0, 0, 0},
}
b[1][3] = 123

字符串

字符串只是一串 byte,便没有文本编码的的概念,可以和 []byte 互相转换。组成如下:

type string struct {
    ptr *byte   // 数据
    len int     // 长度
}
s := "asdf"     // {ptr: "asdf", len: 4}
len(s)          // 4
// 字符串不能修改
s[0] = 'b'      // 编译不过
// 引用一部分字符串,不会 copy 数据
s[2:4]          // {ptr: "df", len: 2}
// 字符串拼接
s = "hi" + s

slice

Go 里的 slice 其实是两种不相关的功能,它可以是下面其中一种:

  1. 表示数组的一个范围,包含指向数组的指针+范围的长度。
  2. 可以增长的动态数组。相当于 C++ vector,Python list

两种 slice 的组成都相当于下面这个 struct:

type sliceT struct {
    ptr *T  // 第0个元素的地址
    len int // 引用的长度
    cap int // 数组大小(从 ptr 开始算)
}

两种 slice 学习时经常混淆,下面我们分开简绍。

a. 作为数组引用的 slice

var arr [8]byte = [8]byte{0, 1, 2, 3, 4, 5, 6, 7}
// 引用一个数组
s1 := arr[1:5]      // {ptr: &arr[1], len: 4, cap: 7}
// 遍历
for i := 0; i < len(s1); i++ {
    println(s1[i])  // 1 2 3 4
}
// 另一种遍历语法
for i := range s1 {
    println(s1[i])  // 1 2 3 4
}
for i, v := range s1 {
    println(v)      // 1 2 3 4
}
// 越界,会导致 panic
v := s1[4]

// 修改 slice 的范围,返回一个新的 slice
s2 := s1[2:3]       // {ptr: &arr[3], len: 1, cap: 5}
for _, v := range s2 {
    println(v)      // 3
}

slice 越界检查:

正常的用法,只会使用 s[i:j] 来缩小引用的范围,而不会增加或移动到原来的范围之外。所以 slice 作为数组引用时,只使用 len 而不会使用 cap

// 额外语法
s[:]    // s[0:len(s)]
s[:j]   // s[0:j]
s[i:]   // s[i:len(s)]

b. 作为动态数组的 slice

// 动态分配一个长度为2的数组,初始化为0,返回 slice
arr1 := make([]byte, 2)     // {ptr: 0x100, len: 8, cap: 2}
// 动态分配一个长度为2的数组,返回的 slice 长度为0
arr2 := make([]byte, 0, 2)  // {ptr: 0x200, len: 0, cap: 2}
// 末尾追加元素,返回长度+1的 slice,赋值给原来的 slice。
arr2 = append(arr2, 123)    // {ptr: 0x200, len: 1, cap: 2}
arr2 = append(arr2, 123)    // {ptr: 0x200, len: 2, cap: 2}
// cap 不够时自动重新分配数组
arr2 = append(arr2, 123)    // {ptr: 0x300, len: 3, cap: 8}
// append 多个元素
arr2 = append(arr2, 2, 3)   // {ptr: 0x300, len: 5, cap: 8}

只有 slice 作为动态数组时才可以 append。作为数组引用的 slice 不能 append,因为会修改数组里的数据。

常见 slice 操作

copy(dst, src)                      // slice 间复制数据
s1 = slices.Clone(s)                // 新分配一个 slice 并复制数据
s = slices.Concat(s1, s2, s2)       // 拼接 slice
s = slices.Delete(s, i, j)          // 删除 s[i:j]
s = slices.Insert(s, i, e1, e2)     // 在位置 i 插入元素
idx = slices.Index(s, v)            // 查找元素 v 的位置
slices.Sort(s)                      // 排序(inplace)

作为数组引用的 slice 仅仅是一个引用,不一定可以通过 slice 去修改所指向的数组,绝对不可以 append。而作为动态数组的 slice 有对数组的 ownership,可以做任何操作。动态数组可以当成成数组引用来用,反之则不行。读者可以思考:上面的这些函数中的参数、返回值分别是哪种 slice?

interface

一个 interface 是一套约定方法(method)。任何实现了这些方法的类型都可以转换成 interface,哪怕只是碰巧匹配上了。

type Reader interface {
    Read(p []byte) (n int, err error)
}
// 满足 interface 的类型
type T struct { /* ... */ }
func (s *T) Read(p []byte) (n int, err error) { /* ... */ }
// 参数使用 interface
func useReader(r ReaderWriter) { /* ... */ }

obj := &T{}         // 具体类型
useReader(obj)      // 转换成 interface

通过 interface 调用方法,不需要依赖具体的类型,类似与 OOP 里的 abstract class。一个 interface 由2个指针组成:

type interfaceFoo struct {
    object   uintptr    // 具体类型的数据
    typeInfo uintptr    // 对应的类型信息
}

将某个值转换成 interface,就是引用这个值+相应的类型信息。调用方法时能通过类型信息找到方法的实现,还能判断具体的类型并取回原来的数据。

没初始化的 interface 里这两个引用都是 nil,这跟把 nil 赋值给 interface 是一样的,但跟把某个值为 nil 的指针转换成 interface 是不一样的。

判断原来的类型

var anything interface{}
anything = 1
anything = "asdf"

// 转换回原来的具体类型
integer, ok := anything.(int)   // ok = false
str, ok := anything.(string)    // ok = true, 可以使用 str 变量
// 用 switch 判断类型
switch origin := anthing.(type) {
case int:
    integer = origin            // origin 是成功转换过的类型
case string:
    str = origin
}