Go简记

Go概述

概念

Go(名称来源于Google的前2个字母)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种并发的、带垃圾回收的、快速编译的静态强类型的编译型语言。
golang并非Go的正式名称,它的来源是web网站golang.org(因为go的域名已经被注册),但很多地方使用golang来索引Go语言。
Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。

The Go programming language is an open source project to make programmers more productive.

Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It’s a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.

参考

Go是开放源码项目,GitHub地址:https://github.com/golang/go。(Go的源码的官方托管地址是:https://go.googlesource.com/go,需要翻墙才能访问,GitHub只是一个镜像)
Go的官网文档是:https://golang.org/doc/(需要翻墙访问)。
Go的wiki是:https://github.com/golang/go/wiki(看上去会更详尽些)
Go的标准库的API:https://golang.org/pkg/ (可以从右上角进行搜索)
注:以上的对应中文文档为go-zh.org域名,go的中文文档化的质量和及时性较好。

Go的练习教程,
在线版:https://tour.golang.org/ (需要翻墙访问,支持中文,即:https://tour.go-zh.org
离线版:

go get golang.org/x/tour
# 中文版:
go get -u github.com/Go-zh/tour

如上命令安装之后,在工作区的bin目录下可以执行./tour命令来使用。

安装

https://golang.org/dl/
直接下载对应版本安装。(需要翻墙)
Mac版本另存csdn地址:https://download.csdn.net/download/zhiyuan411/11567538

设置GOPATH变量,指定go的工作目录。

# 编辑~/.bash_profile
export GOPATH=$HOME/go

使用

编译生成可执行文件:

扫描二维码关注公众号,回复: 9165084 查看本文章
# 源码的同目录下
go build

生成可执行文件到bin目录:

# 源码的同目录下
go install

清除安装的可执行文件:

go clean

安装多个Go版本,卸载Go和更新Go

# 安装一个新的Go版本,支持的版本列表:https://godoc.org/golang.org/dl#pkg-subdirectories
go get golang.org/dl/go1.10.7
# 使用新安装的Go版本
go1.10.7 version
# 查看Go的安装路径
go1.10.7 env GOROOT
# 卸载新版本的Go,直接删除go的安装目录、删除goX.Y.Z可执行文件
rm -rf `go1.10.7 env GOROOT`
rm `which go1.10.7`
# 卸载原始的Go,直接删除go的安装目录、删除PATH环境变量
rm -rf /usr/local/go
sudo rm /etc/paths.d/go

需要更新Go版本时,直接覆盖安装即可。(或者先卸载再重新安装)

为什么选择Go

Go的优点

  • 静态类型语言,有着强类型约束,在编译时可以检查出大多数问题
  • 编译和二级制码发布,不依赖其他库
  • 交叉编译
  • 编译速度快
  • 垃圾回收
  • 内置并发和原语,goroutines是基于协程的轻量级并发模型
  • 代码执行效率高(接近C/C++),开发效率也高(接近动态语言)
  • 代码简洁,格式统一,强制的编码规范
  • 丰富的标准库,尤其是网络库
  • 内嵌C支持,可以集成C代码库
  • 强大的工具链,覆盖了整个软件生命周期(开发、测试、部署、维护等等),例如gofmt自动格式化代码
  • 丰富的文档
  • 开源和活跃的社区
  • 出自Google

Go适用于哪些场景

  • 命令行应用,例如处理日志、数据打包、虚拟机处理、文件系统等。
  • 网络编程
  • Daemons(守护进程)
  • 数据库
  • 基础设施,如云平台
  • 其他,如嵌入式开发

其他特性

  • 函数是基本构成单元
  • 没有类和继承,通过接口来实现多态
  • 没有函数和操作符的重载
  • 不支持隐式类型转换
  • 不支持动态代码加载
  • 不支持动态库
  • 不支持泛型

热点问题

工作区

参见:https://golang.org/doc/code.html#Workspaces
一个工作区类似如下结构:

bin/
    hello                          # command executable
    outyet                         # command executable
src/
    github.com/golang/example/
        .git/                      # Git repository metadata
	hello/
	    hello.go               # command source
	outyet/
	    main.go                # command source
	    main_test.go           # test source
	stringutil/
	    reverse.go             # package source
	    reverse_test.go        # test source
    golang.org/x/image/
        .git/                      # Git repository metadata
	bmp/
	    reader.go              # package source
	    writer.go              # package source
    ... (many more repositories and packages omitted) ...

其中,
bin目录包含了自己代码生成的可执行文件。
src目录包含了自己的源代码和引用的第三方库源码,一般都使用GitHub等来进行版本管理。

Go命令

详细的参见:https://golang.org/cmd/go/

$ go help
Go is a tool for managing Go source code.

Usage:

	go <command> [arguments]

The commands are:

	bug         start a bug report
	build       compile packages and dependencies
	clean       remove object files and cached files
	doc         show documentation for package or symbol
	env         print Go environment information
	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 or modules
	mod         module maintenance
	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:

	buildmode   build modes
	c           calling between Go and C
	cache       build and test caching
	environment environment variables
	filetype    file types
	go.mod      the go.mod file
	gopath      GOPATH environment variable
	gopath-get  legacy GOPATH go get
	goproxy     module proxy protocol
	importpath  import path syntax
	modules     modules, module versions, and more
	module-get  module-aware go get
	packages    package lists and patterns
	testflag    testing flags
	testfunc    testing functions

Use "go help <topic>" for more information about that topic.

执行go get命令报错:unrecognized import path “golang.org/x/net/websocket”

执行go get命令报如下错误:

package golang.org/x/net/websocket: unrecognized import path "golang.org/x/net/websocket" (https fetch: Get https://golang.org/x/net/websocket?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

执行如下命令进行修复:

mkdir -p $GOPATH/src/golang.org/x/
cd $GOPATH/src/golang.org/x/
git clone https://github.com/golang/net.git net 
go install net

支持Go的插件和IDE

参见:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

Go的调试诊断工具

分为4大类:Profiling,Tracing,Debugging,Runtime statistics and events,意义:

  • Profiling: Profiling tools analyze the complexity and costs of a Go program such as its memory usage and frequently called functions to identify the expensive sections of a Go program.
  • Tracing: Tracing is a way to instrument code to analyze latency throughout the lifecycle of a call or user request. Traces provide an overview of how much latency each component contributes to the overall latency in a system. Traces can span multiple Go processes.
  • Debugging: Debugging allows us to pause a Go program and examine its execution. Program state and flow can be verified with debugging.
  • Runtime statistics and events: Collection and analysis of runtime stats and events provides a high-level overview of the health of Go programs. Spikes/dips of metrics helps us to identify changes in throughput, utilization, and performance.

调试诊断工具列表,参见:https://golang.org/doc/diagnostics.html

Go语言的标准库

查询API时从这里搜索。
参见:https://golang.org/pkg/

Go语言规范

参见:https://golang.org/ref/spec

官方FAQ

参见:https://golang.org/doc/faq
涉及以下方面:
由来,应用,设计,实现,对于C的改变。介绍了Go的初衷和设计上的考虑。
类型,值,指针与分配,并发,函数与方法,流程控制。介绍了一些语法规范的内容。
编写代码,包与测试,性能。介绍了一些Go语言在使用上的内容。

  • cgo 程序为“外部函数接口”提供了一种机制, 以允许从Go代码中安全地调用C库。SWIG为C++库扩展了这种能力。
  • Go没有x
    • 没有泛型:泛型可能会在某个时刻加入。
    • 没有异常处理:对于朴素的错误处理,Go的多值返回使错误易于报告而无需重载返回值。拥有内建函数panic和recover的配合来标记出真正的异常状况并从中恢复。
    • 没有断言:程序员们会使用断言作为依靠, 以避免考虑适当的错误处理和报告。Go希望避免这种情况。
    • 没有继承:由于在类型和接口之间没有明确的关系,也就无需管理或讨论类型层级,减少了记账式编程。这种隐式的类型依赖是Go中最具生产力的东西之一。
    • 不支持方法和操作符的重载:在Go的类型系统中,只通过名字进行匹配以及类型的一致性需求是主要的简化决策。
    • 没有 “implements” 声明:Go的类型通过实现接口的方法来满足该接口,仅此而已。这个性质允许定义接口并使用, 而无需修改已有的代码。接口的语义学是Go的灵活、轻量感的主要原因之一。
    • 没有像C那样的无标签联合(untagged unions):无标签联合会违反Go的内存安全保证。
    • 没有变体类型:因为它们以混乱的方式与接口重叠。
    • 句末没有分号:分号是为了解析器,而不是人们,所以Go想要尽可能地消除它。
    • 没有 ?: 三元操作符:只能使用if-else语句来完成相同功能。
    • 没有指针运算:为了安全,不会派生出可以错误地成功访问的非法地址。还可以简化垃圾收集器的实现。
    • ++ 与 – 语句不是表达式,且没有前缀式:没有指针运算,前缀和后缀增量操作符的便利性就会减少,移除它们可以简化表达式语法。
    • 不支持协变返回类型(covariant return types,如果基类的一个虚函数返回值类型是B*,那么其派生类中覆盖这个函数的时候,返回值类型可以是D*,其中D是任何以public方式继承自B的派生类):Go因为返回类型要精确匹配的原因故不支持covariant return types,这样做是为了在接口和实现之间有一个清晰的分割。
    • 不提供隐式数值转换:在C中数值类型之间的自动转换所造成的混乱超过了它的便利。
    • 缺乏在其它语言的测试框架提供的特性:测试框架往往会发展成他们自己的带条件测试、流程控制以及打印机制的迷你语言, 但Go已经拥有了所有的那些能力,为什么要重新创造它们呢?我们更愿意在Go中写测试, 因为它只需学习少量的语言,而这种方式会使测试直截了当且易于理解。
    • 映射不允许将切片作为键:映射查找需要一个相等性操作符,而切片并未实现它。
  • Go的一些实现细节
    • 在底层,接口作为两个元素实现:一个类型和一个值。只有在内部值和类型都未设置时(nil, nil),一个接口的值才为 nil。若我们在一个接口值中存储一个 *int 类型的指针,则内部类型将为 *int,无论该指针的值是什么:(*int, nil)。 因此,这样的接口值会是非 nil 的,即使在该指针的内部为 nil。
    • 在32位架构的机器上编译器默认使用32位来实现int类型,64位架构的机器上则是默认使用64位来实现int类型。
    • 实际上,Go编译器会在该函数的栈帧中, 分配该函数的局部变量。然而,如果编译器不能证明在该函数返回后,该变量不再被引用, 那么编译器必须在垃圾回收的堆上分配该变量,以避免悬空指针错误。此外,若局部变量非常大, 它可能会更合理地将变量存储在堆而非栈中。(变量分配在堆上还是栈上对于编写有效的程序来说并无影响,不需要程序员关心。)
    • Go的内存分配器在虚拟内存中预留了一大块区域作为分配的地方。这块虚拟内存局部于具体的Go进程, 而这种预留并不会剥夺内存中的其它进程。
    • Go的发行版本中所带的默认编译器 gc,在Go 1.5版本之前是C实现,从1.5版本开始已经是Go语言编写实现(通过C到Go的自动翻译工具)。Gccgo 拥有一个耦合到标准GCC后端的,带递归下降解析器的C++前端。Gollvm 正在开发之中,是基于LLVM架构的C++语言编写的实现。
    • gc 运行时代码大部分以C编写(以及一丁点汇编), 不过,目前已经被翻译为Go的实现(除了少部分的汇编)。gccgo 的运行时使用 glibc 支持。Gollvm 则是运行在LLVM架构之上。
    • gc工具链中的连接器默认做静态链接。 因此所有的Go二进制文件都包括了Go运行时,连同运行时类型信息必须支持的动态类型检测、 反射甚至恐慌时栈跟踪。这会导致Go的二进制文件比较大。(编译时加上 -ldflags=-w 参数,可以去除调试信息从而显著减少二级制文件体积)
    • Go有一个名字为runtime的扩展库,它实现了垃圾回收,并发,栈管理等关键特征;但是不像Java的runtime一样,它并不包含虚拟机。
  • len 是函数而非方法,这不会复杂化关于基本类型接口(在Go类型意义上)的问题。
  • Equal接口的实现不应传入被比较的类型T,而应传入接口类型 Equalar。(因为在Go中任何满足 Equaler 的类型都能作为实参传至 T2.Equal,所以在运行时我们必须检查该实参是否为 T2类型。)
type Equaler interface {
    Equal(Equaler) bool
}
type T int
func (t T) Equal(u T) bool { return t == u } // 并未实现Equaler接口
type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) } // 实现了Equaler接口
  • 不能直接将[]T转换为[]interface{},因为它们在内存中的表示并不相同。必须单独地通过循环遍历的方式,将每个元素复制到目标切片。
  • nil错误值不等于nil。错误值error是一种接口类型,故其永远不为nil,需要时应该直接返回nil。
  • 为什么映射是内建的?它们是如此地强大和重要的数据结构,提供了一种有句法上支持的卓越的实现, 使编程更加惬意。
  • 变量分配在堆上还是栈上对于编写有效的程序来说并无影响,不需要程序员关心。
  • 为什么要有垃圾回收?记账式系统编程的最大来源就是内存管理。我们觉得关键就在于消除程序员的开销, 而垃圾回收技术的进步给了我们以足够低的开销和没有明显的延迟来实现它的信心。垃圾回收还使得并发编程和接口变得更简单。

远程包和包版本管理

像Git或Mercurial这样的版本控制系统,可根据导入路径的描述来获取包源代码。
执行 ‘go get’ 命令会自动根据导入路径来下载对应的包和其依赖,然后编译、安装这个包,就像 ’go install’。
示例:

go get github.com/golang/example/hello
# 然后可以直接运行该命令了:
$GOPATH/bin/hello

“go get”并没有明确的包版本概念。“go get”和更大的Go工具链,仅能为包提供不同的导入路径来隔离它们。
若您使用的是外部提供的包,并担心它会以意想不到的方式改变,最简单的解决方案就是把它复制到你的本地仓库中。 (这是Google内部采用的方法。)将该副本存储在一个新的导入路径中,以此来标识出它是个本地的副本。
例如,你可以将“original.com/pkg”复制成“you.com/external/original.com/pkg”。

不过,在Go的1.13版本中,为Go命令增加了Go模块(Go modules)的形式实现的包版本管理支持。
更多参见:https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more
1.创建一个新模块
在模块的根目录下,执行命令go mod init example.com/m来生成一个初始化的go.mod文件,该文件记录了所依赖的模块版本;可以通过go help go.mod来了解更多。
2.自动添加依赖项
对编写Go源码文件执行go build命令后,会自动在 go.mod 记录所需要的远程依赖包及其版本信息。类似下面的内容:

module example.com/m

require (
	golang.org/x/text v0.3.0
	gopkg.in/yaml.v2 v2.1.0
)

go 1.13

3.查看模块的版本和下载对应的版本
需要在模块目录下执行以下命令。


# 查看模块的历史版本
go list -m -versions golang.org/x/text
# 下载对应版本到 ${GOPATH}/pkg/mod/ 目录,并更新go.mod文件
go get golang.org/x/[email protected]
# 查看当前的版本
go list -m all

# go mod的其他命令
>go help mod
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

	go mod <command> [arguments]

The commands are:

	download    download modules to local cache
	edit        edit go.mod from tools or scripts
	graph       print module requirement graph
	init        initialize new module in current directory
	tidy        add missing and remove unused modules
	vendor      make vendored copy of dependencies
	verify      verify dependencies have expected content
	why         explain why packages or modules are needed

Use "go help mod <command>" for more information about a command.

引用和值

  1. 映射、切片和信道是引用,而数组是值。
  2. Go中的所有形参都通过值来传递。也就是说, 函数总是会获得向它传递的东西的一份副本。
  3. 形参为数组时,函数将接收到该数组的一份副本而非指针。
  4. 形参为指针时,则会创建该指针的一份副本,而不是它所指向的数据。复制一个映射或切片会创建一个存储在接口值中的东西的一个副本。
  5. 如果将接口值指针传递给一个期望接受接口的函数,编译器会报错。
  6. 当为一个类型定义了一个方法,则接收者的表现正好就是该方法的实参。 将接收者定义为值还是指针,就像一个函数的实参应该是值还是指针一样的问题。
    • 方法需要修改接收者吗?如果是,则接收者必须是一个指针。
    • 若接受者很大,比如说一个大型的 struct,使用指针接收器将更廉价。(节省空间和时间)
    • 对于诸如基本类型、切片以及小型 struct 这样的类型,值接收者是非常廉价的,应该优先使用。
  7. 其它任意已命名类型 T 的方法集由所有带接收者类型 T 的方法组成。 与指针类型 *T 相应的方法集为所有带接收者 *T 或 T 的方法的集(就是说,它也包含 T 的方法集)。

CSP和Go程概念

Hoare的通信序列过程(即CSP)为并发提供了高级的语言支持,它是最成功的模型之一。 Go的并发原语来自该家族树不同的部分,它最主要的贡献就是将强大的信道概念作为第一类对象。
不要通过共享内存来进行通信,而应该通过通信来共享内存。

Go程是将独立执行的函数—— 协程——多路复用到一组线程上。当协程被阻塞,如通过调用一个阻塞的系统调用时, 运行时会在相同的操作系统线程上自动将其它的协程转移到一个不同的,可运行的, 不会被阻塞的线程上(这个过程对程序员是透明的)。
Go程只会花费比栈多一点的内存, 那只有几KB而已。
为了使栈很小,Go的运行时使用了分段式栈。一个新创建的Go程给定几KB,这几乎总是足够的。 当它不够时,运行时会自动地分配(并释放)扩展片段。每个函数调用平均需要大概三条廉价的指令。
这实际上是在相同的地址空间中创建了成百上千的Go程。如果Go程是线程的话,系统资源会更快地耗尽。
此外,Go程

并发和并行

注意不要混淆并发和并行的概念:并发是用可独立执行的组件构造程序的方法, 而并行则是为了效率在多CPU上平行地进行计算。尽管Go的并发特性能够让某些问题更易构造成并行计算, 但Go仍然是种并发而非并行的语言,且Go的模型并不适合所有的并行问题。

目前Go运行时的实现默认并不会并行执行代码,它只为用户层代码提供单一的处理核心。 任意数量的Go程都可能在系统调用中被阻塞,而在任意时刻默认只有一个会执行用户层代码。
若你希望CPU并行执行, 就必须告诉运行时你希望同时有多少Go程能执行代码。有两种途径可意识形态,要么 在运行你的工作时将 GOMAXPROCS 环境变量设为你要使用的核心数(它的默认值是CPU核心数), 要么导入 runtime 包并调用 runtime.GOMAXPROCS(NCPU)。runtime.NumCPU() 的值可能很有用,它会返回当前机器的逻辑CPU核心数。
注意,GOMAXPROCS 只是影响了实际可以同时执行的 Go程 数量;在运行时会分配比 GOMAXPROCS 更多的线程来服务明显的I/O类型的请求。
目前Go程的调度还不够完美,以后可能会对线程的使用进行更好的优化。

在本质上连续的问题并不能通过添加更多Go程来提高速度。 只有当问题在本质上并行的时候,并发才能编程并行处理。
在实际应用中,比起进行运算,在信道上花费更多时间通信的程序,会在使用多操作系统线程时出现性能下降。 这是因为在线程间发送数据涉及到切换上下文,这需要很大的代价。
比如说,在Go语言规范中 素数筛 的例子并没有明显的并行性, 尽管它启动了一些Go程,但增加 GOMAXPROCS 更有可能会减慢速度,而非提高速度。

一个并行化的例子:

// 我们在对一系列向量项进行极耗资源的操作, 而每个项的值计算是完全独立的。
type Vector []float64

// 将此操应用至 v[i], v[i+1] ... 直到 v[n-1]
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
	for ; i < n; i++ {
		v[i] += u.Op(v[i])
	}
	c <- 1    // 发信号表示这一块计算完成。
}

// 我们在循环中启动了独立的处理块,每个CPU将执行一个处理。 它们有可能以乱序的形式完成并结束,但这没有关系; 我们只需在所有Go程开始后接收,并统计信道中的完成信号即可。
const NCPU = 4  // CPU核心数

func (v Vector) DoAll(u Vector) {
	c := make(chan int, NCPU)  // 缓冲区是可选的,但明显用上更好
	for i := 0; i < NCPU; i++ {
		go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
	}
	// 排空信道。
	for i := 0; i < NCPU; i++ {
		<-c    // 等待任务完成
	}
	// 一切完成。
}

带HTML页面的Web Server

编译运行后,访问 http://127.0.0.1:1718 即可看到生成二维码的页面。

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"
)

var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18

var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()
    http.Handle("/", http.HandlerFunc(QR))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

func QR(w http.ResponseWriter, req *http.Request) {
    templ.Execute(w, req.FormValue("s"))
}

const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<iframe src="https://cli.im/api/qrcode/code?text={{.}}" width="450" height="500" frameborder="0" scrolling="no">
<span>你的浏览器不支持iframe页面嵌套。</span>
</iframe>
<p>
展示的二维码的字符串内容为:<br/>
{{.}}
</p>
<br>
<br>
{{end}}
<form action="/" name=f method="GET"><input maxLength=1024 size=70
name=s value="" title="Text to QR Encode"><input type=submit
value="Show QR" name=qr>
</form>
</body>
</html>
`

一个使用了日志,数据库,并提供JSON格式返回数据的HTTP接口服务的实例

// demo.go 文件
package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"time"
)

// DB使用的信息
const (
	USERNAME = "root"
	PASSWORD = "rootpassword"
	DBNAME   = "myDBName"
	HOST     = "127.0.0.1"
	PORT     = "3306"
	NETWORK  = "tcp"
)

// 用于json.Unmarshal,后面的是对应的json标签
type PostParam struct {
	StartDate string `json:"startDate"`
	EndDate   string `json:"endDate"`
}

type DatabaseInfo struct {
	UserName string
	Password string
	DBName   string
	Host     string
	Port     string
}

// 查询DB获取数据
func dumpData(param PostParam) ([]byte, error) {
	// 打印传入参数的日志
	log.Println(param)

	startTime := time.Now()

	// 数据库连接字符串
	//dsn := fmt.Sprintf("%s:%s@unix(/tmp/mysql.sock)/%s", USERNAME, PASSWORD, DBNAME)
	dbInfo := DatabaseInfo{USERNAME, PASSWORD, DBNAME, HOST, PORT}

	dsn := fmt.Sprintf("%s:%s@%s(%s:%s)/%s", dbInfo.UserName, dbInfo.Password, NETWORK, dbInfo.Host, dbInfo.Port, dbInfo.DBName)
	DB, err := sql.Open("mysql", dsn)

	if err != nil {
		log.Printf("Open mysql failed,err:%v\n", err)
		return nil, err
	}
	defer DB.Close()

	execSql := fmt.Sprintf("select * from testTable where createDate > %s and createDate < %s", param.StartDate, param.EndDate)

	stmt, err := DB.Prepare(execSql)
	if err != nil {
		log.Printf("db prepare err %v", err)
		return nil, err
	}
	defer stmt.Close()

	rows, err := stmt.Query()
	if err != nil {
		log.Printf("db query err %v", err)
		return nil, err
	}
	defer rows.Close()

	columns, err := rows.Columns()
	if err != nil {
		log.Printf("db rows err %v", err)
		return nil, err
	}

	// 存储结果数据
	tableData := make([]map[string]interface{}, 0)

	count := len(columns)
	values := make([]interface{}, count)
	scanArgs := make([]interface{}, count)
	for i := range values {
		scanArgs[i] = &values[i]
	}

	allCount := 0
	for rows.Next() {
		err := rows.Scan(scanArgs...)
		if err != nil {
			log.Printf("db scan err %v", err)
			return nil, err
		}

		entry := make(map[string]interface{})
		for i, col := range columns {
			v := values[i]

			b, ok := v.([]byte)
			if ok {
				entry[col] = string(b)
			} else {
				entry[col] = v
			}
		}

		tableData = append(tableData, entry)
		allCount++
	}

	jsonData, err := json.Marshal(tableData)
	if err != nil {
		log.Printf("json marshal err:  %v", err)
		return nil, err
	}

	endTime := time.Now()

	times := endTime.Sub(startTime)

	log.Printf("查询执行完毕,总共%d条结果,耗时%d", allCount, times)

	return jsonData, nil
}

// QueryHandler 处理/post/query请求
func QueryHandler(w http.ResponseWriter, req *http.Request) {

	// 设置返回值的 Header
	w.Header().Set("Content-Type", "application/json")

	// 读取请求的参数
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		fmt.Println(err)
	}
	defer req.Body.Close()
	var param PostParam
	err = json.Unmarshal(body, &param)
	if err != nil {
		log.Printf("post param unmarshal error: %v", err)
		return
	}
	// 打印日志
	log.Printf("请求参数:startDate=%s, endDate=%s", param.StartDate, param.EndDate)

	// 执行具体功能函数,从DB中读取数据
	jsonData, err := dumpData(param)
	if err != nil {
		emptyMap := make([]map[string]interface{}, 0)
		emptyRes, _ := json.Marshal(emptyMap)
		w.Write(emptyRes)
	}
	w.Write(jsonData)
}

// todayFilename 指定日志的路径和文件名
func todayFilename() string {
	today := time.Now().Format("2006-01-02")
	return "/tmp/myDemo." + today + ".log"
}
func newLogFile() *os.File {
	filename := todayFilename()
	f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		panic(err)
	}

	return f
}

func main() {
	// 日志文件
	f := newLogFile()
	defer f.Close()
	log.SetOutput(f)
	log.SetFlags(log.LstdFlags | log.Lshortfile)

	// 绑定web服务的访问路径和处理函数
	http.HandleFunc("/post/query", QueryHandler)
	// web服务监听8080端口
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		fmt.Println(err)
		log.Fatalf("listen and serve err:%v", err)
	}

}

基础语法

每个 Go 程序都是由包构成的。包使用关键字定义,位于首行:package main
程序从 main 包开始运行。
按照约定,包名与导入路径的最后一个元素一致。例如,“math/rand” 包中的源码均以 package rand 语句开始。
包应当以小写的单个单词来命名,且不应使用下划线或驼峰记法。(长命名并不会使其更具可读性。一份有用的说明文档通常比额外的长名更有价值。)

把所有源文件都放进与它们自己的包相同的目录中去。源文件可随意从不同的文件中引用项, 而无需提前声明或头文件。
除分割成多个文件外,包可以像个单文件包一样编译并测试。

导入和导出名

使用关键字import来表示导入。
使用圆括号组合了多个导入叫做分组导入形式。

// 推荐分组导入的形式
import (
	"fmt"
	"math"
)
// 等同于分别导入:
import "fmt"
import "math"

如果一个变量名称或者函数名称等以大写字母开头,那么它就是已导出的名字。
在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字(小写开头)在该包外均无法访问。

关于导入路径:
标准库中的包有给定的短路径,比如 “fmt” 和 “net/http”;
自己的源码的导入路径和标准库比较像,要注意命名应该和标准库有差异,例如以xxx.com/myProjectName作为根目录;

注:若导入某个包而不使用它就会产生编译错误。

函数

函数定义形式类似如下:

func add(x int, y int) int {
	return x + y
}

// 当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略:
func add(x, y int) int {
	return x + y
}

函数可以返回任意数量的返回值:

func swap(x, y string) (string, string) {
	return y, x
}

函数的返回值可被命名,它们会被视作定义在函数顶部的变量:

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	// 没有参数的 return 语句返回已命名的返回值
	return
}

返回函数内的一个局部变量的地址完全没有问题,这点与C不同。该局部变量对应的数据在函数返回后依然有效。

func NewFile(fd int, name string) *File {
	if fd < 0 {
		return nil
	}
	return &File{fd, name, nil, 0}
}

函数也是值,它们可以像其它值一样传递。

// 使用函数作为入参的函数
func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func add(x, y float64) float64 {
	return x + y
}

func main() {
	// 把一个函数定义为一个变量
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	// 通过变量名来调用该函数
	fmt.Println(hypot(5, 12))

	// 把函数名当作参数来调用另一个函数
	fmt.Println(compute(hypot))
	fmt.Println(compute(add))
	fmt.Println(compute(math.Pow))
}

Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值。

// 此函数的返回值即是一个闭包
func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	// 把函数名赋值给变量,pos和neg相当于是函数adder的别名
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			// pos每次都递增1,累积在adder的内部变量sum上,打印输出为:1,2,3,...
			pos(1),
			// neg每次都递增-2*i,打印输出为:0,-2,-6,...
			neg(-2*i),
		)
	}
}

变量和零值

var 语句用于声明一个变量列表。变量的类型在最后。例如:var i, j int
var 语句可以出现在包或函数级别。

变量声明可以包含初始值,每个变量对应一个。如果初始化值已存在,则可以省略类型(变量会从初始值中获得类型)。例如:var i, j int = 1, 2或者var i, j = 1, 2
注:当右边包含未指明类型的数值常量时,新变量的类型就可能是 int, float64 或 complex128 了,这取决于常量的精度。

在函数中(注:不能在函数外使用),简洁赋值语句 := 可在类型明确的地方代替 var 声明。例如:i, j := 1, 2。此外,当变量已经存在且类型相同时,仍然可以使用 := 来进行再次赋值。

变量声明也可以“分组”成一个语法块:

var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)

没有明确初始值的变量声明会被赋予它们的 零值:

数值类型为 0,
布尔类型为 false,
字符串为 “”(空字符串)。

注:若声明某个变量而不使用它就会产生编译错误。

常量

常量使用 const 关键字来声明,其用法和变量类似。但不能使用:=形式。
数值常量是高精度的 值。并不会受限于int类型的长度。例如:const Big = 1 << 100
常量在编译时创建,由于编译时的限制, 定义它们的表达式必须也是可被编译器求值的常量表达式。例如:1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 math.Sin 的函数调用在运行时才会发生。

iota是golang语言的常量计数器,只能在常量的表达式中使用。
iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
使用iota能简化定义,在定义枚举时很有用。

type ByteSize float64

const (
    _           = iota  // 通过赋予空白标识符来忽略第一个值
    KB ByteSize = 1 << (10 * iota) // KB = 1 << (10 * 1) = 2 ^ 10 = 1024
    MB // MB = 1 << (10 * 2) = 2 ^ 20 = 1024 * 1024
    GB // 1 << (10 * 3)
    TB
    PB
    EB
    ZB
    YB
)

基本类型和类型转换

bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名;表示一个 Unicode 码点
float32 float64
complex64 complex128

表达式 T(v) 将值 v 转换为类型 T。例如:f := float64(11)
在不同类型的项之间赋值时需要显式转换。

循环

Go语言只支持 for 循环:

	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	
    // 初始化语句和后置语句是可选的,此时可以省略掉分号
	sum := 1
    // 下面等同于:for sum < 1000 {
	for ; sum < 1000; {
		sum += sum
	}

    // 无限循环
	for {
	    ...
	}

分支判断

if 形式如下:

	if v > 0 {
		fmt.Println(1)
	} else if v == 0 {  // if-then-else 推荐用switch代替
		fmt.Println(0)
	} else {
		fmt.Println(-1)
	}

	// if 语句可以在条件表达式前执行一个简单的语句(该语句声明的变量作用域仅在 if 之内)
		if p := math.Pow(v, 3); p < 99 {
		return p
	}

switch 形式如下:

	// 1. Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。
	// 2. switch 的 case 无需为常量,且取值不必为整数。
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	// case 可通过逗号分隔来列举相同的处理条件
	case "linux", "Linux":
		fmt.Println("Linux.")
	default:
		fmt.Printf("%s.\n", os)

	// switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。在匹配成功之前的case的语句都会被执行。
	// 如下示例,如果 i != 0 ,则函数 f() 会被调用
	switch i {
	case 0:
	case f():
	case 1:
	}

	// 没有条件的 switch 同 switch true 一样。这种形式能将一长串 if-then-else 写得更加清晰。
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}

defer关键字

defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。立即求值的特性使得我们无需担心变量值在函数执行时被改变。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

defer一般用于执行清理工作,例如在创建数据库连接成功之后调用该语句执行关闭数据库连接,会在函数调用结束后自动进行数据库连接的关闭。
更多介绍的参见:https://blog.go-zh.org/defer-panic-and-recover

高级类型

指针

Go 拥有指针。指针保存了值的内存地址。
类型 *T 是指向 T 类型值的指针。其零值为 nil。
Go 没有指针运算。

// 定义一个指针变量
var p *int
// & 操作符会生成一个指向其操作数的指针
i := 42
p = &i
// * 操作符表示指针指向的底层值,即“间接引用”或“重定向”
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21         // 通过指针 p 设置 i

结构体

一个结构体(struct)就是一组字段(field)。

// 结构体定义
type Vertex struct {
	X int
	Y int
}

// 结构体文法
	v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
	v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
	v3 = Vertex{}      // X:0 Y:0
	p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)

// 结构体字段使用点号来访问
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
	
// 结构体字段可以通过结构体指针来访问。
// 如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X
// 也可以使用隐式间接引用,直接写 p.X 就可以
	v := Vertex{1, 2}
	p := &v
	p.X = 1e9
	fmt.Println(v)

数组和切片

数组

类型 [n]T 表示拥有 n 个 T 类型的值的数组。
数组不能改变大小。
Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。

	// 先定义后赋值
	var a [2]string
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a[0], a[1])
	fmt.Println(a)

	// 定义的同时进行初始化
	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)

切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
类型 []T 表示一个元素类型为 T 的切片。切片的零值是 nil。
实现内幕:切片包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。切片的操作和数组索引操作一样高效。
关于切片更多内容参见:https://blog.go-zh.org/go-slices-usage-and-internals

	// 定义切片的下标为一个半开区间,包括第一个元素,但排除最后一个元素。
	var s []int = primes[1:4]
	fmt.Println(s) // 输出:[ 2 3 5 ]

	// 直接定义一个切片(会创建一个数组,然后构建一个引用了它的切片)
	s := []bool{true, true, false}
	// 使用make来创建切片,会分配一个元素为零值的数组并返回一个引用了它的切片
	s := make([]int, 5)  // 长度和容量都为5
	s := make([]int, 0, 5) // 长度为0,容量为5

	// 切片下界的默认值为 0,上界则是该切片的长度。
	// 对于数组 var a [10]int 来说,以下切片是等价的:
	a[0:10]
	a[:10]
	a[0:]
	a[:]

	// 切片的长度就是它所包含的元素个数;使用 len(s) 来获取。
	// 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数;使用 cap(s) 来获取。(也就是切片可以扩容的最大长度)
	s := []int{2, 3, 5, 7, 11, 13}
	// 截取切片使其长度为 0
	s = s[:0]
	// 拓展其长度,在容量之内都可以向后进行扩容
	s = s[:4]
	// 舍弃前两个值,向前截取后就会改变切片头部地址,永远丢失前面的值
	s = s[2:]

	// 二维切片的定义
	board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

	// 为切片追加元素,注意:当追加元素导致扩容时,每次都是操作了新的数组
	var s []int // 切片未初始化,其值为nil
	s = append(s, 0) // 为一个空切片添加元素
	p := s // 此时,p[0] = 0
	s = append(s, 1) // 这个切片会按需增长
	s[0] = 99 // 此时,s[0]=99, p[0]=0
	s = append(s, 2, 3, 4) // 可以一次性添加多个元素
	s = append(s, s...) //为切片追加另一个切片时,使用...来展开切片

映射(map)

映射即map。映射的零值为 nil 。
其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。 切片不能用作映射键,因为它们的相等性还未定义。

	// 定义map
	var price map[string]float32
	// 初始化map
	price = make(map[string]float32)
	// 为map添加键值对和赋值
	price["cat"] = 1999.00 // 不存在则插入,否则修改值
	price["dog"] = 1599.00
	// 取出map的值
	fmt.Println(price["cat"])
	fmt.Println(price["dog"])
	// 通过双赋值检查元素是否存在:若存在则 ok 为 true ;否则,ok 为 false(此时,elem 是该映射元素类型的零值)
	elem, ok := price["fish"]

	// 定义并初始化map,注意,2个逗号都不可省略。当最后的}没有换行时,第2个逗号可以省略
	price := map[string]float32 {
		"cat" : 1999.00,
		"dog" : 1599.00,
	}

	// 删除元素。即便对应的键不在该映射中,此操作也是安全的。
	delete(price, "dog")

使用range关键字来遍历数组,切片或者map

	// 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
	for i, v := range s {
		fmt.Printf("s[%d] = %d\n", i, v)
	}

	// 可以将下标或值赋予 _ 来忽略它(若你只需要索引,忽略第二个变量即可)
	for i, _ := range s
	for _, value := range s
	for i := range s

高级语法

方法和接口

为结构体类型定义方法

Go 没有类。不过可以为结构体类型定义方法。
方法就是一类带特殊的 接收者 参数的函数。
只能为在同一包内定义的类型的接收者声明方法。

type Vertex struct {
	X, Y float64
}

// Abs 方法拥有一个名为 v,类型为 Vertex 的接收者
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 也可以为非结构体类型声明方法
type MyFloat float64
func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	v := Vertex{3, 4}
	// 可以使用 结构变量.方法名 的方式来调用
	fmt.Println(v.Abs())

	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

对于某类型 T,接收者的类型可以用 *T 的文法。
指针接收者的方法可以修改接收者指向的值,同时,还可以避免在每次调用方法时复制该值。
所以,指针接收者比值接收者更常用。

以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。
之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。
不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。

// 使用指针接收者,可以在方法内部修改接收者的值;如果是使用值接收者,则方法内看到的只是副本,并不可以改变main()函数里的入参
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}
func main() {
	v := Vertex{3, 4}
	// 调用时可以如下简写,等同于:(&v).Scale(10) (注:其实对于值接收者,也可以以该指针形式调用。)
	v.Scale(10)
	fmt.Println(v.X, v.Y)
}

接口

Go中的接口为指定对象的行为提供了一种方法:如果某样东西可以完成这个, 那么它就可以用在这里。

接口类型 是由一组方法签名定义组成的集合。
接口类型的变量可以保存任何实现了这些方法的值。
一个类型通过实现一个接口的所有方法来实现该接口。无需专门显式声明,Go语言也没有“implements”关键字。

// 定义一个接口类型
type Abser interface {
	Abs() float64
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	if v == nil {
		return -1
	}
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	// 定义一个接口类型的变量
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	// a MyFloat 实现了 Abser
	a = f
	fmt.Println(a.Abs())
	// a *Vertex 实现了 Abser
	a = &v
	fmt.Println(a.Abs())

	// 下面一行是错误的,因为v 是一个 Vertex(而不是 *Vertex),没有实现 Abser。
	// a = v

	// 接口值可以像其它值一样传递。
	describe(a)

	// 即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。如下示例,需要在方法Abs()内部进行处理
	var n *Vertex
	a = n
	fmt.Println(a.Abs())

	// 如果接口类型变量为nil,则不能调用其方法
	// var aa Abser 该行定义一个 nil 接口值
	// aa.Abs() 该行会报运行时空指针错误
}

// 接口值可以用作函数的参数或返回值。
func describe(i Abser) {
	// 在内部,接口值可以看做包含具体类型和(该具体类型的)具体值的元组
	fmt.Printf("(%T, %v)\n", i, i) // 打印接口类型变量的类型和值
}

导出接口而非导出具体的类型具有更好的通用性。
若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。 仅导出该接口能让我们更专注于其行为而非实现,其它属性不同的实现则能镜像该原始类型的行为。

例如在 hash 库中,crc32.NewIEEE 和 adler32.New 都返回接口类型 hash.Hash32。因此,要在Go程序中用Adler-32算法替代CRC-32, 只需修改构造函数调用即可,其余代码则不受算法改变的影响。

// crc32.NewIEEE 返回的实际上是digest类型的指针,完整实现参见:https://golang.org/src/hash/crc32/crc32.go
type digest struct {
	crc uint32
	tab *Table
}
func New(tab *Table) hash.Hash32 { return &digest{0, tab} }
func NewIEEE() hash.Hash32 { return New(IEEETable) }

// adler32.New 返回的实际上是digest类型的指针,完整实现参见:https://golang.org/src/hash/adler32/adler32.go
type digest uint32
func New() hash.Hash32 {
	d := new(digest)
	d.Reset()
	return d
}

// hash.Hash32接口定义
type Hash32 interface {
    Hash
    Sum32() uint32
}
type Hash interface {
    // Write (via the embedded io.Writer interface) adds more data to the running hash.
    // It never returns an error.
    io.Writer

    // Sum appends the current hash to b and returns the resulting slice.
    // It does not change the underlying hash state.
    Sum(b []byte) []byte

    // Reset resets the Hash to its initial state.
    Reset()

    // Size returns the number of bytes Sum will return.
    Size() int

    // BlockSize returns the hash's underlying block size.
    // The Write method must be able to accept any amount
    // of data, but it may operate more efficiently if all writes
    // are a multiple of the block size.
    BlockSize() int
}

内嵌接口:

  1. 接口的内嵌的元素为接口。
  2. 被嵌入的接口必须是不相交的方法集。
  3. 结构体的内嵌的元素为指向结构体的指针。
  4. 结构体进行内嵌时,只列出结构体中的类型,不需要也不能给予它们字段名。
  5. 内嵌的结构体的方法可以被直接引用。
  6. 当内嵌一个类型时,该类型的方法会成为外部类型的方法, 但当它们被调用时,该方法的接收者是内部类型,而非外部的。如下例子中,当 bufio.ReadWriter 的 Read 方法被调用时, 接收者是 ReadWriter 的 reader 字段,而非 ReadWriter 本身。
  7. 若我们需要直接引用内嵌字段,可以忽略包限定名,直接将该字段的类型名作为字段名。
  8. 内嵌类型如果引入命名冲突的问题,解决规则:1)字段或方法 X 会隐藏该类型中更深层嵌套的其它项 X。2)若相同的嵌套层级上出现同名冲突,通常会产生一个错误。(然而,若重名永远不会在该类型定义之外的程序中使用,那就不会出错。 )如下例子中,1)若 log.Logger 包含一个名为 Command 的字段或方法,Job 的 Command 字段会覆盖它。2)若 Job 结构体中包含名为 Logger 的字段或方法,再将 log.Logger 内嵌到其中的话就会产生错误。
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

// ReadWriter 接口通过内嵌方式,结合了 Reader 和 Writer 接口。
type ReadWriter interface {
	Reader
	Writer
}

同样的基本想法可以应用在结构体中
bufio 包中有 bufio.Reader 和 bufio.Writer 这两个结构体类型, 它们每一个都实现了与 io 包中相同意义的接口。

// ReadWriter 存储了指向 Reader 和 Writer 的指针。因此,它实现了 io.ReadWriter。
// 内嵌类型的方法可以直接引用(即其对象rw可以这样进行调用rw.Read()),这意味着 bufio.ReadWriter 不仅包括 bufio.Reader 和 bufio.Writer 的方法,它还同时满足下列三个接口: io.Reader、io.Writer 以及 io.ReadWriter。
type ReadWriter struct {
	*Reader  // *bufio.Reader
	*Writer  // *bufio.Writer
}

// 对比:一个常规的命名字段的实现,它不能被直接引用,所以需要自己提供转发的方法来满足io接口
type ReadWriter struct {
	reader *Reader
	writer *Writer
}
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
	return rw.reader.Read(p)
}

// 嵌入字段和命名字段的混合应用
type Job struct {
	Command string
	*log.Logger
}
// 嵌入字段使用时的便利性
job.Log("starting now...")
func (job *Job) Logf(format string, args ...interface{}) {
	// 若我们需要直接引用内嵌字段,可以忽略包限定名,直接将该字段的类型名作为字段名
	job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

内建接口

空接口

指定了零个方法的接口值被称为 空接口:interface{},空接口可保存任何类型的值。
例如,fmt.Print定义为:
func Println(a ...interface{}) (n int, err error)

Stringer

fmt 包中定义的 Stringer 是最普遍的接口之一:

type Stringer interface {
    String() string
}

Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。
自定义类型可以通过实现该方法,来支持自定义格式的打印。

error

Go 程序使用 error 值来表示错误状态。
fmt包中定义的error 类型是一个内建接口:

type error interface {
    Error() string
}

通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil 来进行错误处理。

// error 为 nil 时表示成功;非 nil 的 error 表示失败
i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

Reader

io 包指定了 io.Reader 接口,它表示从数据流的末尾进行读取。
Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。

type Reader interface {
        Read(p []byte) (n int, err error)
}

一个使用Read()函数来读取数据的示例:

	// 创建一个Reader
	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])
		// Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误
		if err == io.EOF {
			break
		}
	}

Image

image 包定义了 Image 接口:

type Image interface {
    ColorModel() color.Model
    Bounds() Rectangle
    At(x, y int) color.Color
}

应用示例:

	// 创建一个图片
	m := image.NewRGBA(image.Rect(0, 0, 100, 100))
	// 返回类型为接口image.Rectangle
	fmt.Println(m.Bounds())
	// color.Color 和 color.Model 类型也是接口,一般直接使用预定义的实现 image.RGBA 和 image.RGBAModel
	fmt.Println(m.At(0, 0).RGBA())

并发

Go 程

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

Go程具有简单的模型:它是与其它Go程并发运行在同一地址空间的函数。它是轻量级的, 所有消耗几乎就只有栈空间的分配。而且栈最开始是非常小的,所以它们很廉价, 仅在需要时才会随着堆空间的分配(和释放)而变化。
Go程在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待I/O, 那么其它的线程就会运行。Go程的设计隐藏了线程创建和管理的诸多复杂性。

使用方法:

// 下面会启动一个新的 Go 程并执行f()函数
// f和参数x, y, z的计算是在当Go程中,f()的执行是在新的Go程中。
go f(x, y, z)

信道

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

// 创建信道
ch := make(chan int)
// 将 v 发送至信道 ch
ch <- v
// 从 ch 接收值并赋予 v
v := <-ch

	// 信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:
	ch := make(chan int, 2)
	// 仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞
	// 缓冲区里的数据是先进先出
	ch <- 1
	ch <- 2
	fmt.Println(<-ch) // 打印:1
	fmt.Println(<-ch) // 打印:2

应用实例:
对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // 将和送入 c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // 从 c 中接收

	fmt.Println(x, y, x+y)
}

range和close

发送者且仅有发送者可以通过 close() 关闭一个信道来表示没有需要发送的值了。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:v, ok := <-ch,若没有值可以接收且信道已被关闭,ok会被置为false。
range循环会不断从信道接收值,直到它被关闭。
注:信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。
应用实例:

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	i := 0
	for v := range c {
		fmt.Println(i, v)
		i++
	}
}

select

select 语句使一个 Go 程可以等待多个通信操作:它会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
当 select 中的其它分支都没有准备好时,default 分支就会执行。为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。例如:在default分支可以打印信息并sleep一定时间后重试。
应用实例:

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		// 主Go程里,要么给信道c写入数据,要么读取到信道quit的信号结束本函数
		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程时,直接创建一个无名函数并无参数调用
	go func() {
		for i := 0; i < 10; i++ {
			// 在Go程里读取信道c的内容并打印
			fmt.Println(i, <-c)
		}
		// 给信道quit发结束信号
		quit <- 0
	}()
	fibonacci(c, quit)
}

定时器的应用实例:

	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) 这一数据结构来提供多个Go程互斥地访问同一个共享的变量的能力。
可以通过在代码前调用Mutex变量的Lock 方法,在代码后调用 Mutex变量的Unlock 方法来保证一段代码的互斥执行。已锁定的 Mutex 并不与特定的Go程相关联,这样便可让一个Go程锁定 Mutex,然后安排其它Go程来解锁。
也可以用 defer 语句来保证互斥锁一定会被解锁。

// Mutex 是一个互斥锁。 Mutex 可作为其它结构的一部分来创建;Mutex 的零值即为已解锁的互斥体。
type Mutex struct {
    // contains filtered or unexported fields
}
// Lock 用于锁定 m。 若该锁正在使用,调用的Go程就会阻塞,直到该互斥体可用。
func (m *Mutex) Lock()
// Unlock 用于解锁 m。 若 m 在进入 Unlock 前并未锁定,就会引发一个运行时错误。
func (m *Mutex) Unlock()

一个支持并发的计数器实例:

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	c.v[key]++
	c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	defer c.mux.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}

闭包作为Go程的应用

闭包引用的函数体之外的变量可能在Go程执行的过程中已经被改变,从而不能得到预期中的结果。
要在这类问题发生前发现它们,请运行 go vet。

举例:

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
        	// 并发执行时,实际上v值在循环中已经被改变,导致所有Go程打印内容都为"c"
            fmt.Println(v)
            done <- true
        }()
    }

    // 在退出前等待所有Go程完成
    for _ = range values {
        <-done
    }
}

要将 v 的当前值在每一个闭包启动后绑定至它们,就必须在每一次迭代中, 通过修改内部循环来创建新的变量。

举例:

	// 将变量作为实参传至该闭包中
    for _, v := range values {
        go func(u string) {
            fmt.Println(u)
            done <- true
        }(v)
    }

	// 推荐:简单地创建新的变量。这种赋值给自己的方式在Go中很常见。
    for _, v := range values {
        v := v // 创建新的“v”
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

Map操作不具有原子性

为了更好的效率,在Go中映射操作是没有定义为原子性的。这也就意味着不受控制的映射访问会使程序崩溃。

  1. 在多Go程中,当有Go程有更新(增删改)操作时,对映射的访问是不安全的,可能会读到脏数据和导致程序崩溃;
  2. 如果所有Go程只是读取或遍历查询映射,那么他们完全可以安全地对映射进行并发访问而无需使用同步机制;

测试

Go有一个轻量级的测试框架。
你可以通过创建一个名字以 _test.go 结尾的,包含名为 TestXXX 且签名为 func (t *testing.T) 函数的文件来编写测试。
执行go test来运行测试。测试框架会运行每一个这样的函数;若该函数调用了像 t.Error 或 t.Fail 这样表示失败的函数,此测试即表示失败。
支持的具体函数参见:https://golang.org/pkg/testing/#T
fmt包的测试案例:https://golang.org/src/pkg/fmt/fmt_test.go

待测试文件reverse.go:

// stringutil 包含有用于处理字符串的工具函数。
package stringutil

// Reverse 将其实参字符串以符文为单位左右反转。
func Reverse(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

测试文件reverse_test.go:

package stringutil

import "testing"

func TestReverse(t *testing.T) {
	// 推荐通过表格控制, 在数据结构中定义的输入输出列表上进行迭代
	cases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{"Hello, 世界", "界世 ,olleH"},
		{"", ""},
	}
	// 循环遍历变量来测试函数执行结果是否符合预期
	for _, c := range cases {
		// 调用被测函数,因为是同一个package的,所以直接使用方法名调用
		got := Reverse(c.in)
		if got != c.want {
			// 调用tError或tErrorf来主动抛出失败异常
			t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
		}
	}
}

Go风格编码

参见:https://golang.org/doc/effective_go.html

代码格式化

工具支持
gofmt 程序(也可用 go fmt,它以包为处理对象而非源文件)将Go程序按照标准风格缩进、 对齐,保留注释并在需要时重新格式化。
缩进
我们使用制表符(tab)缩进,gofmt 默认也使用它。在你认为确实有必要时再使用空格。
行的长度
Go对行的长度没有限制,别担心打孔纸不够长。如果一行实在太长,也可进行折行并插入适当的tab缩进。

注释

Go语言支持C风格的块注释 /* */ 和C++风格的行注释 //。
godoc 既是一个程序,又是一个Web服务器,它对Go的源码进行处理,并提取包中的文档内容。 出现在顶级声明之前,且与该声明之间没有空行的注释,将与该声明一起被提取出来,作为该条目的说明文档。 这些注释的类型和风格决定了 godoc 生成的文档质量。
注释无需进行额外的格式化,如用星号来突出等。生成的输出甚至可能无法以等宽字体显示, 因此不要依赖于空格对齐,godoc 会像 gofmt 那样处理好这一切。 注释是不会被解析的纯文本,因此像HTML或其它类似于 _这样_ 的东西将按照 原样 输出,因此不应使用它们。godoc 所做的调整, 就是将已缩进的文本以等宽字体显示,来适应对应的程序片段。
具体应该包括:

  1. 包注释,即放置在Package前的一个块注释。对于包含多个文件的包, 包注释只需出现在其中的任一文件中即可。包注释应在整体上对该包进行介绍,并提供包的相关信息。
  2. 文档注释,任何顶级声明前面的注释都将作为该声明的文档注释。 在程序中,每个可导出(首字母大写)的名称都应该有文档注释。文档注释的第一句应当以被声明的名称(例如函数名或者变量名)开头,并且是单句的摘要。

命名

标识符字符必须是由Unicode定义的字符或数字(letters or digits),意味着可以使用中文字符来命名标识符。
Go中约定使用驼峰记法 MixedCaps 或 mixedCaps。
大小写可视性规则是我们最喜爱的Go特性之一。

  1. 包名。包名与导入路径的最后一个元素一致。且应当以小写的单个单词来命名。(不应使用下划线或驼峰记法)
  2. 获取器。若你有个名为 owner (小写,未导出)的字段,其获取器应当名为 Owner(大写,可导出)而非 GetOwner。若要提供设置器方法,SetOwner 是个不错的选择。
  3. 接口名。只包含一个方法的接口应当以该方法的名称加上-er后缀来命名,如 Reader、Writer、 Formatter、CloseNotifier 等。
  4. Read、Write、Close、Flush、 String 等都具有典型的签名和意义。为避免冲突,请不要用这些名称为你的方法命名。反之,若你的类型实现了的方法, 与一个众所周知的类型的方法拥有相同的含义,那就使用相同的命名。 请将字符串转换方法命名为 String 而非 ToString。

符号

分号

词法分析器会自动插入分号,因此源码中基本就不用分号了。

  1. 通常Go程序只在诸如 for 循环子句这样的地方使用分号, 以此来将初始化器、条件及增量元素分开。如果你在一行中写多个语句,也需要用分号隔开。
  2. 警告:无论如何,你都不应将一个控制结构(if、for、switch 或 select)的左大括号放在下一行。如果这样做,就会在大括号前面插入一个分号,这可能引起不需要的效果。

++和–

++ 和 – 为语句而非表达式。 因此,若你想要在 for 中使用多个变量,应采用平行赋值的方式 (例如:i, j := i+1, j-1

_(空白标识符)

空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。

若导入某个包或声明某个变量而不使用它就会产生错误。未使用的包会让程序膨胀并拖慢编译速度, 而已初始化但未使用的变量不仅会浪费计算能力,还有可能暗藏着更大的Bug。
要让编译器停止关于未使用导入的错误提示,需要空白标识符来引用已导入包中的符号。 同样,将未使用的变量 fd 赋予空白标识符也能关闭未使用变量错误。

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // 用于调试,结束时删除。
var _ io.Reader    // 用于调试,结束时删除。

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // 用于调试
    _ = fd
}

大多数 Go 程序员会使用 goimports 工具,它能自动重写 Go 源文件使其拥有正确的导入,消除实践中的未使用导入问题。 此程序很容易连接到大多数编辑器,使其在保存 Go 源文件时自动运行。

为副作用而导入。
import _ "net/http/pprof" 这种导入格式能明确表示该包是为其副作用而导入的,该导入方式会调用pprof包的init函数,而不能调用其内部方法。实际上,这种导入方式使得该包没有名字,因此也不可能有被使用的可能。

打印

全量的格式符号,参见:https://golang.org/pkg/fmt

  1. Go采用的格式化打印风格和C的 printf 族类似,这些函数位于 fmt 包中,且函数名首字母均为大写:如 fmt.Printf、fmt.Fprintf,fmt.Sprintf 等。
  2. 字符串函数(Sprintf 等)会返回一个字符串,而非填充给定的缓冲区。
  3. 每个 Printf、Fprintf 和 Sprintf 都分别对应另外的函数,如 Print 与 Println。 这些函数并不接受格式字符串,而是为每个实参生成一种默认格式。Println 系列的函数还会在实参中插入空格,并在输出时追加一个换行符,而 Print 版本仅在操作数两侧都没有字符串时才添加空白。
  4. fmt.Fprint 一类的格式化打印函数可接受任何实现了 io.Writer 接口的对象作为第一个实参;变量os.Stdout 与 os.Stderr 都是人们熟知的例子。
  5. 像 %d 这样的数值格式,会根据实参的类型来决定打印的格式。
  6. 若你只想要默认的转换,如使用十进制的整数,你可以使用通用的格式 %v(对应“值”);其结果与 Print 和 Println 的输出完全相同。这种格式还能打印任意值,甚至包括数组、结构体和映射。
  7. 当打印结构体时,改进的格式 %+v 会为结构体的每个字段添上字段名,而另一种格式 %#v 将完全按照Go的语法打印值。
  8. 若你想控制自定义类型的默认格式,只需为该类型定义一个具有 String() string 签名的方法。
  9. 另一种实用的格式是 %T,它会打印某个值的类型。
  10. 当遇到 string 或 []byte 值时, 可使用 %q 产生带引号的字符串;而格式 %#q 会尽可能使用反引号。 (%q 格式也可用于整数和符文,它会产生一个带单引号的符文常量。)
  11. %x 还可用于字符串、字节数组以及整数,并生成一个很长的十六进制字符串, 而带空格的格式(% x)还会在字节之间插入空格。
  12. 我们的 String 方法也可调用 Sprintf,但是,请勿在该Sprintf中直接引用结构对象来打印,因为它会无限递归你的 String 方法;而是应该使用string()函数来类似这样处理:
type MyString string
func (m MyString) String() string {
	return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意此处string(m)不可以写为m
}

数据

new和make

new 不会初始化内存,只会将内存置零。 也就是说,new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值。

有时零值还不够好,这时就需要一个初始化构造函数。以下为举例:

*File f = &File{fd, name, nil, 0}
*File f = &File{fd: fd, name: name}
// 表达式 new(File) 和 &File{} 是等价的
*File f = &File{}

make只用于创建切片、map和信道,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。
因为这三种类型本质上为引用数据类型,它们在使用前必须初始化。

对比:

var p *[]int = new([]int)       // 分配切片结构;*p == nil;基本没用
var v  []int = make([]int, 100) // 切片 v 现在引用了一个具有 100 个 int 元素的新数组

// 没必要的复杂:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

数组等数据结构

  1. 数组是值。将一个数组赋予另一个数组会复制其所有元素。
  2. 若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
  3. 数组的大小是其类型的一部分。类型 [10]int 和 [20]int 是不同的。
  4. 二维切片的两种分配方式:
// 分配顶层切片。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 遍历行,为每一行都分配切片
for i := range picture {
	picture[i] = make([]uint8, XSize)
}

// 分配顶层切片,和前面一样。
picture := make([][]uint8, YSize) // 每 y 个单元一行。
// 分配一个大的切片来保存所有像素
pixels := make([]uint8, XSize*YSize) // 拥有类型 []uint8,尽管图片是 [][]uint8.
// 遍历行,从剩余像素切片的前面切出每行来。
for i := range picture {
	picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

初始化

  1. 每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。
  2. 每个文件都可以拥有多个 init 函数。
  3. 所有 init 只有在所有已导入的包都被初始化后才会被求值。
  4. 只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用。
  5. init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。

类型操作

类型转换

对于定义的别名类型,可通过将数据条目转换为多种类型来使用相应的功能(每种类型都实现了部分的接口),每次转换都完成一部分工作。
在原类型和别名类型二者之间进行转换是合法的, 转换过程并不会创建新值,它只是值暂让现有的时看起来有个新类型而已。
还有些合法转换则会创建新值,如从整数转换为浮点数等。

type Sequence []int

func (s Sequence) String() string {
	sort.Sort(s)
	return fmt.Sprint([]int(s)) // 为了使用Sprint对 []int 类型支持的功能,将Sequence转换为[]int类型
}

在别名类型之间转换的一个通用原则是:

The general rule is that you can change the name of the type being converted (and thus possibly change its method set) but you can’t change the name (and method set) of elements of a composite type.

举例:

type T1 int
type T2 int
var t1 T1
var x = T2(t1) // 正确
var st1 []T1
var sx = ([]T2)(st1) // 编译错误

常量的类型转换规则会宽松很多。
原因是常量的实现是拥有任意精度,不会栈溢出的理想数字。
比如,math.Pi 保存了63位精度,这远超过一个float64类型可以保持的精度。
因此,直接写为 sqrt2 := math.Sqrt(2),则理想数字2会被安全并且精确地转换为所需的float64类型,而无需使用强制类型转换。

类型选择

类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。

	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)
	}

类型断言

类型断言接受一个接口值, 并从中提取指定的明确类型的值。

	var i interface{} = "hello"

	// 语句断言接口值 i 保存了具体类型 string,并将其底层类型为 string 的值赋予变量 s
	s := i.(string)
	fmt.Println(s)

	// ok 为 true
	s, ok := i.(string)
	fmt.Println(s, ok)

	// 若类型断言失败,f 为该类型的零值,即 0,ok 为 false
	f, ok := i.(float64)
	fmt.Println(f, ok)

	// 下一句会触发错误(panic)
	// f = i.(float64)

	// 自己做异常处理
	str, ok := value.(string)
	if ok {
		fmt.Printf("字符串值为 %q\n", str)
	} else {
		fmt.Printf("该值非字符串\n")
	}

接口检查

大部分接口转换都是静态的,因此会在编译时检测。
但是,有些接口检查会在运行时进行。encoding/json 包中就有个实例它定义了一个 Marshaler 接口。当JSON编码器接收到一个实现了该接口的值,那么该编码器就会调用该值的编组方法, 将其转换为JSON,而非进行标准的类型转换。 编码器在运行时通过类型断言检查其属性。
若只需要判断某个类型是否是实现了某个接口,而不需要实际使用接口本身 (可能是错误检查部分),就使用空白标识符来忽略类型断言的值:

if _, ok := val.(json.Marshaler); ok {
	fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

以下方式可以在编译阶段进行验证接口类型是否符合预期:

// 用空白标识符声明一个全局变量:
var _ json.Marshaler = (*RawMessage)(nil)

在此声明中,我们调用了一个 *RawMessage 转换并将其赋予了 Marshaler,以此来要求 *RawMessage 实现 Marshaler,这时其属性就会在编译时被检测。 若 json.Marshaler 接口被更改,此包将无法通过编译, 而我们则会注意到它需要更新。
作为约定, 仅当代码中不存在静态类型转换时才能这种声明,毕竟这是种罕见的情况。

错误

panic

内建的 panic 函数,会产生一个运行时错误并终止程序。该函数接受一个任意类型的实参(一般为字符串),并在程序终止时打印。
当 panic 被调用后(包括不明确的运行时错误,例如切片检索越界或类型断言失败), 程序将立刻终止当前函数的执行,并开始回溯Go程的栈,运行任何defer定义的被推迟的函数。若回溯到达Go程栈的顶端,程序就会终止。

实际的库函数应避免 panic。若问题可以被屏蔽或解决, 最好就是让程序继续运行而不是终止整个程序。一个可能的反例就是初始化: 若某个库真的不能让自己工作,且有足够理由产生Panic,那就由它去吧。

var user = os.Getenv("USER")

func init() {
	if user == "" {
		panic("no value for $USER")
	}
}

recover

在调用 panic 后,或者切片检索越界或类型断言失败等运行时错误时,回溯Go程的栈的过程中,我们可以用内建的 recover 函数来重新取回Go程的控制权限并使其恢复正常执行。
调用 recover 将停止回溯过程,并返回传入 panic 的实参。 由于在回溯时只有defer定义的被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。

实例:以局部的错误类型调用 panic 来报告解析错误,这种方式使得报告解析错误变得更容易,而无需手动处理回溯的解析栈。但它应当仅在包内使用,不向调用者暴露出 panic。

// Error 是解析错误的类型,它满足 error 接口。
type Error string
func (e Error) Error() string {
	return string(e)
}

// error 是 *Regexp 的方法,它通过用一个 Error 触发Panic来报告解析错误。
func (regexp *Regexp) error(err string) {
	panic(Error(err))
}

// Compile 返回该正则表达式解析后的表示。
func Compile(str string) (regexp *Regexp, err error) {
	regexp = new(Regexp)
	defer func() {
		// 在本延迟函数中处理panic
		if e := recover(); e != nil {
			regexp = nil    // 清理返回值。
			err = e.(Error) // 若它不是解析错误,将重新触发Panic。此时会产生运行时错误,并继续栈的回溯。该检查意味着若发生了一些像索引越界之类的意外,那么即便我们使用了 panic 和 recover 来处理解析错误,代码仍然会失败。
		}
	}()
	// 可能会抛出panic
	return regexp.doParse(str), nil
}
发布了193 篇原创文章 · 获赞 23 · 访问量 33万+

猜你喜欢

转载自blog.csdn.net/zhiyuan411/article/details/100110910
今日推荐