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
编译生成可执行文件:
# 源码的同目录下
go build
生成可执行文件到bin目录:
# 源码的同目录下
go install
清除安装的可执行文件:
go clean
# 安装一个新的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版本时,直接覆盖安装即可。(或者先卸载再重新安装)
参见: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等来进行版本管理。
详细的参见:https://golang.org/cmd/go/
$ go help
Go is a tool for managing Go source code.
Usage:
go [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 " 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 " for more information about that topic.
执行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
参见:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
分为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
查询API时从这里搜索。
参见:https://golang.org/pkg/
参见:https://golang.org/ref/spec
参见:https://golang.org/doc/faq
涉及以下方面:
由来,应用,设计,实现,对于C的改变。介绍了Go的初衷和设计上的考虑。
类型,值,指针与分配,并发,函数与方法,流程控制。介绍了一些语法规范的内容。
编写代码,包与测试,性能。介绍了一些Go语言在使用上的内容。
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接口
像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 [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 " for more information about a command.
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 // 等待任务完成
}
// 一切完成。
}
编译运行后,访问 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 = `
QR Link Generator
{{if .}}
展示的二维码的字符串内容为:
{{.}}
{{end}}
`
// 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, ¶m)
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一般用于执行清理工作,例如在创建数据库连接成功之后调用该语句执行关闭数据库连接,会在函数调用结束后自动进行数据库连接的关闭。
更多介绍的参见: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。映射的零值为 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")
// 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
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
}
内嵌接口:
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)
fmt 包中定义的 Stringer 是最普遍的接口之一:
type Stringer interface {
String() string
}
Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。
自定义类型可以通过实现该方法,来支持自定义格式的打印。
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)
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 接口:
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 程(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)
}
发送者且仅有发送者可以通过 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 语句使一个 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 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
}()
}
为了更好的效率,在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)
}
}
}
参见: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 所做的调整, 就是将已缩进的文本以等宽字体显示,来适应对应的程序片段。
具体应该包括:
标识符字符必须是由Unicode定义的字符或数字(letters or digits),意味着可以使用中文字符来命名标识符。
Go中约定使用驼峰记法 MixedCaps 或 mixedCaps。
大小写可视性规则是我们最喜爱的Go特性之一。
词法分析器会自动插入分号,因此源码中基本就不用分号了。
++ 和 – 为语句而非表达式。 因此,若你想要在 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
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意此处string(m)不可以写为m
}
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)
// 分配顶层切片。
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:]
}
对于定义的别名类型,可通过将数据条目转换为多种类型来使用相应的功能(每种类型都实现了部分的接口),每次转换都完成一部分工作。
在原类型和别名类型二者之间进行转换是合法的, 转换过程并不会创建新值,它只是值暂让现有的时看起来有个新类型而已。
还有些合法转换则会创建新值,如从整数转换为浮点数等。
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 被调用后(包括不明确的运行时错误,例如切片检索越界或类型断言失败), 程序将立刻终止当前函数的执行,并开始回溯Go程的栈,运行任何defer定义的被推迟的函数。若回溯到达Go程栈的顶端,程序就会终止。
实际的库函数应避免 panic。若问题可以被屏蔽或解决, 最好就是让程序继续运行而不是终止整个程序。一个可能的反例就是初始化: 若某个库真的不能让自己工作,且有足够理由产生Panic,那就由它去吧。
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
在调用 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
}