我的笨办法学 Go

在学习新语言这个事情上,笨办法往往是最高效的办法。效仿『笨办法学 Python』和『笨办法学 Ruby』,我来写一篇『笨办法学 Go』吧!


系列文章

Go 作为一门非常工程向的语言,虽然比较年轻,但是已经让我很开心了,我想,这应该是我最近会持续投入的一门语言。用这个系列跟大家分享我的学习之路。

番外

开始之前

在上一篇日志中我们大概对 Go 有了基本的认识,不过还不够系统,也远达不到掌握。所以就通过简单的例子和大量的编码练习来快速入门吧!因为 Go 的语法非常简洁明了,所以重点在于用习题突出一些特性,力求简短。

学新语言的关键点有三个:实践、能力培养和习惯养成。这些可能要比语言的语法本身更加重要!本日志基于 Go 1.6.3,仿照『笨办法学语言』系列的模式,用闯关的方式来学习。

具体的安装和基础知识可以参考 Go 快速入门

本文的配套代码可以在这里查阅。

习题 1 第一个程序

我们先从简单的弄起,按照惯例,是一个 Hello World。新建一个名为 ex1.go 的文件,内容为:

package main
import "fmt"
func main() {
// 一个变量声明
var name = "dawang"
// 也是一个变量声明
gender := "male"
fmt.Println("What a great day! Let's Go!")
fmt.Println(name + "'s gender is " + gender)
}

然后执行 go run ex1.go,可以看到结果为

$ go run ex1.go
What a great day! Let's Go!
dawang's gender is male

知识点

  • 注释用 ///* */,与 C/C++ 一样
  • 变量声明用 var:=
  • 输出语句需要引用 fmt 包
  • 函数的声明用 func,每个程序都需要一个 main 函数
  • 程序基本结构
  • 包 Package,每个程序都需要一个 main 包

习题 2 数字和数学运算

主要来了解一下基本的运算符使用和最关键的取地址操作,代码在 ex2.go 中,如下

package main
import "fmt"
func main() {
fmt.Println("Here are some basic fact:")
fmt.Println("2 hour has", 2*60 , "seconds", )
fmt.Println("Averge days in a month is", 365.0 / 12)
fmt.Println("Is it true that 3 + 2 < 5 - 7?", (3 + 2) < (5 - 7))
name := "wdxtub.com"
fmt.Println("The address for variable 'name' is", &name)
}

输出为

$ go run ex2.go
Here are some basic fact:
2 hour has 120 seconds
Averge days in a month is 30.416666666666668
Is it true that 3 + 2 < 5 - 7? false
The address for variable 'name' is 0xc42000a410

知识点

  • 遍历的声明
  • 数值大小的比较
  • 取变量的地址

习题 3 输入输出

输入输出主要涉及两个包,一个是 fmt,一个是 bufio。这里我们只做一个简单的任务,就是把用户输入的东西再输出出来,见 ex3.go,如下:

package main
import(
"fmt"
"bufio"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Type something: ")
if line, err := reader.ReadString('\n'); err != nil {
fmt.Println("Something Wrong")
} else {
fmt.Println("You just typed:", line)
}
}

结果为:

$ go run ex3.go
Type something: Today is a bad day?
You just typed: Today is a bad day?

知识点

  • bufio 包的简单使用

习题 4 命令行参数

很多时候我们把需要配置的部分设置成命令行参数是非常方便的选择,代码可参见 ex4.go,如下

package main
import (
"fmt"
"os"
"strings"
)
func main() {
name := "dawang" // 设置默认名字
if len(os.Args) > 1 {
// os.Args[0] 是程序名,后面才是自带的参数
name = strings.Join(os.Args[1:], " ") // 把所有除程序名之外的内容用空格拼起来
}
fmt.Println("Hello", name)
}

输出为

$ go run ex4.go da wang
Hello da wang

知识点

  • 包 os, strings 的简单尝试
  • 命令行参数的获取

习题 5 读写文件、条件判断和函数

这个例子会稍微复杂一点,我们会同时涉及读写文件、条件判断和函数(这样我们才能完成一些有趣的工作),不过因为 Go 本身的设计比较简洁,所以并不会像想象中那么复杂,我们做的工作就是把一个文件的内容复制到另一个文件中,具体见 ex5.go,如下

package main
import (
"bufio"
"fmt"
"io"
"log"
"os"
"path/filepath"
)
func main() {
infileName, outfileName, err := loadFromCmd()
if err != nil {
// 如果从命令行中读取参数错误,则打印错误信息并退出程序
fmt.Println(err)
os.Exit(1)
}
// 默认接收标准输入输出
infile, outfile := os.Stdin, os.Stdout
if infileName != "" {
// 处理输入文件不存在的情况
if infile, err = os.Open(infileName); err != nil {
log.Fatal(err)
}
defer infile.Close()
}
if outfileName != "" {
// 处理输出文件
if outfile, err = os.Create(outfileName); err != nil {
log.Fatal(err)
}
defer outfile.Close()
}
// 具体进行处理
if err = process(infile, outfile); err != nil {
log.Fatal(err)
}
}
// 从命令行读取参数
func loadFromCmd() (infileName, outfileName string, err error) {
if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help"){
err = fmt.Errorf("usage: %s [<] infile.txt [>] outfile.txt", filepath.Base(os.Args[0]))
return "", "", err
}
if len(os.Args) > 1 {
infileName = os.Args[1]
if len(os.Args) > 2 {
outfileName = os.Args[2]
}
}
if infileName != "" && infileName == outfileName {
log.Fatal("won't overwrite the infile")
}
return infileName, outfileName, nil
}
// 处理文件
func process(infile io.Reader, outfile io.Writer)(err error) {
reader := bufio.NewReader(infile)
writer := bufio.NewWriter(outfile)
defer func() {
if err == nil {
err = writer.Flush()
}
}()
eof := false
for !eof {
var line string
line, err = reader.ReadString('\n')
if err == io.EOF {
err = nil
eof = true
} else if err != nil {
return err
}
if _, err = writer.WriteString(line); err != nil {
return err
}
}
return nil
}

这里引入了几个我们之前没有见过的包,简单介绍一下:

  • bufio 带缓冲的 io 处理
  • io 底层 io 功能
  • log 日志相关

知识点

  • defer 语句会在包含它的函数执行完成之后执行(延迟执行),通常可以用作收尾工作
  • 利用函数分离长串代码,一个函数只做一个事情
  • 利用返回值来做检查

习题 6 枚举类型

与枚举类型配合有一个变量叫做 iota,每次调用的时候其值都会默认加一,下面是一个很有趣的例子,需要大家仔细感受一下,见 ex6.go,如下

package main
import(
"fmt"
"strings"
)
type BitFlag int
const (
Active BitFlag = 1 << iota // 1 << 0 == 1
Send // 隐式设置为 1 << iota,所以 1 << 1 == 2
Receive // 隐式设置为 1 << iota,所以 1 << 2 == 4
)
func (flag BitFlag) String() string {
var flags []string
if flag & Active == Active {
flags = append(flags, "Active")
}
if flag & Send == Send {
flags = append(flags, "Send")
}
if flag & Receive == Receive {
flags = append(flags, "Receive")
}
if len(flags) > 0 { // 这里,int(flag) 用于防止无限循环,至关重要
return fmt.Sprintf("%d(%s)", int(flag), strings.Join(flags, "|"))
}
return "0()"
}
func main() {
var flag = Active | Send
fmt.Println(BitFlag(0), Active, Send, flag, Receive, flag|Receive)
}

运行结果为

$ go run ex6.go
0() 1(Active) 2(Send) 3(Active|Send) 4(Receive) 7(Active|Send|Receive)

知识点

  • iota 的用法
  • 如何给指定类型重写 String() 方法来自定义输出

习题 7 值与引用

关于值和引用前面已经说过,这里需要注意的是数组在 Go 中是按值传递的,所以传递一个大数组的代价非常大,不过我们通常用切片来进行代替,传递切片的成本与字符串差不多。

这里我们分别用两种方式来传递参数,具体见 ex7.go,如下

package main
import (
"fmt"
)
func main() {
i := 9
j := 5
product := 0
fmt.Println("Origin Data:", i, j)
swapAndProduct1(&i, &j, &product)
fmt.Println("Swapped1:", i, j, product)
i = 9
j = 5
i, j, product = swapAndProduct2(i, j)
fmt.Println("Swapped2:", i, j, product)
}
func swapAndProduct1(x, y, product *int) {
if *x > *y {
*x, *y = *y, *x
}
*product = *x * *y
}
func swapAndProduct2(x, y int) (int, int, int) {
if x > y {
x, y = y, x
}
return x, y, x * y
}

输出为

$ go run ex7.go
Origin Data: 9 5
Swapped1: 5 9 45
Swapped2: 5 9 45

知识点

  • 两种交换方式看起来结果一样,不过第二种不是原地交换,这里需要注意一下
  • 不过第二种不需要操心太多的 *,可读性更好

习题 8 数组与切片

Go 语言的数组是一个定长的序列,其中的元素类型相同,数组的创建语法和切片有些类似,注意要区别一下

// 数组声明
[length]Type
[N]Type{value1, value2, ..., valueN}
[...]Type{value1, value2, ..., valueN}
// 切片声明
make([]Type, length, capacity)
make([]Type, length)
[]Type{}
[]Type{value1, value2, ..., valueN}

看到了吗,唯一的区别其实在于 [] 中的内容,如果有,则是数组,如果没有,则是切片。具体的例子可见 ex8.go,如下:

package main
import(
"fmt"
)
func main() {
// 声明数组
cities := [...]string{"Shanghai", "Guangzhou", "Shenzhen", "Beijing"}
fmt.Printf("%-8T %2d %q\n", cities, len(cities), cities)
// 声明切片
s := []string{"A", "B", "C", "D", "E", "F", "G"}
t := s[:5]
u := s[3:len(s)-1]
fmt.Println(s, t, u)
u[1] = "x"
fmt.Println(s, t, u)
// 遍历切片
for i, letter := range s {
fmt.Println(i, letter)
}
}

结果为

$ go run ex9.go
[4]string 4 ["Shanghai" "Guangzhou" "Shenzhen" "Beijing"]
[A B C D E F G] [A B C D E] [D E F]
[A B C D x F G] [A B C D x] [D x F]
0 A
1 B
2 C
3 D
4 x
5 F
6 G

知识点

  • 数组和切片的区别
  • 数组和切片的基本使用

习题 9 映射

简单来说就是字典,用来保存键值对,先来说说基本用法

  • m[k] = v 基本的赋值,会覆盖
  • Delete(m, k) 从 m 中删除 k,如果 k 不存在,啥都不发生
  • v := m[k] 取值,如果 k 不存在,v 为 0
  • v, found := m[k] 取值,如果存在,值保存在 v 中,found 为 true,反之,v 为 0 且 found 为 false
  • len(m) 返回 m 中键值对的数量

创建映射的语法为

// 创建映射
make(map[KeyType]ValueType, initialCapacity)
make(map[KeyType]ValueType)
map[KeyType]ValueType{}
map[KeyType]ValueType{key1: value1, key2: value2, ..., keyN: valueN}

具体的例子可参见 ex9.go,如下

package main
import (
"fmt"
)
func main() {
massForPlanet := make(map[string]float64)
massForPlanet["Mercury"] = 0.06
massForPlanet["Venus"] = 0.82
massForPlanet["Earth"] = 1.00
massForPlanet["Mars"] = 0.11
fmt.Println(massForPlanet)
massForPlanet["Jupiter"] = 317.82
massForPlanet["Saturn"] = 95.16
massForPlanet["Uranus"] = 14.371
massForPlanet["Neptune"] = 17.147
fmt.Println(massForPlanet)
big := "Jupiter"
fmt.Println(big, massForPlanet[big])
}

知识点

  • 映射的基本使用

习题 10 Switch 语句

Go 中有两种类型的 switch 语句,可以用表达式或者类型本身来进行分支跳转,另外 Go 的 switch 语句是不会自动向下执行的,如果需要的话,就得加上 fallthrough 关键词。Go 语言的表达式 switch 语句非常有用,很多时候可以代替 if 语句,还更加紧凑。

两种类型的示例可见 ex10.go,如下:

package main
import(
"fmt"
)
func main() {
// 表达式 switch
var suffix = ".gz"
var result string
switch suffix {
case ".gz":
result = "This is a Gzip file"
case ".tar", ".tar.gz", ".tgz":
result = "This is a Tar file"
case ".zip":
result = "This is a Zip file"
}
fmt.Println(result)
// 类型 switch
classifier(5, -17.9, "LOL", nil, true, complex(1, 2))
}
func classifier(items...interface{}) {
for i, x := range items {
switch x.(type) {
case bool:
fmt.Printf("Param #%d is a bool\n", i)
case float64:
fmt.Printf("Param #%d is a float64\n", i)
case int, int8, int16, int32, int64:
fmt.Printf("Param #%d is a int\n", i)
case uint, uint8, uint16, uint32, uint64:
fmt.Printf("Param #%d is an unsigned int\n", i)
case nil:
fmt.Printf("Param #%d is nil\n", i)
case string:
fmt.Printf("Param #%d is a string\n", i)
default:
fmt.Printf("Param #%d is unknown type\n", i)
}
}
}

运行结果为

$ go run ex10.go
This is a Gzip file
Param #0 is a int
Param #1 is a float64
Param #2 is a string
Param #3 is nil
Param #4 is a bool
Param #5 is unknown type

知识点

  • 可变参数列表的声明
  • interface{} 空接口的使用
  • 两种类型的 switch 语句
  • for 循环语句

习题 11 并发

Go 语言中可以轻松创建 goroutine,可以利用 channel 进行类型安全的单向/双向通信或同步 goroutine,因为其机制是传递数据而非共享数据。这里我们用一个坐标系转换的例子来说明,具体可见 ex11.go,如下

package main
import(
"bufio"
"fmt"
"math"
"os"
"runtime"
)
type polar struct{
radius float64
theta float64
}
type cartesian struct {
x float64
y float64
}
var prompt = "输入一个弧长 radius 和角度 theta,例如 12.5 90,或 %s 来退出"
func init() {
if runtime.GOOS == "windows" {
prompt = fmt.Sprintf(prompt, "Ctrl+Z, Enter")
} else {
prompt = fmt.Sprintf(prompt, "Ctrl+D")
}
}
func main() {
questions := make(chan polar)
defer close(questions)
answers := createSolver(questions)
defer close(answers)
interact(questions, answers)
}
func createSolver(questions chan polar) chan cartesian {
answers := make(chan cartesian)
go func() {
for {
polarCoord := <-questions
theta := polarCoord.theta * math.Pi / 180.0
x := polarCoord.radius * math.Cos(theta)
y := polarCoord.radius * math.Sin(theta)
answers <- cartesian{x, y}
}
}()
return answers
}
const result = "Polar radius=%.02f theta=%.02f degree -> Cartesian x=%.02f y=%.02f\n"
func interact(questions chan polar, answers chan cartesian) {
reader := bufio.NewReader(os.Stdin)
fmt.Println(prompt)
for {
fmt.Printf("Radius and angle: ")
line, err := reader.ReadString('\n')
if err != nil {
break
}
var radius, theta float64
if _, err := fmt.Sscanf(line, "%f %f", &radius, &theta); err != nil {
fmt.Println(os.Stderr, "invalid input")
continue
}
questions <- polar{radius, theta}
coord := <-answers
fmt.Printf(result, radius, theta, coord.x, coord.y)
}
fmt.Println()
}

具体的输出为

$ go run ex11.go
输入一个弧长 radius 和角度 theta,例如 12.5 90,或 Ctrl+D 来退出
Radius and angle: 12.5 90
Polar radius=12.50 theta=90.00 degree -> Cartesian x=0.00 y=12.50
Radius and angle: ^D

知识点

  • 通过 struct 定义结构体
  • runtime 包提供运行时控制,比方说确定程序运行的平台
  • func init() 不能显式调用,会在执行 main() 之前自动执行
  • 通道的行为跟先进先出队列一致,可以堵塞,能够以此来进行同步
  • goroutine 执行机制的简单理解

习题 12 Select 语句

在一个 Select 语句中,Go 语言会按顺序从头至尾评估每一个发送和接收语句,如果有一条可以执行,那就执行,不然如果没有 default 语句,就阻塞,下面是一个简单的模拟骰子的例子,具体见 ex12.go,如下:

package main
import(
"fmt"
"math/rand"
)
func main() {
channels := make([]chan bool, 6)
for i := range channels {
channels[i] = make(chan bool)
}
go func() {
for {
channels[rand.Intn(6)] <- true
}
}()
for i := 0; i < 36; i++ {
var x int
select {
case <-channels[0]:
x = 1
case <-channels[1]:
x = 2
case <-channels[2]:
x = 3
case <-channels[3]:
x = 4
case <-channels[4]:
x = 5
case <-channels[5]:
x = 6
}
fmt.Printf("%d ", x)
}
fmt.Println()
}

执行结果为

$ go run ex12.go
6 4 6 6 2 1 2 3 5 1 3 2 1 6 5 3 4 6 6 3 6 1 3 5 4 2 2 5 1 4 2 1 6 6 4 3

知识点

  • 生成随机数的方法
  • select 语句的基本用法

习题 13 错误与异常

通过内置的 panic()recover() 函数,Go 提供了一套异常处理机制,虽然可以以此实现通用的异常处理机制,但并不推荐这样做。

Go 将错误和异常两者区分对待。错误是指可能出错的东西,程序需要以优雅的方式将其处理。而异常是指『不可能』发生的事情。

Go 语言中处理错误的惯用法是将错误以函数或者方法最后一个返回值的形式将其返回,并总是在调用它的地方检查返回的错误值。

本例来自 Golang 语言基础之九: error, panic, recover,见 ex13.go,如下:

package main
import(
"fmt"
)
// 最简单的例子
func SimplePanicRecover() {
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic info is: ", err)
}
}()
panic("SimplePanicRecover function panic-ed!")
}
// 当 defer 中也调用了 panic 函数时,最后被调用的 panic 函数的参数会被后面的 recover 函数获取到
// 一个函数中可以定义多个 defer 函数,按照 FILO 的规则执行
func MultiPanicRecover() {
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic info is: ", err)
}
}()
defer func() {
panic("MultiPanicRecover defer inner panic")
}()
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic info is: ", err)
}
}()
panic("MultiPanicRecover function panic-ed!")
}
// recover 函数只有在 defer 函数中被直接调用的时候才可以获取 panic 的参数
func RecoverPlaceTest() {
// 下面一行代码中 recover 函数会返回 nil,但也不影响程序运行
defer recover()
// recover 函数返回 nil
defer fmt.Println("recover() is: ", recover())
defer func() {
func() {
// 由于不是在 defer 调用函数中直接调用 recover 函数,recover 函数会返回 nil
if err := recover(); err != nil {
fmt.Println("Panic info is: ", err)
}
}()
}()
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic info is: ", err)
}
}()
panic("RecoverPlaceTest function panic-ed!")
}
// 如果函数没有 panic,调用 recover 函数不会获取到任何信息,也不会影响当前进程。
func NoPanicButHasRecover() {
if err := recover(); err != nil {
fmt.Println("NoPanicButHasRecover Panic info is: ", err)
} else {
fmt.Println("NoPanicButHasRecover Panic info is: ", err)
}
}
// 定义一个调用 recover 函数的函数
func CallRecover() {
if err := recover(); err != nil {
fmt.Println("Panic info is: ", err)
}
}
// 定义个函数,在其中 defer 另一个调用了 recover 函数的函数
func RecoverInOutterFunc() {
defer CallRecover()
panic("RecoverInOutterFunc function panic-ed!")
}
func main() {
SimplePanicRecover()
MultiPanicRecover()
RecoverPlaceTest()
NoPanicButHasRecover()
RecoverInOutterFunc()
}

执行结果为

$ go run ex13.go
Panic info is: SimplePanicRecover function panic-ed!
Panic info is: MultiPanicRecover function panic-ed!
Panic info is: MultiPanicRecover defer inner panic
Panic info is: RecoverPlaceTest function panic-ed!
recover() is: <nil>
NoPanicButHasRecover Panic info is: <nil>
Panic info is: RecoverInOutterFunc function panic-ed!

知识点

  • panic 的机制
  • recover 的机制

习题 14 Interface

关于接口的内容比较特别,因为 Go 中没有类的概念,而且是通过 interface 类型转换支持在动态类型语言中常见的 鸭子类型 达到运行时多态的效果。

更多详情请参考 Golang 语言基础之八: interface,代码在 ex14.go 中(同样来自 Golang 语言基础之八: interface),如下

package main
import "fmt"
// 定义接口类型 PeopleGetter 包含获取基本信息的方法
type PeopleGetter interface {
GetName() string
GetAge() int
}
// 定义接口类型 EmployeeGetter 包含获取薪水的方法
// EmployeeGetter 接口中嵌入了 PeopleGetter 接口,前者将获取后者的所有方法
type EmployeeGetter interface {
PeopleGetter
GetSalary() int
Help()
}
// 定义结构 Employee
type Employee struct {
name string
age int
salary int
gender string
}
// 定义结构 Employee 的方法
func (self *Employee) GetName() string {
return self.name
}
func (self *Employee) GetAge() int {
return self.age
}
func (self *Employee) GetSalary() int {
return self.salary
}
func (self *Employee) Help() {
fmt.Println("This is help info.")
}
// 匿名接口可以被用作变量或者结构属性类型
type Man struct {
gender interface {
GetGender() string
}
}
func (self *Employee) GetGender() string {
return self.gender
}
// 定义执行回调函数的接口
type Callbacker interface {
Execute()
}
// 定义函数类型 func() 的新类型 CallbackFunc
type CallbackFunc func()
// 实现 CallbackFuncExecute() 方法
func (self CallbackFunc) Execute() { self() }
func main() {
// 空接口的使用,空接口类型的变量可以保存任何类型的值
// 空格口类型的变量非常类似于弱类型语言中的变量
var varEmptyInterface interface{}
fmt.Printf("varEmptyInterface is of type %T\n", varEmptyInterface)
varEmptyInterface = 100
fmt.Printf("varEmptyInterface is of type %T\n", varEmptyInterface)
varEmptyInterface = "Golang"
fmt.Printf("varEmptyInterface is of type %T\n", varEmptyInterface)
// Employee 实现了 PeopleGetter 和 EmployeeGetter 两个接口
varEmployee := Employee{
name: "Jack Ma",
age: 50,
salary: 100000000,
gender: "Male",
}
fmt.Println("varEmployee is: ", varEmployee)
varEmployee.Help()
fmt.Println("varEmployee.name = ", varEmployee.GetName())
fmt.Println("varEmployee.age = ", varEmployee.GetAge())
fmt.Println("varEmployee.salary = ", varEmployee.GetSalary())
// 匿名接口对象的使用
varMan := Man{&Employee{
name: "Nobody",
age: 20,
salary: 10000,
gender: "Unknown",
}}
fmt.Println("The gender of Nobody is: ", varMan.gender.GetGender())
// 接口类型转换,从超集到子集的转换是可以的
// 从方法集的子集到超集的转换会导致编译错误
// 这种情况下 switch 不支持 fallthrough
var varEmpInter EmployeeGetter = &varEmployee
switch varEmpInter.(type) {
case nil:
fmt.Println("nil")
case PeopleGetter:
fmt.Println("PeopleGetter")
default:
fmt.Println("Unknown")
}
// 使用 “执行回调函数的接口对象” 执行回调函数
// 这种做法的优势是函数显式地 “实现” 特定接口
varCallbacker := CallbackFunc(func() { println("I am a callback function.") })
varCallbacker.Execute()
}

运行结果为

$ go run ex14.go
varEmptyInterface is of type <nil>
varEmptyInterface is of type int
varEmptyInterface is of type string
varEmployee is: {Jack Ma 50 100000000 Male}
This is help info.
varEmployee.name = Jack Ma
varEmployee.age = 50
varEmployee.salary = 100000000
The gender of Nobody is: Unknown
PeopleGetter
I am a callback function.

知识点

  • interface 的定义和使用

总结

本文通过 14 个简单的例子来说明了 Go 的基本用法,如果能大致理解,就可以开始具体的探险了,让我们边学边练,搞一个练手项目吧。

参考链接

捧个钱场?