Golang

给 Python 程序员的 Go 语言教程

楔子

最近读到一亩三分地上一篇讲 Facebook 架构和国内对比的文章,感觉自己真是井底之蛙。头脑中一些架构方面的概念和 Status of the Art 的理念还相去甚远,迫切想要进一步了解一些先进知识。比如说,以前觉得 git flow 这个概念还挺不错的,实践了半年,发现 develop 分支完全是多余的;以前觉得每个项目分一个仓库方便管理,现在觉得 monorepo 似乎更好一点。另外就是对“互联网时代的 C 语言” Golang 有点想了解一下。

一年前休假的时候看了几眼 Golang,感觉还不错,但是想实际写点什么的时候发现 GOPATH 这个设计真是奇葩至极。而现在我的思想已经完全倒向 Monorepo 了,那么 GOPATH 也就看起来很可爱了,Golang 看起来也就很可爱了,也就决定再翻翻 Go 语言的书吧,以后说不定会写点儿什么呢。

忘了在哪里看过一句话:人的知识像一个网络,新学到的知识只有和已有的知识关联起来才能真正记得住、记得牢,否则的话像是一个孤岛的新知识很快就会被忘记了,于是就有了本文。

需要注意的是,本文并不是一个简单的语法对比,倘若只是语法的话,直接把代码一列其实就差不多了。除去语法之外,本文还在设计理念上做了一些对比。以下为目录。(没有链接的表示还没有写,敬请期待)

目录

  1. 语法基础
    1. 类型与变量
    2. 数据结构与控制语句
    3. 函数定义
    4. 面向对象
    5. 错误处理
    6. 包管理
  2. 并发与网络
    1. 并发机制
    2. Http 请求
  3. 常用标准库
    1. 时间解析
    2. 文件 IO
    3. 正则表达式
    4. 数学函数
    5. 定时机制

写这些文章的另一个目的就是对 Python 中相关的知识做个梳理,以便以后再学习新的语言(比如 rust, clojure)能够更有条理。

Ref

  1. Python slice notation. https://stackoverflow.com/questions/509211/understanding-slice-notation/50929x
  2. How to get type of go. https://stackoverflow.com/questions/20170275/how-to-find-a-type-of-an-object-in-go
  3. Golang online repo. https://repl.it/languages/go
  4. A tour of go. https://tour.golang.org/moretypes/6
  5. golang vs python. http://govspy.peterbe.com/#lists
  6. https://www.353.solutions/py2go/index.html

Go 语言 Map 实战

相比 Rust 中尚未实现 IndexMut 的 Hash 类型来说,Go 中的 Map 实现度可以说是非常高了。

基本用法

Map 的类型是 map[KeyType]ValueType 的。也就是由 Key 类型和 Value 类型同时决定的。声明一个 Map:

var m map[string]int

不过一般很少有人这样写,还是生命并赋值比较常见,还是使用我们的 make 函数:

m = make(map[string]int)
commits := map[string]int{
    "rsc": 3711,
    "r":   2138,
    "gri": 1908,
    "adg": 912,
}

基本上除了 slice,map 和 function 以外,都可以做 map 的键。

赋值

m["route"] = 66

获取值

i := m["route"]  // 如果 route 不存在,那么获取的就是对应的零值
j := m["non-exist"]

删除值

delete(m, "route")  // 如果不存在的话,也不会抛出异常。这里和 Python 不一样

判断是否存在

i, ok := m["route"]

遍历

for key, value := range m {
    fmt.Println("Key:", key, "Value:", value)
}

并发性

map 不是线程安全的。

Go语言读写文件相关函数对比

字符串和 bytes 操作

Go 语言中的字符串并没有很多原生方法,需要使用 stringsbytes 模块来操作。strings 模块
假定字符串是 utf-8 编码的。

字符串操作的函数

函数签名 说明
func Compare(a, b string) int 按照字典序比较两个字符串的大小
func Contains(s, substr string) bool 是否包含字符串
func ContainsAny(s, chars string) bool 是否包含字符串中的任意一个字符
func Count(s, substr string) int 计算子串出现的次数
func EqualFold(s, t string) bool 是否在 Unicode Case Fold 意义下等价
func Fields...(s string) []string 返回按照 unicode.IsSpace 中的字符分隔出来的 slice
func HasPrefix(s, prefix string) bool 匹配前缀
func HasSuffix(s, suffix string) bool 匹配后缀
func Index...(s, substr string) int 子串在字符串中的位置,没找到返回 -1
func Join(a []string, sep string) string 连接字符串
func LastIndex...(s, substr string) int 反向查找
func Map(mapping func(rune) rune, s string) string 对每一个 rune 应用mapping函数,如果 mapping 返回负值就忽略掉这个字符
func Repeat(s string, count int) string 把源字符串重复 n 次
func Replace(s, old, new string, n int) string 替换源字符串中的值
func Split...(s, sep string) []string 按照 sep 作为分隔符,把字符串分开
func Title(s string) string 转换成 Title Case
func ToLower...(s string) string 转换成小写
func ToUpper...(s string) string 转换成大写
func Trim...(s string, cutset string) string 删掉两边的字符

字符串相关 struct

strings.Builder 用来拼接字符串,类似于 Java 中的 StringBuilder。strings.Builder
包含了 Write 方法,也就是说实现了 io.Writer 的接口,因此可以直接向其中写入内容。

var b strings.Builder
for i := 3; i >= 1; i-- {
    fmt.Fprintf(&b, "%d...", i)
}
b.WriteString("ignition")
fmt.Println(b.String())

3...2...1...ignition
函数签名 说明
func (b *Builder) Grow(n int) 预分配内存,避免多次分配
func (b *Builder) Len() int 长度
func (b *Builder) Reset() 重置回 0
func (b *Builder) String() string 转换成字符串
func (b *Builder) Write(p []byte) (int, error) 写入 bytes
func (b *Builder) WriteString(s string) (int, error) 写入 string

strings.Reader 实现了 io.Reader、io.ReaderAt, io.Seeker, io.WriterTo, io.ByteScanner, and io.RuneScanner 等一系列的接口。主要用来把字符串转换成一个满足对应接口的类型。传递个接受对应 interface 的函数。

函数签名 说明
func NewReader(s string) *Reader 从指定字符串构建一个 Reader

strconv

strconv 包包含了把一些字符串转换相关的函数。

函数签名 说明
func ParseBool(str string) (bool, error) 从字符串中读取 bool
func ParseFloat(s string, bitSize int) (float64, error) 从字符串中读取浮点数
func ParseInt(s string, base int, bitSize int) (i int64, err error) 从字符串中读取整形
func QuoteRuneToASCII(r rune) string 把unicode字符转换成\uxxxx的形式
func Unquote(s string) (string, error) 从各种编码解析出unicode字符
func Atoi(s string) (int, error) 字符串转变成 Int,注意不是Int64
func Itoa(i int) string 数字转变成字符串

bytes 的操作

在 Go 语言中,字符串实际上是一种只读的 byte slice,一些适用于 string 的操作也适用于 byte slice。因此 Go 语言还实现了一个包,用来以类似的方式操作 byte slice

bytes 包中的函数基本都是和 strings 包中对应的,除了把参数 string 换成了 []byte 之外。

bytes.Bufferbytes.Reader 有点类似于 strings.Builderstrings.Reader 这两个类型,实现了一大堆io的接口,也是利用 bytes 进行 IO 的

比如说,从 io.Reader 中读取 string,可以利用 Buffer.ReadFrom

buf := new(bytes.Buffer)
buf.ReadFrom(yourReader)
s := buf.String()
函数签名 说明
func NewBuffer(buf []byte) *Buffer 从指定 bytes 构建一个 Buffer
func NewBufferString(s string) *Buffer 从指定 string 构建一个 Buffer
func (b *Buffer) Read(p []byte) (n int, err error) 这个方法实现了 io.Reader
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) 从一个 io.Reader 中读取内容到自己的 buffer 中
func (b *Buffer) UnreadByte() error 回退一个byte
func (b *Buffer) Write...(p []byte) (n int, err error) 写入到自己的 buffer 中
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error) 把自己的 buffer 写入到另一个 writer 中

有趣的是,ReadFrom 和 Write 函数两个看起来意思是相反的。实际上都是读取并写入自己的 buffer 中。在 Go 语言中,ReadFrom 和 WriteTo 两个方法才是相反的

bufio

顾名思义,bufio 就是 buffered io 的缩写,也就是有缓存的 io。bufio 包主要提供了三个类型,Reader, WriterScanner。这三个类型都接受 io.Reader/Writer 作为参数,同时又实现了这两个接口。

bufio.Reader

Reader 是比较底层的实现

函数签名 说明
func NewReader...(rd io.Reader) *Reader 返回一个增加了缓存的 io.Reader
func (b *Reader) Buffered() int 返回缓存的大小
func (b *Reader) Discard(n int) (discarded int, err error) 抛弃 n 个字节
func (b *Reader) Peek(n int) ([]byte, error) 返回 n 个字节,但是不会读取
func (b *Reader) Read...(p []byte) (n int, err error) 读取

bufio.Scanner

Scanner 有点类似于 scanf,通过设置不同的 SplitFunc,得到不同的 token。

bufio 中内置了几个 SplitFunc,ScanBytes, ScanLines, ScanRunes, ScanWords用来分别扫描得到 字节、行、Rune、单词。

Scannner 适合对普通文件的分隔,如果需要过多的底层控制,应该使用 bufio.Reader

Programs that need more control over error handling or large tokens, or must run sequential scans on a reader, should use bufio.Reader instead.

Scanner 的默认 buffer 大小是 bufio.MaxScannerTokenSize = 64K如果文件过大,可能会出现 bufio.Scanner: token too long 的报错。可以换用更大的 buffer 或者使用 Reader

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
for scanner.Scan() {
    // do your stuff
}

Scanner 的使用模式

scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
    fmt.Println(scanner.Text()) // Println will add back the final '\n'
}
if err := scanner.Err(); err != nil {
    fmt.Fprintln(os.Stderr, "reading standard input:", err)
}
函数签名 说明
func NewScanner(r io.Reader) *Scanner 生成一个新的
func (s *Scanner) Buffer(buf []byte, max int) 指定 Scanner 的新Buffer
func (s *Scanner) Bytes() []byte 以 bytes 形式返回当前扫描到的 token
func (s *Scanner) Scan() bool 扫描下一个并返回是否结束
func (s *Scanner) Split(split SplitFunc) 指定分隔函数,这个函数名字起得太简短了
func (s *Scanner) Text() string 以 string 形式返回当前扫描到的 token

文件 IO

io.Readerio.Writer。这两个是两个特别重要的 interface。一般来说凡是可以抽象为输入的 IO 操作都会使用 io.Reader。凡是可以抽象为输出的 IO 操作都会使用 io.Writer。

io/ioutil

对于配置文件等等比较小的常规文件,一般来说我们可以使用 io/ioutil 包中的辅助函数操作就好了,比较快捷方便。

函数签名 说明
func NopCloser(r io.Reader) io.ReadCloser 把 io.Reader 包装成一个 io.ReadWriter
func ReadAll(r io.Reader) ([]byte, error) 读取所有字符,成功的话 err == nil
func ReadDir(dirname string) ([]os.FileInfo, error) 读取当前目录的所有文件
func ReadFile(filename string) ([]byte, error) 读取文件的所有内容
func TempDir(dir, prefix string) (name string, err error) 创建临时目录
func TempFile(dir, prefix string) (f *os.File, err error) 创建临时文件
func WriteFile(filename string, data []byte, perm os.FileMode) error 写入文件

对于比较大的文件,直接使用 ioutil.ReadFile 读到内存里显然是不现实的,这时候应该使用 os 模块中的函数。

文件操作

其他语言中一般统一通过 open(filename, rw) 这个函数来打开文件,而 golang 中有所
不同,一般来说是通过 os.Open(filename) 打开文件用于读取,使用 os.Create(filename)
打开文件用于写入。

Go语言处理 CSV 文件

在 Go 语言中可以使用 encoding/csv 包来处理 csv 文件。

csv 包中主要有两个 struct,Reader 和 Writer。Reader 从一个 io.Reader
中读取每一行的内容,同时提供了一些设置的选项。Writer 用来写入 csv 文件。

Reader

r := csv.NewReader(strings.NewReader(in))

for {
    record, err := r.Read()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(record)
}
函数签名 说明

func NewReader(r io.Reader) *Reader
func (r *Reader) Read() (record []string, err error) | 返回 []string 类型数据
func (r *Reader) ReadAll() (records [][]string, err error) | 直接返回所有数据

Writer

records := [][]string{
    {"first_name", "last_name", "username"},
    {"Rob", "Pike", "rob"},
    {"Ken", "Thompson", "ken"},
    {"Robert", "Griesemer", "gri"},
}

w := csv.NewWriter(os.Stdout)

for _, record := range records {
    if err := w.Write(record); err != nil {
        log.Fatalln("error writing record to csv:", err)
    }
}

// Write any buffered data to the underlying writer (standard output).
w.Flush()

if err := w.Error(); err != nil {
    log.Fatal(err)
}
函数签名 说明
func NewWriter(w io.Writer) *Writer

func (w *Writer) Error() error
func (w *Writer) Flush()
func (w *Writer) Write(record []string) error
func (w *Writer) WriteAll(records [][]string) error

Go 语言处理大文件

日常开发中,总有一些文件或者数据过于大而无法放到内存中。在 Python 中,我们可以构造生成器,从而在迭代过程中动态生成元素。而在 Go 语言中没有也不鼓励使用迭代器这种模式,但是观察标准库和一些常见的库,可以看到一些常用的模式。

使用 slice

把所有数据放到一个 slice 中

使用闭包(closure)

package main
import "fmt"
func main() {
    iter := NewEven()
    fmt.Println(iter())
    fmt.Println(iter())
    fmt.Println(iter())
    gen = nil // release for garbage collection
}
func NewEven() func() int {
    n := 0
    // closure captures variable n
    return func() int {
        n += 2
        return n
    }
}

使用有状态的自定义类型的方法

如果不想使用 closure 来保存状态的话,可以使用一个类型来表示。

package main
import "fmt"
func main() {
    gen := even(0)
    fmt.Println(gen.next())
    fmt.Println(gen.next())
    fmt.Println(gen.next())
}
type even int
func (e *even) next() int {
    *e += 2
    return int(*e)
}

不过一般来说复杂一点的对象都会使用 Next() 和 Value() 两个方法,方便在 for 循环中使用,其中 Next() 函数返回是否

package main

import "fmt"

type Range struct {
    start int
    stop int
    step int
    current int
}

func (r *Range) Next() bool {
    return r.current < r.stop
}

func (r *Range) Value() int {
    c := r.current
    r.current += r.step
    return c
}

func main() {
    r := Range{0, 100, 10}
    for r.Next() {
        fmt.Printf("value is %d", r.Value()
    }
}

channel

package main

import "fmt"

func Range(start int, stop int, step int) <-chan int {
    ch := make(chan int)
    go func() {
        for i := start; i < stop; i+= step {
            ch <- i
        }
        close(ch) // Remember to close or the loop never ends!
    }()
    return ch
}

func main() {
    for i := range Range(0, 100, 10) {
        fmt.Printf("value is %d", i)
    }
}

性能

一般来说,使用 channel 和 closure 的性能不好,而使用有状态的类型性能更好一点。

https://stackoverflow.com/questions/14000534/what-is-most-idiomatic-way-to-create-an-iterator-in-go

Go 语言初体验

安装与配置

安装 go 很简单,在 mac 上直接使用 brew install go 就可以了。注意的是需要设定GOPATH这个环境变量,如果不设定。默认就到了 ~/go 这个目录。GOPATH 中也可以设置多个目录,默认安装包会安装到第一个路径。不过现在

export GOPATH=$HOME/lib:$HOME/go

语法和风格

Golang 的整个语法还是极其简单的,基本是 C 和 Python 的混合体。Go 语言详尽地规定了代码的风格,所以就不用为了大括号究竟是放在哪儿而开始圣战了。Go 语言对程序员的约束很强,不容易犯错。

Go 语言官方提供了一个工具 goimports 用来管理 import 语句,还不错。

import进来的包必须使用,声明的变量也必须使用。import 语句必须在 package 语句后面。虽然有大括号,但是大括号的位置也是指定的。

类型

Go 语言有四种类型:基础类型、聚合类型、引用类型和接口类型。

var i int  // 声明一个变量
i = 42  // 赋值

或者直接使用简写:

i := 42  // 声明并赋值

几乎所有的值提供了默认都会初始化到对应的零值,即使没有初始化,不少函数处理 nil 的时候也是按照零值处理,这样避免了好多无谓的异常抛出。在go中也不会遇到len(None)这种问题,即使len(nil)也会返回0。

基础类型

整型包括了: int, int8/16/32/64, uint, uint8/16/32/64 几种类型。另外 byte 是 uint8 的别名,rune 是 int32 的别名。

比如定义了 size 函数,虽然返回的合法值都是 uint范围内的,但是可能使用 -1 等表示非法值。所以除非要使用比特位操作,尽量使用 int,而不是 uint。

浮点型也包括了:float32 和 float64 两种类型,注意没有单独的 float/double 类型。其中 math.MaxFloat32 和 math.MaxFloat64 表示最大值。默认的浮点数是 float64。

Go 中还包含了复数类型:complex64 和 complex128,不过就像 Python 中的复数类型一样,从来没用过。

字符串

值得一提的是 Go 中的字符串,实际上是一个 byte 的只读数组,如果使用索引访问的话,是按照 byte 为单位来访问的。但是在打印和 range 的时候会直接按照 utf-8 解码输出。如果需要按照 rune(Go 语言对 unicode code point 的称呼)来遍历,需要使用 unicode/utf8 这个包中的函数。

即使字符串不是 utf-8的,或者不管怎样用错了编码,至少不会panic,而python中时不时就会抛出UnicodeDecodeError 。

不用思考蛋疼的 Unicode 问题,不过虽然 Go 的 string 是 utf-8 的,但是使用下标访问的是字节,而使用 for range 访问的又是 rune。

Go 中的字符串,实际上是一个 byte 的只读数组,如果使用索引访问的话,是按照 byte 为单位来访问的。但是在打印和 range 的时候会直接按照 utf-8 解码输出。如果需要按照 rune(Go 语言对 unicode code point 的称呼)来访问,需要使用 unicode/utf8 这个包中的函数。

复合类型

Go 中内置的复合类型主要有三种:Array, Slice 和 Map。分别对应了其他语言中的定长数组、边长数组(切片)和字典。

像是 C 语言一样,Go 中也有定长数组,var a [3]int

在 Go 中经常作为变长数组使用的是 Slice。Slice 指的是一个数组的一个切片。在 Go 语言中,默认的函数调用都是值传递的,但是 Slice 做参数的时候传递的是一个 Slice Header,也就相当于按照引用传递。

Map 类型是引用类型,也就是函数调用的时候是按照引用传递的。nil map 可以像空map一样使用,但是插入的时候会 panic,因为没有给他分配内存,所以 map 类型一般使用 make 初始化。

m := make(map[string]int)

数组

数组的类型是 [x]type,比如 [1]int[2]int 是不同的类型

var a [3]int = [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [...]int{1,2,3}
d := [...]{99: -1}

如果数组的元素是可以比较的(comparable),那么数组也是也以比较的,也就是可以用来做 map 的 key。

默认情况下,数组是按照值传递的,这一点和 C 语言默认传递指针不一样。

向 nil 的 slice 中直接存入元素是不合法的,向 nil 的 map 中直接存入元素也是不合法的。所以最好使用 make 来声明map。

切片——变长数组

像是好多动态语言一样,Go 也支持切片操作:a[i:j]。不过和其他语言不一样的是,Go 的切片操作符产生了新的类型:切片,而不是数组。切片是对源数组的部分元素的一个引用。他们指向的是同一个内存单元。

切片可以当做一个变长数组使用,实际上我们不会每次都构造一个数组,然后获取切片,而是直接使用切片字面量。

a = []int{1, 2, 3}

和数组字面量很像,区别是没有指定长度。

和数组不同的一点是,切片是不能比较的,不管他内部的元素是什么。

Slice 底层引用了数组,但是并不会自动扩容,因此想往其中添加元素需要注意不能越界,提前扩充容量。可以使用 make 函数提前指定大小,或者使用 append 函数动态扩展切片大小。

make([]T, len)
make([]T, len, cap)

var x []int
x = append(x, 1)

Map

Go 语言也原生支持字典。

a := make(map[string]int)
a := map[string]int {
    "alice": 12,
    "bob": 12,
}

Go 语言和其他语言不同的是,尝试访问不存在的键也不会报错,而是会返回对应类型的零值,不过可以采用两个参数来验证是否存在这个键

v := a["foo"]
v, ok := a["foo"]

if v, ok : a["foo"]; ok {
    fmt.Print(v)
}

控制语句

go 还从 C 中 继承了 if (p = fopen("xxx", "w")) != NULL 这种在把赋值语句写在if中的写法,不过好在 Go 语言中可以写做两句。

if err := r.ParseForm(); err != nil {
      //...
}

另外 switch 语句默认就会 break 了,而不是 fall through 了。

循环语句很有意思,Go 语言直接把 while 关键字扔掉了,只用 for 本身就够了

for {
// ...
}

就相当于其他语言中的 while(true) 了。

不管是 Python 中的 for…in… 还是 JavaScript 中的 for…of… 语句,迭代数组和字典的时候多少感到一些不一致。在 Go 语言中,还是比较统一的,每次迭代都会返回两个值:key, value。

迭代 slice

words := []string{"a", "b"}
for i, word := range words {
    fmt.Printf("%d -> %s", i, word)
}

迭代 map

words := make(map[string]string)
words["a"] = "a"
words["b"] = "b"
for k, v := range words {
    fmt.Printf("%s -> %s", k, v)
}

goroutine

goroutine 相对于 Python 的 coroutine 的好处就在于它是抢占的,不用主动交出。

Python 的 coroutine 需要特别小心不要调用阻塞性的函数,比如 time.sleep,而要使用asyncio.sleep,所以写起来不是非常得方便。Python必须使用 await 来显式交出控制,而 Go 中则没有这种限制。

Timers and tickers

Timers 定义在你在未来的某个时间想要去做一次某件事。而 Tickers 则是定期执行某一个动作。这两个有点像是 js 里面的 setTimeout 和 setInterval 两个函数。

defer 实际上就相当于 C++ 中的RAII,和Python中的 with 语句

Go 的类型总体来说,和 Python 的 duck type 有点像,而和 Java 严格的继承则是完全背道而驰的。

发生赋值时候会不会检查类型呢?

interface{}

io

一般读取统一从 io.Reader 类型中读取

记得一定要使用append函数,而不要直接在slice的结尾通过下标添加字符,这样可能会panic

函数应该接受 interface 作为参数,并使用 struct 作为返回值。

目前为止有几个不爽的地方

  1. nil 字典不能直接赋值,但是 nil slice 可以 append
  2. interface 的 nil 始终没有搞明白。empty slice(a[0:0]) 和 nil 也不一样
  3. 没有一个统一的包管理工具,刚刚花一下午时间学习了 dep,号称是官方的试验,结果又看到一篇文章说 vgo 要取代 dep,WTF
  4. defer 执行的地方是函数的结尾,而不是块的结尾

数据类型

s := "世界"

len(s)  // -> 6,utf-8 编码的中文一般是3个字节。
utf8.RuneCountInString(s)  // -> 2

// 使用 range 遍历
for r := range s {
    fmt.Printf("%s", r)
}

// 使用 DecodeRuneInString
for i := 0; i < len(s) {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%d\t%c\n", i, r)
}

// 全部转换成 rune
r := []rune(s)
fmt.Printf("%x\n", r)

即使字符串不是 utf-8的,或者不管怎样用错了编码,至少不会 panic,而是把不能解析的字符替换成 \uFFFD。而python中时不时就会抛出UnicodeDecodeError,非常蛋疼。

Go 语言中的字符串函数并没有作为 string 类型的方法,而是单独放在了 strings 包中,比如 strings.Splitstrings.Join 函数。因为 []byte 类型和 string 类型也比较类似,因此 strings 包中提供的方法,在 bytes 包中也可以找到类似的。

复合类型

Struct

定义一个 struct 如下:

type A struct {
    foo int
    bar int
}

Go 中也有指针的概念,不过没有 -> 这个关键字,统一使用 . 操作。如果一个 struct 中的所有字段都是可以比较的,整个 struct 就也是可以比较的。

Struct 嵌入

Go 语言支持一种特殊的骚操作,叫做 struct 嵌入。这样一个 struct 就可以直接调用被嵌入的 struct 的属性和方法,听起来有点像继承,而且的确实现了继承的功能,但的确不是继承,而是复合。

Go 语言是一门有 gc 的语言,所以所有变量的生命周期并不是严格限定于作用域的,由编译器来决定使用栈上还是堆上的空间。

在 Go 语言中没有 private 和 public 这些关键字。如果变量名字是大写字母开头的,那么就是导出的,如果是小写字母开头的,那么就是包内私有的变量。

参考:

https://stackoverflow.com/questions/18058164/is-a-go-goroutine-a-coroutine

Go 语言数据库教程

在学习 Go 的过程中重新思考了数据库相关的一些知识,之前认为数据库的驱动就是应该有一个 conn 对象表示连接, 然后再有一个 cursor 对象来具体操作。但是 Go 完全没有这么来,而是直接生成一个 db 对象来操作,开始觉得不适应,然而后来我也实在想不起来为什么需要用两个对象了。

另外,为什么要用 ORM 呢?之前用 Django 的ORM比较多,因为生成后台非常方便,而且自己对 SQL 也不是很熟悉,对数据库的操作基本上都在使用这个ORM。然而,现在感觉到如果想要自己的代码性能比较高的话,自己手工写 SQL 几乎是不可避免的,而且 SQL 其实也没有那么吓人。

Go语言中的 database/sql 包提供了一个数据库的访问接口,但是对于不同的数据库,还需要不同的驱动。

一些常见的数据库的驱动参见这里:http://golang.org/s/sqldrivers

连接数据库

db, err := sql.Open(driver, dataSourceName)

这里值得注意的有两点

  1. Go语言中不像其他语言一样,除了 connection 对象之外还有 cursor 对象,golang 里面很简单,直接用db对象操作就好了。
  2. Open 函数并不会去真的链接数据库,直到第一条语句才会去链接,如果想检测是否连接成功,可以使用:
if err := db.Ping(); err != nil {
  log.Fatal(err)
}

执行语句

使用 db.Exec 方法

result, err := db.Exec(
    "INSERT INTO users (name, age) VALUES ($1, $2)",
    "gopher",
    27,
)

result 类型定义如下:

type Result Interface {
    LastInsertId()
    RowAffcted()
}

查询

rows, err := db.Query(
    "SELECT NAME FROM  users WHERE age = $1",
    age
)

if err != nil {
    log.Fatal(err)
}

for rows.Next() {
    var name string
    if err := rows.Scan(&name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s is %d old\n", name, age)
}

if err := rows.Err(); err != nil {
    log.Fata(err)
}

Row 有一个方法 Scan,而 Rows 中常用的两个方法是 Next 和 Err

如果查询结果只有一列的话,使用 QueryRow 方法。

var age int64
row := db.QueryRow("SELECT age FROM users WHERE name = $1", name)
err := row.Scan(&age)

当然像其他所有的语言一样,可以预编译语句然后执行。

age := 27
stme, err := db.Prepare("SELECT name FROM users WHERE age = $1")
if err != nil {
    log.Fatal(err)
}
rows, err := stmt.Query(age)
defer stmt.Close()

事务(Transaction)

tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
...
tx.Commit() 
// or tx.Rollback()

处理 null

如果一列可能为 null 的话,那么传递个Scan的参数就不应该是对应的基础类型,而应该是对应的包含null的复合类型

var name NullString
err := db.QueryRow("SELECT name FROM names WHERE id = $1", &name)
if name.Valid {
   // 使用 name.String 访问
} else {
  //
}

除此之外还包含了其他几种 Null 复合值,NullBool、NullFloat64、NullInt64。可以使用对应的参数访问

使用 sqlite3

% go get github.com/mattn/go-sqlite3
import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
)

db, err := sql.Open("sqlite3", "./foo.db")

sqlx

待续