最近为了学习Go语言,选择了从《Go In Action》这本书入手。书里的知识简单易懂,适合新手学习,这里大致记录一下阅读笔记,内容也大多取自于该书。

1. 打包和工具链

1.1 包

Go语言本身就有很好的代码复用性。所有的Go工程都会组织若干组文件,每组文件被称为一个包。包就是一个很小的复用单元。

包命名惯例:使用包所在目录的名字。如net/http

和所有语言一样,main()函数是go程序的入口。所有的Go语言编译的可执行程序都必须有一个名叫main的包。

1.2 import

导入包的关键字:import。如下:

import (
	"fmt"
    "strings"
)

go查找包的顺序,由GOROOT和GOPATH决定。如

/usr/local/go/src/pkg/net/http    -- GOROOT
/home/workspace/go/src/net/http   -- GOPATH
/home/myproject/src/net/http	  -- GOPATH

一旦编译器找到一个满足import语句的包,就停止进一步查找。

**远程导入 ** : 迎合分布式版本控制系统,使用go get获取包,如下:

// 项目里面导入包
import "github.com/alecthomas/log4go"
// 使用goget来导入包
go get "github.com/alecthomas/log4go"

命名导入: 在import语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。主要是为了解决包重名的问题。如下:

package main 
import (
	"fmt"
    myfmt "mylib/fmt"
)
func main() {
    fmt.Println("hello, Go!")
    myfmt.Println("hello, Go!")
}

另外,有时候需要导入一个包,但是不需要引用这个包的标志符。这种情况可以使用空白标识符_来重命名这个导入。如下:

import (
	_ "fmt"
)

1.3 init函数

初始化函数,每个包可以包含任意多个init函数,在程序执行开始的时候被调用。

1.4 Go tools

go命令行工具—— go。go这个指令提供了很多功能,如下:

Go is a tool for managing Go source code.

Usage:

	go command [arguments]

The commands are:

	build       compile packages and dependencies
	clean       remove object files and cached files
	doc         show documentation for package or symbol
	env         print Go environment information
	bug         start a bug report
	fix         update packages to use new APIs
	fmt         gofmt (reformat) package sources
	generate    generate Go files by processing source
	get         download and install packages and dependencies
	install     compile and install packages and dependencies
	list        list packages
	run         compile and run Go program
	test        test packages
	tool        run specified go tool
	version     print Go version
	vet         report likely mistakes in packages

Use "go help [command]" for more information about a command.

Additional help topics:

	c           calling between Go and C
	buildmode   build modes
	cache       build and test caching
	filetype    file types
	gopath      GOPATH environment variable
	environment environment variables
	importpath  import path syntax
	packages    package lists
	testflag    testing flags
	testfunc    testing functions

Use "go help [topic]" for more information about that topic.

Go buile 和 run是最常见的操作,分别是编译和运行go项目。可以以最简单的helloword为例,来练习这些操作。

// 代码清单
package main
import "fmt"
func main() {
	fmt.Println("hello, Go!")
}

// 以下为终端命令
➜  go go build helloworld.go
➜  go ./helloworld
hello, Go!

go vet 帮助开发人员检测代码的常见错误。通常,能捕获的错误类型如下:

  • Printf类函数调用时,类型匹配错误的参数
  • 定义常用的方法时,方法签名的错误
  • 错误的结构标签
  • 没有指定字段名的结构字面量

如下所示,如果变量没有指定类型,go vet可以找出错误。

str = fmt.Println("hello, Go!")
// 以下为终端命令
➜  go go vet helloworld.go
# command-line-arguments
./helloworld.go:7:2: undefined: str
./helloworld.go:7:19: multiple-value fmt.Println() in single-value context

go fmt格式化代码。fmt工具会将开发人员的代码布局成和Go源代码类似的风格,不用再为了大括号是不是要放到行尾,或者用tab还是空格来做缩进而纠结。

go doc 获取go帮助文档,直接指定包来获取tar包的帮助文档

➜  go go doc tar
package tar // import "archive/tar"

Package tar implements access to tar archives.

Tape archives (tar) are a file format for storing a sequence of files that
can be read and written in a streaming manner. This package aims to cover
most variations of the format, including those produced by GNU and BSD tar
tools.

const TypeReg = '0' ...
var ErrHeader = errors.New("archive/tar: invalid tar header") ...
type Format int
    const FormatUnknown Format ...
type Header struct{ ... }
    func FileInfoHeader(fi os.FileInfo, link string) (*Header, error)
type Reader struct{ ... }
    func NewReader(r io.Reader) *Reader
type Writer struct{ ... }
    func NewWriter(w io.Writer) *Writer

或者从浏览器查看go的文档。

godoc -http=:6060

然后在浏览器键入localhost:6060。

1.5 依赖管理

管理工具:

  • godep
  • vender
  • Gopkg.in
  • gb Go社区成员开发的全新构建工具。

这里额外补充一下使用gb来管理go工程依赖的相关知识。

// 安装gb工具
go get github.com/constabulary/gb/...
// 以下可以放在.bash_profile中
unalias gb // 于git branch的简写冲突
export PATH=$PATH:$GOPATH/bin

这样就可以使用gb命令了,如下:

➜  bin gb
gb, a project based build tool for the Go programming language.

Usage:

        gb command [arguments]

The commands are:

        build       build a package
        doc         show documentation for a package or symbol
        env         print project environment variables
        generate    generate Go files by processing source
        info        info returns information about this project
        list        list the packages named by the importpaths
        test        test packages

Use "gb help [command]" for more information about a command.

Additional help topics:

        plugin      plugin information
        project     gb project layout

Use "gb help [topic]" for more information about that topic.

这里我们简要的搭建一个工程来试一试gb。参考(Golang包依赖管理工具gb):

  1. 创建gb项目目录结构
   cd ~/helloworld
   mkdir -p src/helloworld
   mkdir -p vendor/src
   vim src/helloworld/main.go
   // 编写helloworld代码
   package main
   
   import (
       "fmt"
       "net/http"
   
       "github.com/tabalt/gracehttp"
   )
   
   func main() {
       http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
           fmt.Fprintf(w, "hello world")
       })
   
       err := gracehttp.ListenAndServe(":8080", nil)
       if err != nil {
           fmt.Println(err)
       }
   }
  1. 添加第三方依赖包:使用gb vendor
   gb vendor fetch github.com/tabalt/gracehttp
   // 目前为止,整个目录的架构如下
   ├── src
   │   └── helloworld
   │       └── main.go
   └── vendor
       ├── manifest
       └── src
           └── github.com
               └── tabalt
                   └── gracehttp
                       ├── README.md
                       ├── gracehttp.go
                       ├── gracehttpdemo
                       │   └── main.go
                       └── server.go
  1. 编译可执行程序
   gb build helloworld
   ./bin/helloworld
   // 此时打开一个终端,并执行curl http://127.0.0.1:8000/,会得到如下输出
   hello world

gb还有其他的一直指令,如下:

        build       build a package 编译包
        doc         show documentation for a package or symbol  显示文档
        env         print project environment variables 打印项目的环境变量
        generate    generate Go files by processing source 处理源代码生成go文件
        info        info returns information about this project 显示项目信息
        list        list the packages named by the importpaths 显示项目下的所有包
        test        test packages 执行测试

gb vendor获取依赖,其功能列表如下

        fetch       fetch a remote dependency 获取一个远程依赖
        update      update a local dependency 更新一个本地依赖
        list        lists dependencies, one per line 每行一个列出所有依赖
        delete      deletes a local dependency 删除一个本地依赖
        purge       purges all unreferenced dependencies 清除所有未引用的依赖
        restore     restore dependencies from the manifest 从manifest清单文件还原依赖

2. 数组、切片和映射

数组是切片和映射的基础数据结构。

2.1 数组

在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连 续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。

所以说,GO内置数组长度固定,内存连续分配,更容易计算索引,快速迭代数组。

声明一个数组后,数据类型和数组长度就不能改变了。声明后就会初始化,数组也是,数组内每个元素都初始化为对应类型的零值。

var array [5]int
// Go自动计算声明数组的长度
array := [...]int{10,20,30,40,50}
// 声明数组,并给其中特定索引的元素赋值,其余保持零值
// 如下,索引为1和3的元素赋值为10,20
array := [5]int{1:10, 3:20}

使用数组的方法

array := [5]int(10,20,30)
array[2]=20 // 修改索引为2的元素的值
// 声明数组指针
array := [5]*int{1: new(int), 1: new(int)}
// 为索引为0和1的元素赋值
*array[0] = 10
// 索引为0的元素是一个指针,该指针指向整数10

只有数组类型相同(长度和每个元素的类型均相同)的两个数组才能相互赋值。

多维数组:组合多个数组来创建多维数组。

// 声明一个二维整形数组
var array [4][2]int
// 初始化与一个二维数组
array := [4][2]int{{1,2},{2,3},{3,4},{4,5}}
// 使用二维数组
array[0][0] // 值为1

在函数间传递数组时,最好使用指针,避免用值得方式传递。

2.2 切片

切片:动态数组,可以按需自动增长和缩小。便于使用和管理数据集合。

切片的底层内存也是在连续块中分配,所以切片可以获得索引、迭代以及为垃圾回收优化的好处

咦,和STL的vector有点类似,后面继续看看具体实现。

切片的数据结构有三个字段,分别如下:

  • 指向底层数组的指针
  • 切片访问的元素格式,即长度
  • 切片允许增长到的元素个数,即容量

创建和初始化:内置的make函数可以创建和初始化切片。

// 创建一个字符串切片
slice := make([]string, 5) // 长度和容量都是5个元素,类型为字符串
// 指定长度和容量
slice := make([]int, 3, 5) // 长度为3,容量为5

需要注意如下几点:

  • 声明切片的长度不能超过容量
  • 访问切片的索引不能超过其长度,否则会报错,index out of range

声明数组和声明切片的不同:

// 创建有三个元素的整形数组
array := [3]int{10,20,30}
// 创建长度和容量都是3的整形切片
slice := []int{10,20,30}
// []运算符里指定了值就是创建数组,反之为创建切片

创建nil和空切片

var slice []int // 底层数组的指针为nil,长度和容量均为0
// 空切片
slice := make([]int,0) // 使用make创建空的整型切片,地址针织不为nil,长度和容量均为0
// 使用切片字面量创建空的整型切片
slice := []int{}

切片的使用:基本使用和数组类似,可以使用索引。这里关注一下,使用切片来创建切片。

// 创建切片
slice := []int{1,2,3,4,5}
// 创建新切片
newslice := slice[1:3]
// newslice和slice共享一段底层数组,slice的指针指向1,newslice的指针指向2
// slice 指针 长度5 容量5
//   	  ↓ 
// 		| 1 | 2 | 3 | 4 | 5 |
// 			  ↑
// newslice  指针 长度2,容量4
// 切片长度和容量计算方式:
// 对底层数组容量是k的切片slice[i:j]来说
// 长度: j - i
// 容量: k - i

Warning:由于是指针,修改newsclice的元素值会导致slice中的值也相应被修改。

切片动态增长:append函数。

  • 当容量足够时,添加到数组后面,
  • 当容量不足时,当切片容量小于1000个元素时,总是增长为原来的两倍;一旦元素个数超过1000,容量的增长因子会设为1.25,每次以25%增长。

两倍增长和STL:VECTOR相似,具体增长方式可能会随着语言的演化有所改变。

创建切片时限制容量 : 创建切片时,可以限制容量,如下:

slice := []int{1,2,3,4,5,6}
newsclie := slice[1,3,4]
// newslice长度为2,容量为4,{2,3,4}
// 计算方式:i:j为前闭后开区间
// 对于 slice[i:j:k]
// 长度: j – i
// 容量: k – i

迭代切片:关键字range,和for配合来迭代切片

slice := []int{1,2,3,4}
// 迭代每一个元素,
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n",index, value)
}
// Output:
// Index: 0 Value: 1
// Index: 1 Value: 2
// Index: 2 Value: 3
// Index: 3 Value: 4

多维切片 :和数组一样,组合多个切片形成多维切片。

// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为 20 的元素 
slice[0] = append(slice[0], 20)	

在函数间传递切片:切片尺寸很小,可以看成是指针的传递。

2.3 映射

映射是一种数据结构,用于存储一系列无序的键值对。映射的底层实现采用了散列表。有机会去看看Go map的实现,这里就不再赘述了。

翻译真的是醉了,这货明明就是一个hashmap,翻译成中文看着真难受。

创建和初始化:主要是make和直接声明。

dict := make(map[string]int) // 键是string,值是int
// 使用键值对直接初始化
dict := map[string]string{"Red":"#da1337", "Orange":"#e95a22"}

字符串切片不能作为map的键,但是可以作为map的值。

dict := map[[]string]int{}
// 会报错Compiler Exception: invalid map key type []string
dict := map[int][]string{}
// 正常使用

使用映射:直接当做键值对使用。

// 创建map
colors := map[string]string{}
colors["Red"] = “#da1337”
// 创建一个值为nil的map
colors :=map[string]string
colors["Red"] = “#da1337” // 报错panic: runtime error: assignment to entry in nil map
// 判断键存在
value, exists := colors["Blue"]
if exists {}  // exists返回键存在与否
value := colors["Blue"]
if value != "" {} // 值为空代表不存在
// 删除
delete(colors, "Red")

函数间传递映射:函数中传递mao也是相当于指针的方式,并不会复制底层的数据结构。因此在函数内做的修改在函数外也有效,这个要值得注意一下。

3. Go语言的类型系统

Go语言是一种静态类型的编程语言。变量类型不能动态获取,编译时就需要知道每个值的类型。

3.1 用户自定义的类型

使用struct关键字来创建一个结构类型

// 新建一个结构类型
type user struct {
    name string 
    email string
    ext   int
}
// 声明一个user类型的变量
var bill user
// 初始化一个user类型的变量
lisa := user {
    name : "Lisa",
    email : "[email protected]", 
    // 未指定的变量按照类型的默认值给定
}
// 类型允许嵌套
type admin struct {
    person user
    age int
}
// 初始化的时候可以直接在内部用字面量进行初始化
fred := admin {
    person: user {
        name : "fred",
    	email : "[email protected]"
    },
    age: 20
}
// 类型不同的变量之间不能赋值,下面这种特殊情况也不行
type Duration int64
var dur Duration
dur = int64(1000) // 编译会报错。cannot use int64(1000) (type int64) as type Duration in assignment

3.2 方法

方法即函数,只是在声明时,在关键字func和方法名之间增加了一个参数。

package main 
import "fmt"
type user struct {
    name string 
    email string
}
// notify使用值接收的方式,实现了一个方法
func (u user) notify {
    fmt.Printf("sending User Email To %s<%s>\n", u.name, u.email)
    u.name = "jasper" // 值传递的话改变值只在当前函数有效,函数外部无效
}
// notify使用指针接收的方式,实现了一个方法
func (u *user) changeEmail(name string) {
    u.email = email
}
func main {
	// Values of type user can be used to call methods
	// declared with a value receiver.
	bill := user{"Bill", "[email protected]"}
	bill.notify()

	// Pointers of type user can also be used to call methods
	// declared with a value receiver.
	lisa := &user{"Lisa", "[email protected]"}
    lisa.notify() // (*lisa).notify() Go底层做了优化

	// Values of type user can be used to call methods
	// declared with a pointer receiver.
	bill.changeEmail("[email protected]") 
    // Go背后调用如下:
    // (&bill).changEmail()
	bill.notify()

	// Pointers of type user can be used to call methods
	// declared with a pointer receiver.
	lisa.changeEmail("[email protected]")
	lisa.notify()
}
// 输出
// Sending User Email To Bill<[email protected]>
// Sending User Email To Lisa<[email protected]>
// Sending User Email To Bill<[email protected]>
// Sending User Email To Lisa<[email protected]>

Go语言既允许使用值,也允许使用指针来调用方法,不必严格符合接受者的类型。 对这个抱有怀疑,这样会不会造成混淆?

3.3 类型的本质

内置类型:int, string ,bool,本质是原始的类型

引用类型:切片。映射、通道和函数类型,本质是在共享底层数据结构

结构类型:struct关键字定义的,本质可以是原始的也可以是非原始的

反正这里噼里啪啦说了一大堆,最后还是被绕晕了。

3.4 接口

多态是指代码可以根据类型的具体实现采取不同行为的能力。

接口是用来定义行为的类型,接口不实现具体的方法,而是通过方法由用户定义的类型实现。

接口使用interface关键字实现。

// Go实现简单的多态
package main

import (
	"fmt"
)

// 定义接口,包含一个接口函数notify
type notifier interface {
	notify()
}

// user defines a user in the program.
type user struct {
	name  string
	email string
}

// 使用指针传递的方式实现notifier接口
func (u *user) notify() {
	fmt.Printf("Sending user email to %s<%s>\n",
		u.name,
		u.email)
}

// admin defines a admin in the program.
type admin struct {
	name  string
	email string
}

// 使用指针传递的方式实现notifier接口
func (a *admin) notify() {
	fmt.Printf("Sending admin email to %s<%s>\n",
		a.name,
		a.email)
}

// main is the entry point for the application.
func main() {
	// Create a user value and pass it to sendNotification.
	bill := user{"Bill", "[email protected]"}
	sendNotification(&bill)

	// Create an admin value and pass it to sendNotification.
	lisa := admin{"Lisa", "[email protected]"}
	sendNotification(&lisa)
}

// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
	n.notify()
}

3.5 嵌入类型 type embedding

Go支持类型嵌套。

外层的接口会覆盖内层的接口,但是可以通过直接访问内层接口来调用内层接口。这里来个简单的代码吧。

// user 实现了notify接口
// admain 实现了notify接口
type admin struct{
    u user
    age int
}
// admin中嵌套了user类型。
// 初始化一个admin 记为ad吧
ad.notify()  // 会调用admin实现的notify接口
ad.u.notify() // 会调用user实现的notify接口
// 就是这么理解,和C++的继承差不多。

3.6 公开和未公开的标志符

这个在刚接触go的时候就发现了。

当声明一个标志符以小写字母开头时,这个标志符是未公开的,private!

当声明一个标志符以大写字母开头时,这个标志符是公开的,public!

不得不说,go在这方面还是很优雅的,省的每次写public 和private。

—— 未完待续