这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

Learning Golang

Golang 学习笔记

1 - 介绍

Golang 介绍和资料收集

1.1 - 介绍

Golang 介绍

golang 官网的介绍:

Build simple, secure, scalable systems with Go:

  • An open-source programming language supported by Google
  • Easy to learn and great for teams
  • Built-in concurrency and a robust standard library
  • Large ecosystem of partners, communities, and tools

使用 Go 构建简单、安全、可扩展的系统:

  • Google 支持的开源编程语言
  • 易于学习,非常适合团队
  • 内置并发和强大的标准库
  • 由合作伙伴、社区和工具组成的大型生态系统

资料

1.2 - 资料

Golang 资料收集

官方网站

官方网站地址为:

https://go.dev/

或者中国的镜像站:

https://golang.google.cn/

内容包括:

  • learn:官方教程,包括文档,教程,案例

  • Docs: 官方文档

  • Packge:包

  • Community:社区

官方教程

https://go.dev/learn/

  • Documentation: 指到官方文档去了,忽略

  • Tour of Go:最基础的 go 知识

  • Go by example:非常基础的一些 golang go相关的示例代码

官方文档

Packge

可以搜索各种 package。

社区

2 - 开发环境

搭建 Golang 开发环境

2.1 - 安装 golang

在各个平台上安装和配置 golang

2.1.1 - 下载golang

下载 golang 的安装文件

下载

下载地址:

https://go.dev/dl/

windows 选择 msi 安装文件:

https://go.dev/dl/go1.24.2.windows-amd64.msi

linux 下选择 tar.gz 压缩文件:

wget https://go.dev/dl/go1.24.2.linux-amd64.tar.gz

版本一般选最新。

准备gopath

gopath 是 golang 的 workspace,用于存放项目代码和依赖。

mkdir -p $HOME/work/soft/gopath/

2.1.2 - nexus

为 golang 项目添加 nexus 的代理

nexus 仓库类型

默认情况下 nexus 只支持 golang 的 proxy 和 group 类型的仓库,没有 hosted 类型的仓库:

create-golang-repository

从下面的官方文档

https://help.sonatype.com/en/go-repositories.html#configuring-a-go-project-in-sonatype-nexus-repository

得知:

You can serve and cache modules remotely from resources such as GitHub. Currently, hosted repositories aren’t available.

您可以从 GitHub 等资源远程提供和缓存模块。目前,hosted 存储库不可用。

配置 go proxy 仓库

创建新的 go proxy 仓库:

注意:取消勾选 Cache responses for content not present in the proxied repository 选项。

多建几个 proxy 仓库:

其他备选(暂时不用):

再新建一个名为 go-proxy-all 的 group 类型的仓库,将上述 proxy 仓库添加进去,顺序为:

  • go-proxy-cn
  • go-proxy-official-io
  • go-proxy-official

记录此时仓库的地址备用,如:

http://192.168.0.246:8081/repository/go-proxy-all/

验证

备注: 在设置好 goproxy 之后再验证

mkdir -p ~/work/code/temp/go-demo
cd ~/work/code/temp/go-demo

git clone https://github.com/gobuffalo/buffalo

cd buffalo
go build

以下为输出:

go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/gobuffalo/envy v1.10.2
go: downloading github.com/gobuffalo/events v1.4.3
go: downloading github.com/gobuffalo/flect v1.0.0
go: downloading github.com/gobuffalo/grift v1.5.2
go: downloading github.com/gobuffalo/logger v1.0.7
go: downloading github.com/gobuffalo/plush/v4 v4.1.18
go: downloading github.com/gobuffalo/refresh v1.13.3
go: downloading github.com/gorilla/handlers v1.5.1
go: downloading github.com/gorilla/mux v1.8.0
go: downloading github.com/gorilla/sessions v1.2.1
go: downloading github.com/gobuffalo/nulls v0.4.2
go: downloading github.com/monoculum/formam v3.5.5+incompatible
go: downloading github.com/sirupsen/logrus v1.9.0
go: downloading github.com/gobuffalo/meta v0.3.3
go: downloading github.com/spf13/cobra v1.6.1
go: downloading github.com/gobuffalo/github_flavored_markdown v1.1.3
go: downloading github.com/gobuffalo/helpers v0.6.7
go: downloading github.com/gobuffalo/tags/v3 v3.1.4
go: downloading golang.org/x/text v0.6.0
go: downloading github.com/joho/godotenv v1.4.0
go: downloading github.com/rogpeppe/go-internal v1.9.0
go: downloading github.com/felixge/httpsnoop v1.0.1
go: downloading golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
go: downloading github.com/gorilla/securecookie v1.1.1
go: downloading github.com/BurntSushi/toml v1.2.1
go: downloading github.com/gofrs/uuid v4.2.0+incompatible
go: downloading golang.org/x/sys v0.0.0-20220908164124-27713097b956
go: downloading github.com/spf13/pflag v1.0.5
go: downloading github.com/microcosm-cc/bluemonday v1.0.20
go: downloading github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d
go: downloading github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e
go: downloading golang.org/x/net v0.0.0-20221002022538-bcab6841153b
go: downloading github.com/fatih/color v1.13.0
go: downloading github.com/fsnotify/fsnotify v1.6.0
go: downloading github.com/mitchellh/go-homedir v1.1.0
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/fatih/structs v1.1.0
go: downloading github.com/gobuffalo/validate/v3 v3.3.3
go: downloading github.com/sergi/go-diff v1.2.0
go: downloading github.com/mattn/go-colorable v0.1.9
go: downloading github.com/mattn/go-isatty v0.0.14
go: downloading github.com/aymerick/douceur v0.2.0
go: downloading github.com/gorilla/css v1.0.0

此时本地 gopath 目录下能看到这些已经下载的 go module:

$ ls /home/sky/work/soft/gopath/pkg/mod/github.com 

 aymerick        felixge     gorilla        mitchellh   sirupsen
'!burnt!sushi'   fsnotify    joho           monoculum   sourcegraph
 dustin          gobuffalo   mattn          rogpeppe    spf13
 fatih           gofrs       microcosm-cc   sergi

打开 nexus,浏览 go-proxy-all 仓库地址,能看到这些 go module 已经被缓存:

打开顺序排在第一位的 go-proxy-cn 仓库,也能看到这些 go module 已经被缓存:

而排在顺序第二和第三位的代理仓库,则为空:

这说明 go group 类型的仓库,会按照添加的顺序,依次查找缓存的 go module,如果找到,则使用,否则继续查找下一个,直到找到或者查找完所有仓库。

2.1.3 - windows安装

在 windows 上安装 golang

卸载旧版本

如果发现安装有旧版本,会要求先卸载旧版本再继续安装。

备注:不知道为什么,删除旧版本文件的过程非常的慢。

更新:遇到安装完成后,执行 go install 命令时报错:

\xxx\map.go xxxxx mapiterinit redeclared in this block

后来发现先在 windows 的 app 列表中卸载 golang 后,重新安装 golang,问题解决。

因此,强烈建议先手工卸载旧版本,再安装新版本。

安装

运行下载下来的 go1.24.0.windows-amd64.msi 安装文件。

安装过程中需要设置安装路径,我一般不喜欢用默认的 Program Files 路径,通常设置为:D:\sky\work\soft\golang\

安装完成后,打开 cmd 验证一下:

$ go version

go version go1.24.0 windows/amd64

打开 gitbash,检查:

$ go version
go version go1.24.0 windows/amd64

$ which go
/d/sky/work/soft/golang/bin/go

设置环境变量:

  • GOPATH=D:\sky\work\soft\gopath\

  • GOROOT=D:\sky\work\soft\golang\

修改环境变量(用户变量)Path,增加内容 %GOPATH%\bin%GOROOT%\bin

验证环境变量是否生效:

$ env | grep -E 'GOPATH|GOROOT'
GOROOT=D:\sky\work\soft\golang\
GOPATH=D:\sky\work\soft\gopath\

$ go env GOPATH
D:\sky\work\soft\gopath\

$ go env GOROOT
D:\sky\work\soft\golang\

$ echo $PATH

2.1.4 - linux安装

在 linux 上安装 golang

首先删除之前已经安装的版本 :

sudo rm -rf /usr/local/go

解压缩下载下来的 go1.xx.0.linux-amd64.tar.gz 文件:

sudo tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz

设置 GOROOT / GOPATH 然后将 GOROOT/bin 和 GOPATH/bin 加入到 PATH:

# golang
export GOROOT=/usr/local/go
export GOPATH=/home/sky/work/soft/gopath
export PATH=$GOROOT/bin:$GOPATH/bin:$PATH

执行 go version / go env 等检验。

$ go version
go version go1.24.2 linux/amd64

2.1.5 - 设置

golang 安装完成后的设置

设置 GOPROXY

查看默认的 goproxy 设置:

$ go env GOPROXY
https://proxy.golang.org,direct

设置环境变量 GOPROXY 来设置 go module 公共代理仓库,代理并缓存go模块,以加速构建。

# golang
......
export GOPROXY="https://goproxy.cn,direct"

可用的 goproxy 有:

对于通过 nexus 建立了本地代理仓库的情况,设置为本地代理仓库的地址,如:

export GOPROXY="http://192.168.0.246:8081/repository/go-proxy-all/,direct"

设置私有模块

查看默认的 GOPRIVATE 设置,默认为空:

$ go env GOPRIVATE

可以通过设置 GOPRIVATE 环境变量来控制私有仓库、依赖等 (如公司内部仓库) 不通过 goproxy 拉取,而是走本地:

# 设置不走 goproxy 的私有仓库
# 如果有多个则用逗号分隔
export GOPRIVATE=*.someone.com

设置 GOSUMDB

查看默认的 GOSUMDB 设置:

$ go env GOSUMDB
sum.golang.org

如果遇到 GOSUMDB sum.golang.org 连接超时,则需要设置 GOSUMDB:

# 设置不走 goproxy 的私有仓库
# 如果有多个则用逗号分隔
export GOSUMDB=sum.golang.google.cn

参考:https://learnku.com/go/wikis/66836

2.2 - IDE配置

在各个 IDE 中配置 golang

2.2.1 - VS Code 配置

在 VS Code 中配置 golang

extensions

ctrl + shift + x 打开 vs code 的 extension 设置。

安装 go extension

工具软件

Ctrl + Shift + P 打开命令面板,输入 Go: Install/Update Tools,然后勾选所有推荐的工具开始安装。

3 - 基本语法

Golang 基本语法

3.1 - 语法元素

Golang 语法元素

3.1.1 - 注释

Golang 代码注释

Go语言实战

摘录自 Go语言实战 3.5.3 Go 语言的文档一节

为了在 godoc 生成的文档里包含自己的代码文档,开发人员需要用下面的规则来写代码和注释。

在标识符之前,把文档作为注释加入到代码中,这个方式对包、函数、类型和全局变量都适用。注释可以以双斜线开头,也可以用斜线和星号风格:

// Retrieve 连接到配置库,收集各种链接设置、用户名和密码。这个函数在成功时
// 返回 config 结构,否则返回一个错误。
func Retrieve() (config, error) {
	// ...省略
}

如果想给包写一段文字量比较大的文档,可以在工程里包含一个叫作 doc.go 的文件,使用同样的包名,并把包的介绍使用注释加在包名声明之前:

/*
包 usb 提供了用于调用 USB 设备的类型和函数。想要与 USB 设备创建一个新链接,使用 NewConnection
...
*/
package usb

这段关于包的文档会显示在所有类型和函数文档之前。这个例子也展示了如何使用斜线和星 号做注释。

Golang语言规范

https://golang.org/ref/spec#Comments

注释作为程序文档。有两种形式:

  1. 行注释以字符序列 // 开始,止于行末。
  2. 一般注释以字符序列 /* 开始,并以随后的第一个字符序列 */ 停止。

注释不能在符文或字符串字面值内部开始,也不能在注释内部开始。一个不包含换行符的一般注释就像一个空格。任何其他注释的作用就像一个换行符。

Effective Go

https://golang.org/doc/effective_go.html#commentary

Go 提供了 C风格 /* */ 块注释和 C++风格的 // 行注释。行注释是标准的;块注释主要是作为包注释出现的,但在表达式中或禁用大片代码时很有用。

godoc(程序和 web 服务器) 处理 Go 源文件,以提取有关包内容的文档。出现在顶层声明之前的注释,没有中间的换行符,与声明一起被提取出来,作为该项目的解释文本。这些注释的性质和风格决定了 godoc 产生的文档的质量。

每个包都应该有一个包注释,即在包子句之前的块注释。对于多文件的包,包注释只需要出现在一个文件中,任何一个文件都可以。包的注释应该介绍包,并提供与包整体相关的信息。它将首先出现在 godoc 页面上,并且应该构建后面的详细文档。

/*
Package regexp implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

    regexp:
        concatenation { '|' concatenation }
    concatenation:
        { closure }
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

如果包的内容简单,包的注释可以简短一些。

// Package path implements utility routines for
// manipulating slash-separated filename paths.

注释不需要额外的格式化,比如星星的横幅。生成的输出可能甚至不会以固定宽度的字体呈现,所以不要依赖对齐的间距–godoc,像gofmt一样,会照顾到这一点。注释是未解释的纯文本,所以 HTML 和其他注释(如 _this_)会逐字重现,不应使用。godoc 做的一个调整是用固定宽度的字体显示缩进的文本,适合程序片段。fmt包的注释就很好地利用了这一点。

根据上下文的不同,godoc甚至可能不会重新格式化注释,所以确保它们直接看起来不错:使用正确的拼写、标点和句子结构,折叠长行,等等。

在一个包内,任何紧接在顶层声明之前的注释都会作为该声明的 doc 注释。程序中的每一个导出(大写)的名字都应该有一个 doc 注释。

doc注释最好是作为完整的句子来使用,这样就可以有各种各样的自动呈现方式。第一句话应该是一句话的总结,以被声明的名称开始。

// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {

如果每个doc注释都以它所描述的物品名称开头,你可以使用go工具的doc子命令,通过grep运行输出。想象一下,你记不住 “Compile” 这个名字,但又在寻找正则表达式的解析函数,所以你运行了这个命令:

$ go doc -all regexp | grep -i parse

如果包中所有的 doc 注释都以 “This function…” 开头,grep 就不会帮你记住这个名字。但因为软件包中的每条文档注释都以名称开头,你会看到这样的内容,它能让你想起你要找的单词。

$ go doc -all regexp | grep -i parse
    Compile parses a regular expression and returns, if successful, a Regexp
    MustCompile is like Compile but panics if the expression cannot be parsed.
    parsed. It simplifies safe initialization of global variables holding
$

Go的声明语法允许对声明进行分组。一个文档注释可以介绍一组相关的常量或变量。由于整个声明都会被介绍,所以这样的注释往往可以敷衍了事。

// Error codes returned by failures to parse an expression.
var (
    ErrInternal      = errors.New("regexp: internal error")
    ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
    ...
)

分组也可以表示项目之间的关系,比如一组变量是由一个mutex保护的。

var (
    countLock   sync.Mutex
    inputCount  uint32
    outputCount uint32
    errorCount  uint32
)

3.1.2 - Token

Golang Token

备注:摘录自 golang语言规范 https://golang.org/ref/spec#Tokens

Token

Token构成了go语言的词汇。有四类Token:identifiers(标识符)、keywords(关键字)、operators and punctuation(运算符和标点符号)、以及literals(字面量)。

由空格(U+0020)、水平制表符(U+0009)、回车符(U+000D)和换行符(U+000A)形成的空白(whitespace)会被忽略,除非它是用来分隔Token,避免多个Token合并成一个Token。

此外,换行或文件末尾可能会触发分号的插入。

在将输入内容分解成Token的同时,下一个Token是形成有效Token的最长字符序列。

分号

在一些产品中正式语法使用分号"; “作为终结符。Go程序可以使用以下两条规则来省略大部分的分号:

  1. 当输入的内容被分解成Token时,分号会自动插入到Token流中,紧接在一行的最后一个Token之后,如果该Token是… …

    • identifier/标识符
    • 整型,浮点,虚数,rune(符文)或者 字符串字面值
    • break、continue、fallthrough、return等关键字之一
    • 运算符和标点符号 ++, --, ), ], 或 } 中的一种
  2. 为了让复杂的语句只占一行,可以在结尾”) “或”}“前省略分号。

3.1.3 - 标识符

Golang 标识符

Identifiers/标识符

https://golang.org/ref/spec#Identifiers

identifiers/标识符用于命名程序实体,如变量和类型。标识符是由一个或多个字母和数字组成的序列。标识符的第一个字符必须是字母(不能是数字开头)。

identifier = letter { letter | unicode_digit } .

a
_x9
ThisVariableIsExported
αβ

空白标识符

https://golang.org/ref/spec#Blank_identifier

空白标识符由下划线字符 _ 表示。它作为匿名的占位符,而不是常规的(非空白)标识符,在声明、操作数和赋值中具有特殊意义。

预定义的标识符

universe block(宇宙块)中隐式声明了以下标识符:

Types:
	bool byte complex64 complex128 error float32 float64
	int int8 int16 int32 int64 rune string
	uint uint8 uint16 uint32 uint64 uintptr

Constants:
	true false iota

Zero value:
	nil

Functions:
	append cap close complex copy delete imag len
	make new panic print println real recover

导出的标识符

标识符可以导出,以便从另一个包中访问它。标识符在以下两种情况下被导出:

  1. 标识符名称的第一个字符是Unicode大写字母(Unicode class “Lu”);和
  2. 标识符是在package block(包块)中声明的,或者是字段名或方法名。

所有其他的标识符都不会被导出。

标识符的唯一性

给定一组标识符,如果一个标识符与集合中的其他标识符不同,则称为唯一标识符。如果两个标识符的拼写不同,或者它们出现在不同的包中并且没有被导出,那么它们就是不同的。否则,它们是相同的。

3.1.4 - 关键字

Golang 关键字

Keywords/关键字

https://golang.org/ref/spec#Keywords

以下是保留的关键词,不得作为标识符使用。

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

3.1.5 - 运算符

Golang 运算符

Operators and punctuation/运算符和标点符号

https://golang.org/ref/spec#Operators_and_punctuation

以下字符序列代表运算符(包括赋值运算符)和标点符号。

+    &     +=    &=     &&    ==    !=    (    )
-    |     -=    |=     ||    <     <=    [    ]
*    ^     *=    ^=     <-    >     >=    {    }
/    <<    /=    <<=    ++    =     :=    ,    ;
%    >>    %=    >>=    --    !     ...   .    :
     &^          &^=

3.1.6 - 字面量

Golang 字面量

备注:摘录自 golang语言规范 https://golang.org/ref/spec#Integer_literals

Integer literals/整型字面量

整数字面量是代表整数常数的数字序列。可选的前缀设置了一个非十进制的基数:0b0B代表二进制,0, 0o, 或 0O代表八进制,0x0X代表十六进制。单一的0被认为是十进制的0。在十六进制中,字母a到f和A到F代表10到15的数值。

为了便于阅读,下划线字符_可能会出现在基数前缀之后或连续的数字之间;这种下划线不会改变文字的价值。

int_lit        = decimal_lit | binary_lit | octal_lit | hex_lit .
decimal_lit    = "0" | ( "1"  "9" ) [ [ "_" ] decimal_digits ] .
binary_lit     = "0" ( "b" | "B" ) [ "_" ] binary_digits .
octal_lit      = "0" [ "o" | "O" ] [ "_" ] octal_digits .
hex_lit        = "0" ( "x" | "X" ) [ "_" ] hex_digits .

decimal_digits = decimal_digit { [ "_" ] decimal_digit } .
binary_digits  = binary_digit { [ "_" ] binary_digit } .
octal_digits   = octal_digit { [ "_" ] octal_digit } .
hex_digits     = hex_digit { [ "_" ] hex_digit } .
42
4_2
0600
0_600
0o600
0O600       // 第二个字是大写字母 'O'
0xBadFace
0xBad_Face
0x_67_7a_2f_cc_40_c6
170141183460469231731687303715884105727
170_141183_460469_231731_687303_715884_105727

_42         // 标识符,不是整型字面量
42_         // invalid: _ 必须隔开连续的数字
4__2        // invalid: 一次只能有一个_
0_xBadFace  // invalid: _ 必须隔开连续的数字

Floating-point literals/浮点字面量

浮点字面量是浮点常数的十进制或十六进制表示。

十进制浮点字面量由整数部分(小数点)、小数点、分数部分(小数点)和指数部分(e或E后面跟着一个可选的符号和小数点)组成。整数部分或小数部分中的其中一个可以省略;小数点或指数部分中的其中一个可以省略。指数值exp将mantissa(整数和小数部分)的比例为10exp。

十六进制浮点文字由0x或0X前缀、整数部分(十六进制数字)、小数点、小数部分(十六进制数字)和指数部分(p或P,后面跟着一个可选的符号和十进制数字)组成。整数部分或分数部分中的其中一个可以省略;弧度点也可以省略,但指数部分是必须的。(这个语法与IEEE 754-2008 §5.12.3中给出的语法相匹配。)指数值exp将尾数(整数和分数部分)按2exp缩放。

为了便于阅读,下划线字符_可以出现在基数前缀之后或连续的数字之间;这种下划线不会改变字面值。

float_lit         = decimal_float_lit | hex_float_lit .

decimal_float_lit = decimal_digits "." [ decimal_digits ] [ decimal_exponent ] |
                    decimal_digits decimal_exponent |
                    "." decimal_digits [ decimal_exponent ] .
decimal_exponent  = ( "e" | "E" ) [ "+" | "-" ] decimal_digits .

hex_float_lit     = "0" ( "x" | "X" ) hex_mantissa hex_exponent .
hex_mantissa      = [ "_" ] hex_digits "." [ hex_digits ] |
                    [ "_" ] hex_digits |
                    "." hex_digits .
hex_exponent      = ( "p" | "P" ) [ "+" | "-" ] decimal_digits .
0.
72.40
072.40       // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5.         // == 15.0
0.15e+0_2    // == 15.0

0x1p-2       // == 0.25
0x2.p10      // == 2048.0
0x1.Fp+0     // == 1.9375
0X.8p-0      // == 0.5
0X_1FFFP-16  // == 0.1249847412109375
0x15e-2      // == 0x15e - 2 (integer subtraction)

0x.p1        // invalid: mantissa has no digits
1p-2         // invalid: p exponent requires hexadecimal mantissa
0x1.5e-2     // invalid: hexadecimal mantissa requires p exponent
1_.5         // invalid: _ must separate successive digits
1._5         // invalid: _ must separate successive digits
1.5_e1       // invalid: _ must separate successive digits
1.5e_1       // invalid: _ must separate successive digits
1.5e1_       // invalid: _ must separate successive digits

Imaginary literals/虚数字面量

虚数字面量表示复数常数的虚数部分,它由一个整数或浮点字组成,后面是小写字母i。

imaginary_lit = (decimal_digits | int_lit | float_lit) "i" .

为了向后兼容,虚字的整数部分完全由十进制数字(可能还有下划线)组成,即使它以前导0开始,也被认为是一个十进制整数。

0i
0123i         // == 123i for backward-compatibility
0o123i        // == 0o123 * 1i == 83i
0xabci        // == 0xabc * 1i == 2748i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i       // == 0x1p-2 * 1i == 0.25i

Rune literals / 符文字面量

符文字面量代表一个符文常量,是一个用于识别Unicode code point的整数值。符文字面量表示为一个或多个用单引号括起来的字符,如’x’或’\n’。在引号内,除了换行和未转义的单引号外,任何字符都可以出现。单引号字符代表字符本身的Unicode值,而以反斜杠开头的多字符序列则以各种格式编码值。

最简单的形式代表引号内的单个字符;由于go源文本是以UTF-8编码的Unicode字符,所以多个UTF-8编码的字节可以代表单个整数值。例如,字面量 ‘a’ 持有一个字节,代表字面量 a,Unicode U+0061,值0x61,而 “ä “持有两个字节(0xc3 0xa4),代表文字 “a-dieresis”,U+00E4,值0xe4。

几个反斜杠转义符允许任意值被编码为ASCII文本。有四种方法可以将整数值表示为数字常数。\x后面跟着两位十六进制数字;\u后面跟着四位十六进制数字;\U后面跟着八位十六进制数字,以及后面跟着三位八进制数字的反斜杠\。在每一种情况下,字面量的值都是相应基数的数字所代表的值。

虽然这些表示方法都会产生一个整数,但它们的有效范围不同。八进制转义符必须在0到255之间表示一个值。十六进制转义符通过结构满足这个条件。转义符\u和\U代表Unicode码点,所以在它们里面有些值是非法的,特别是那些高于0x10FFFF的值和代用的一半。

在反斜杠之后,某些单字符转义符代表特殊值:

\a   U+0007 alert or bell
\b   U+0008 backspace
\f   U+000C form feed
\n   U+000A line feed or newline
\r   U+000D carriage return
\t   U+0009 horizontal tab
\v   U+000b vertical tab
\\   U+005c backslash
\'   U+0027 single quote  (valid escape only within rune literals)
\"   U+0022 double quote  (valid escape only within string literals)

所有其他以反斜杠开头的序列在符文字里面都是非法的。

rune_lit         = "'" ( unicode_value | byte_value ) "'" .
unicode_value    = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value       = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value   = `\` "x" hex_digit hex_digit .
little_u_value   = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value      = `\` "U" hex_digit hex_digit hex_digit hex_digit
                           hex_digit hex_digit hex_digit hex_digit .
escaped_char     = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
'a'
'ä'
'本'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
'\''         // rune literal containing single quote character
'aa'         // illegal: too many characters
'\xa'        // illegal: too few hexadecimal digits
'\0'         // illegal: too few octal digits
'\uDFFF'     // illegal: surrogate half
'\U00110000' // illegal: invalid Unicode code point

String literals/字符串字面量

字符串字面量表示从字符序列的连接中获得的字符串常量。有两种形式:原始(raw)字符串字元和解释(interpreted)字符串字元。

原始字符串字元是后引号之间的字符序列,如 “`foo`"。在引号内,除了后引号,任何字符都可以出现。原始字符串字面量的值是由引号之间未解释(隐含UTF-8编码)的字符组成的字符串;特别是,反斜杠没有特殊意义,字符串可能包含换行符。原始字符串字元中的回车字符(’\r’)会从原始字符串值中被丢弃。

被解释的字符串字面量是双引号之间的字符序列,如 “bar”。在引号内,除了换行和未转义的双引号外,任何字符都可以出现。引号之间的文字构成了字面量意义的值,反斜杠转义的解释与符文字面意义的解释相同(除了’/‘是非法的,而”/“是合法的),有相同的限制。三位数的八进制(\nnnnn)和两位数的十六进制(\xnn)转义符代表结果字符串的单个字节;所有其他转义符代表单个字符的UTF-8编码(可能是多字节)。因此,在一个字符串中,字面意义中的\377和\xFF代表一个价值0xFF=255的单一字节,而ÿ、\u00FF、\U000000FF和xc3\xbf代表字符U+00FF的UTF-8编码的两个字节0xc3 0xbf。

string_lit             = raw_string_lit | interpreted_string_lit .
raw_string_lit         = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
`abc`                // same as "abc"
`\n
\n`                  // same as "\\n\n\\n"
"\n"
"\""                 // same as `"`
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800"             // illegal: surrogate half
"\U00110000"         // illegal: invalid Unicode code point

这些例子都代表同一个字符串:

"日本語"                                 // UTF-8 input text
`日本語`                                 // UTF-8 input text as a raw literal
"\u65e5\u672c\u8a9e"                    // the explicit Unicode code points
"\U000065e5\U0000672c\U00008a9e"        // the explicit Unicode code points
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"  // the explicit UTF-8 bytes

如果源码将一个字符表示为两个code point,比如涉及重音和字母的组合形式,如果放在符文rune字面量中,结果将是一个错误(它不是单个的code point),如果放在字符串字面量中,将出现两个code point。

3.2 - Golang 基础语法

Golang 的基础语法

3.2.1 - 包

Golang packge

在 Go 语言里,包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能,更好地复用代码,并对每个包内的数据的使用有更好的控制。

包的规则

  • 所有的.go 文件,除了空行和注释,都应该在第一行声明自己所属的包。
  • 每个包都在一个单独的目录里。
    • 不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。
    • 这意味着,同一个目录下的所有.go 文件必须声明同一个包名。

包的命名

  • 给包命名的惯例是使用包所在目录的名字
  • 给包及其目录命名时,应该使用简洁、清晰且全小写的名字

记住:并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。一般情况下,包被导入后会使用你的包名作为默认的名字,不过这个导入后的名字可以修改。

main包

在 Go 语言里,命名为 main 的包具有特殊的含义。 Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。

包的导入

导入包需要使用关键字import ,它会告诉编译器你想引用该位置的包内的代码。如果需要导入多个包,习惯上是将 import 语句包装在一个导入块中。

import (
	"fmt"
	"strings"
)

编译器查找packge的顺序:

  1. 标准库,如 /usr/local/go/src/*/*,这个路径由环境变量GOROOT决定。
  2. 当前项目源代码路径,如 /home/myproject/src/*/*
  3. gopath路径,如/home/sky/work/soft/go/*/*,这个路径由环境变量GOPATH决定。

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

远程导入

Go 支持从远程网站获取源代码,如github:

import "github.com/spf13/viper"

获取过程使用 go get 命令完成

命名导入

重名的包可以通过命名导入来导入。

命名导入是指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。

import (
	"fmt"
	myfmt "mylib/fmt"
)

导入一个不在代码里使用的包时,Go 编译器会编译失败,输出错误。这个特性可以防止导入了未被使用的包,避免代码变得臃肿。

如果需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_ 来重命名这个导入:

import (
	_ "mylib/fmt"
)

init函数

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

多个init函数之间的执行顺序不定,不要依赖这个顺序。

所有被编译器发现的 init 函数都会安排在 main 函数之前执行。

init 函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。

3.2.2 - 常量

Golang 常量

常量的定义

常量的定义与变量类似,只不过使用 const 关键字。

const Pi = 3.14

func main() {
	const World = "world"
	fmt.Println("Hello", World)
	fmt.Println("Happy", Pi, "Day")

	const Truth = true
	fmt.Println("Go rules?", Truth)
}

常量可以是字符、字符串、布尔或数字类型的值。

注意:常量不能使用 := 语法定义。

数值常量

数值常量是高精度的

const (
	Big   = 1 << 100
	Small = Big >> 99
)

一个未指定类型的常量由上下文来决定其类型。

go语言规范

https://golang.org/ref/spec#Constants

https://moego.me/golang_spec.html#id271

常量有 布尔值常量 、 rune 常量 、 整数常量 、 浮点数常量 、 复数常量 和 字符串常量 。 Rune、整数、浮点数和复数常量统称为数值常量。

常量的值是由如下所表示的: rune,整数,浮点数,虚数,字符串字面值,表示常量的标识符,常量表达式,结果为常量的变量转换,或者一些内置函数所生成的值,这些内置函数比如应用于任意值的 unsafe.Sizeof ,应用于一些表达式 的 cap 或 len ,应用于复数常量的 real 和 imag 以及应用于数值常量的 complex 。布尔值是由预先声明的常量 true 和 false 所代表的。预先声明的标识符 iota 表示一个整数常量。

通常,复数常量是 常量表达式 的一种形式,会在该节讨论。

数值常量代表任意精度的确切值,而且不会溢出。因此,没有常量表示 IEEE-754 负零,无穷,以及非数字值集。

常量可以是有类型的也可以是无类型的。字面值常量, true , false , iota 以及一些仅包含无类型的恒定操作数的 常量表达式 是无类型的。

常量可以通过 常量声明 或 变量转换 被显示地赋予一个类型,也可以在 变量声明 或 赋值 中,或作为一个操作数在 表达式 中使用时隐式地被赋予一个类型。如果常量的值不能按照所对应的类型来表示的话,就会出错。「前一版的内容: 比如, 3.0 可以作为任何整数类型或任何浮点数类型,而 2147483648.0 (相当于 1«31 )可以作为 float32 , float64 或 uint32 类型,但不能是 int32 或 string 。」

一个无类型的常量有一个 默认类型 ,当在上下文中需要请求该常量为一个带类型的值时,这个 默认类型 便指向该常量隐式转换后的类型,比如像 i := 0 这样子的 短变量声明 就没有显示的类型。无类型常量的默认类型分别是 bool , rune , int , float64 , complex128 或 string ,取决于它是否是一个布尔值、 rune、整数、浮点数、复数或字符串常量。

实现限制:虽然数值常量在这个语言中可以是任意精度的,但编译器可能会使用精度受限的内部表示法来实现它。也就是说,每一种实现必须:

  • 使用最少 256 位来表示整数。
  • 使用最少 256 位来表示浮点数常量(包括复数常量的对应部分)的小数部分,使用最少 16 位表示其带符号的二进制指数部分。
  • 当无法表示一个整数常量的精度时,需要给出错误。
  • 当因为溢出而无法表示一个浮点数或复数常量时,需要给出错误。
  • 当因为精度限制而无法表示一个浮点数或复数常量时,约到最接近的可表示的常量。

这些要求也适用于字面值常量,以及 常量表达式 的求值结果。

Effective Go

https://golang.org/doc/effective_go.html#constants

Go中的常量仅仅是常量。它们是在编译时创建的,即使是在函数中定义为locals,也只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制,定义它们的表达式必须是常量表达式,可被编译器评估。例如,1«3是一个常量表达式,而math.Sin(math.Pi/4)不是,因为对math.Sin的函数调用需要在运行时发生。

在Go中,使用iota枚举器创建枚举常量。由于 iota 可以成为表达式的一部分,而且表达式可以隐式重复,因此很容易建立复杂的值集。

type ByteSize float64

const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

将String这样的方法附加到任何用户定义的类型上的能力,使得任意值在打印时自动格式化成为可能。虽然你最常看到的是它应用于结构体,但这种技术对于标量类型也很有用,比如浮点类型(如ByteSize)。

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

表达式YB打印为1.00YB,而ByteSize(1e13)打印为9.09TB。

这里使用Sprintf实现ByteSize的String方法是安全的(避免无限期重复),不是因为转换,而是因为它用%f调用Sprintf,而%f不是字符串格式。Sprintf只有在需要字符串时才会调用String方法,而%f需要的是浮点值。

3.2.3 - 变量

Golang 变量

变量定义

var 语句定义一个变量的列表;跟函数的参数列表一样,类型在后面。

var 语句可以定义在包或函数级别。

var c, python, java bool

func main() {
	var i int
	fmt.Println(i, c, python, java)
}

变量初始化

// 变量定义可以包含初始值,每个变量对应一个。
var i, j int = 1, 2

func main() {
    // 如果初始化是使用表达式,则可以省略类型;
    // 变量从初始值中获得类型。
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}

也可以用这个方式初始化多个变量,每个变量一行代码:

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

如果变量在定义时没有明确的初始化,则会赋值为该类型对应的零值

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)

短声明变量

在函数中,:= 简洁赋值语句在明确类型的地方,可以用于替代 var 定义。

func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"
}

函数外的每个语句都必须以关键字开始(varfunc、等等),:= 结构不能使用在函数外。

多赋值模式

上面同时赋值多个变量的模式,可以用于if, for 等语句,实现在if, for 语句中的多赋值模式:

for i, j, s := 0, 5, "a"; i < 3 && j < 100 && s != "aaaaa"; i, j, s = i+1, j+1, s + "a"  {
    fmt.Println("Value of i, j, s:", i, j, s)
}

go语言规范

https://golang.org/ref/spec#Variables

变量是用来存放数值的存储位置。允许的值集由变量的类型决定。

变量声明,或者对于函数参数和结果,函数声明或函数字面量的签名为命名的变量保留了存储空间。调用内置函数new或取复合字面的地址,在运行时为变量分配存储空间。这样的匿名变量是通过(可能是隐含的)指针间接引用的。

数组、切片和结构体类型的结构化变量具有可以单独寻址的元素和字段。每个这样的元素都像一个变量一样。

变量的静态类型(或者说只是类型)是在它的声明中给出的类型,新调用或复合文字中提供的类型,或者是结构变量的元素的类型。接口类型的变量也有一个独特的动态类型,它是在运行时分配给变量的值的具体类型(除非该值是预先声明的标识符nil,它没有类型)。动态类型在执行过程中可能会发生变化,但存储在接口变量中的值总是可以分配给变量的静态类型。

var x interface{}  // x 是 nil,它有一个静态类型 interface{}
var v *T           // v 的值为 nil,静态类型为 *T
x = 42             // x 的值为 42,动态类型为 int
x = v              // x 的值为 (*T)(nil),动态类型为 *T

变量的值通过在表达式中引用变量来检索;它是分配给变量的最新值。如果一个变量还没有被赋值,它的值就是它的类型的零值。

Effective Go

https://golang.org/doc/effective_go.html#variables

变量可以像常量一样被初始化,但初始化器可以是一个在运行时计算的通用表达式。

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

3.2.4 - 指针

Golang 指针

Go 具有指针。 指针保存了变量的内存地址。

基本语法

定义:类型 *T 是指向类型 T 的值的指针。其零值是 nil

var p *int

生成:& 符号会生成一个指向其作用对象的指针。

i := 42
p = &i

取值:* 符号表示指针指向的底层的值。

fmt.Println(*p) // 通过指针 p 读取 i
*p = 21         // 通过指针 p 设置 i

注意:与 C 不同,Go 没有指针运算

3.2.5 - 函数

Golang 函数

语法

Golang的函数语法和Java、C等有非常大的不同:

  • 函数参数定义是类型在变量名之后,而不是变量名之前
  • 函数返回值定义在函数签名之后,而不是函数签名之前
  • 函数可以返回多个值
func swap(x, y string) (string, string) {
	return y, x
}

参数

函数可以没有参数或有多个参数。

注意类型在变量名 之后

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 main() {
	a, b := swap("hello", "world")
	fmt.Println(a, b)
}

Go 的返回值可以被命名,并且像变量那样使用。

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

3.2.6 - Getter

Golang Getter 函数

备注:摘录自 Effective Go https://golang.org/doc/effective_go.html#Getters

Getters

Go并没有提供对getter和setter的自动支持。自己提供getter和setter并没有错,而且这样做通常是合适的,但在getter的名称中加上Get既不习惯也没有必要。如果你有一个叫做 owner 的字段(小写,未导出),那么 getter 方法应该叫做 Owner(大写,导出),而不是 GetOwner。导出时使用大写的名称提供了区分字段和方法的钩子。如果需要的话,setter函数可能会被称为SetOwner。这两个名字在实践中都很好读。

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

3.2.7 - slice

Golang slice

Go Slices: usage and internals

https://blog.golang.org/slices-intro

介绍

Go 的 slice 类型为处理类型化数据序列提供了一种方便而有效的方法。分片类似于其他语言中的数组,但有一些不同寻常的特性。本文将介绍什么是切片以及如何使用它们。

数组

分片类型是建立在Go的数组类型之上的一种抽象,所以要理解分片,我们必须先理解数组。

数组类型定义指定了长度和元素类型。例如,类型 [4]int 表示一个由四个整数组成的数组。数组的大小是固定的,它的长度是其类型的一部分([4]int 和 [5]int 是不同的、不兼容的类型)。数组可以用通常的方式进行索引,所以表达式 s[n] 是访问从零开始的第n个元素。

var a [4]int
a[0] = 1
i := a[0]
// i == 1

数组不需要显式初始化,数组的零值是一个可以直接使用的数组,其元素本身是置零的:

// a[2] == 0, the zero value of the int type

[4]int的内存表示是四个整数值线性排列:

Go的数组是数值。一个数组变量表示整个数组;它不是指向第一个数组元素的指针(在 C 语言中是这样)。这意味着,当您分配或传递一个数组值时,您将复制它的内容。(为了避免复制,你可以传递一个指向数组的指针,但那是指向数组的指针,而不是数组)。有一种方法可以把数组看作是一种结构,但它是有索引而不是命名字段:一个固定大小的复合值。

可以像这样指定一个数组文字:

b := [2]string{"Penn", "Teller"}

或者,你可以让编译器为你计算数组元素:

b := [...]string{"Penn", "Teller"}

在这两种情况下,b的类型都是 [2]string 。

Slices

数组有它们的位置,但它们有点不灵活,所以你在 Go 代码中不太常见。不过,Slices却无处不在。它们建立在数组的基础上,提供了强大的功能和便利。

分片的类型规范是 []T,其中T是分片元素的类型。与数组类型不同,分片类型没有指定的长度。

分片文字的声明就像数组文字一样,只是省略了元素数:

letters := []string{"a", "b", "c", "d"}

可以用内置函数make创建切片,它的签名是:

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

其中T代表要创建的分片的元素类型。make函数需要类型、长度和可选的容量。调用时,make会分配一个数组并返回一个指向该数组的分片:

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

当省略capacity参数时,它默认为指定的长度。下面是相同代码的一个更简洁的版本:

s := make([]byte, 5)

使用内置的 length 和 capacity 函数可以检查切片的长度和容量:

len(s) == 5
cap(s) == 5

接下来的两节将讨论长度和容量之间的关系。

分片的零值是nil。len 和cap 函数都会对nul分片返回0。

分片也可以通过 “切片” 现有的分片或数组来形成。切片是通过指定一个半开放的范围,用冒号隔开两个指数来完成的。例如,表达式 b[1:4] 创建了一个包含b元素1到3的分片(生成的分片的指数为0到2)。

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

分片表达式的 start 和 end 指数是可选的,它们分别默认为零和分片的长度:

// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

这是创建一个给定数组的分片的语法:

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x

切片内部

分片是数组段的描述符,它由指向数组的指针、段的长度和它的容量(段的最大长度)组成。

我们前面通过 make([]byte,5) 创建的变量s,结构是这样的:

长度是指分片指针所指的元素数。容量是底层数组中的元素数(从分片指针所指的元素开始)。在接下来的几个例子中,长度和容量之间的区别将变得很清楚。

当我们分片时,观察分片数据结构的变化以及它们与底层数组的关系:

s = s[2:4]

分片并不复制分片的数据,而是创建一个指向原始数组的新分片值。它创建一个新的分片值,指向原始数组。这使得分片操作与操作数组索引一样高效。因此,修改重新分片的元素(不是分片本身)会修改原始分片的元素:

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

刚才我们把s切成了比它的容量短的长度。我们可以通过再次将s切成片来增长它的容量:

s = s[:cap(s)]

分片的增长不能超过其容量。试图这样做会引起运行时的恐慌(panic),就像在分片或数组的边界之外索引一样。同样,不能将分片重新分割到零以下以访问数组中的早期元素。

增长切片(copy和append函数)

要增加一个分片的容量,必须创建一个新的、更大的分片,并将原来分片的内容复制到其中。这种技术是其他语言的动态数组在幕后实现的工作方式。下一个例子通过创建一个新的分片t,将s的内容复制到t中,然后将分片值t赋给s,从而使s的容量增加一倍。

t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
        t[i] = s[i]
}
s = t

这个常用操作的循环部分,通过内置的copy函数变得更加简单。顾名思义,copy将数据从一个源片复制到一个目标片。它返回复制的元素数量:

func copy(dst, src []T) int

copy函数支持在不同长度的切片之间进行复制(它将只复制到较小数量的元素)。此外,copy还可以处理共享同一个底层数组的源片和目的片,正确处理重叠的分片。

使用copy,我们可以简化上面的代码片段:

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

一个常见的操作是将数据追加到一个分片的末尾。这个函数将字节元素追加到一个字节分片上,如果需要的话,会增加分片,并返回更新后的分片值:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

可以这样使用AppendByte:

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

像AppendByte这样的函数很有用,因为它们提供了对分片生长方式的完全控制。根据程序的特性,可能需要以较小或较大的分片进行分配,或者对重新分配的大小设置一个上限。

但大多数程序不需要完全控制,所以Go提供了一个内置的append函数,对大多数目的都很好,它的签名是:

func append(s []T, x ...T) []T

append函数将元素x追加到分片s的末尾,如果需要更大的容量,则增长分片:

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

要将一个分片附加到另一个分片上,请使用…将第二个参数扩展为一个参数列表:

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

由于分片的零值(nil)就像一个零长度的分片,所以你可以声明一个分片变量,然后在循环中追加到它:

// Filter returns a new slice holding only
// the elements of s that satisfy fn()
func Filter(s []int, fn func(int) bool) []int {
    var p []int // == nil
    for _, v := range s {
        if fn(v) {
            p = append(p, v)
        }
    }
    return p
}

一个可能的 “疑难杂症”

如前所述,重新切割一个分片并不会对底层数组进行复制。完整的数组将被保留在内存中,直到它不再被引用。偶尔,这可能会导致程序在只需要一小块数据的时候,将所有数据保存在内存中。

例如,这个FindDigits函数将一个文件加载到内存中,并在内存中搜索第一组连续的数字数字,将它们作为一个新的分片返回。

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

这段代码的行为很明确,但是返回的 []字节 指向一个包含整个文件的数组。由于分片引用了原来的数组,所以只要分片被保留在周围,垃圾收集器就不能释放数组;文件的几个有用字节将整个内容保留在内存中。

为了解决这个问题,可以在返回之前将感兴趣的数据复制到一个新的分片中:

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

这个函数的一个更简洁的版本可以通过使用append来构建。这是留给读者的一个练习。

进一步阅读

Effective Go包含了对切片和数组的深入处理,Go语言规范定义了切片及其相关的帮助函数。

Slides in Effective Go

https://golang.org/doc/effective_go.html#slices

https://bingohuang.gitbooks.io/effective-go-zh-en/content/08_Data.html

切片通过对数组进行封装,为数据序列提供了更通用、强大而方便的接口。 除了矩阵变换这类需要明确维度的情况外,Go 中的大部分数组编程都是通过切片来完成的。

切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组。 若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。

因此,Read 函数可接受一个切片实参 而非一个指针和一个计数;切片的长度决定了可读取数据的上限。以下为 os 包中 File 类型的 Read 方法签名:

func (file *File) Read(buf []byte) (n int, err error)

该方法返回读取的字节数和一个错误值(若有的话)。若要从更大的缓冲区 b 中读取前 32 个字节,只需对其进行切片即可:

    n, err := f.Read(buf[0:32])

这种切片的方法常用且高效。若不谈效率,以下片段同样能读取该缓冲区的前 32 个字节:

var n int
var err error
for i := 0; i < 32; i++ {
   nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
   n += nbytes
   if nbytes == 0 || e != nil {
      err = e
      break
   }
}

只要切片不超出底层数组的限制,它的长度就是可变的,只需将它赋予其自身的切片即可。 切片的容量可通过内建函数 cap 获得,它将给出该切片可取得的最大长度。 以下是将数据追加到切片的函数。若数据超出其容量,则会重新分配该切片。返回值即为所得的切片。 该函数中所使用的 len 和 cap 在应用于 nil 切片时是合法的,它会返回 0.

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

最终我们必须返回切片,因为尽管 Append 可修改 slice 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的。

向切片追加东西的想法非常有用,因此有专门的内建函数 append。 要理解该函数的设计,我们还需要一些额外的信息,我们将稍后再介绍它。

二维切片

Go 的数组和切片都是一维的。要创建等价的二维数组或切片,就必须定义一个数组的数组, 或切片的切片,就像这样:

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

由于切片长度是可变的,因此其内部可能拥有多个不同长度的切片。在我们的 LinesOfText 例子中,这是种常见的情况:每行都有其自己的长度。

text := LinesOfText{
	[]byte("Now is the time"),
	[]byte("for all good gophers"),
	[]byte("to bring some fun to the party."),
}

有时必须分配一个二维数组,例如在处理像素的扫描行时,这种情况就会发生。 我们有两种方式来达到这个目的。一种就是独立地分配每一个切片;而另一种就是只分配一个数组, 将各个切片都指向它。采用哪种方式取决于你的应用。若切片会增长或收缩, 就应该通过独立分配来避免覆盖下一行;若不会,用单次分配来构造对象会更加高效。 以下是这两种方法的大概代码,仅供参考。首先是一次一行的:

// 分配顶层切片。
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:]
}

Go语言规范中的Slice

https://golang.org/ref/spec#Slice_types

分片是针对一个底层数组的连续段的描述符,它提供了对该数组内有序序列元素的访问。分片类型表示其元素类型的数组的所有分片的集合。元素的数量被称为分片长度,且不能为负。未初始化的分片的值为 nil

SliceType = "[", "]", ElementType .

分片 s 的长度可以被内置函数 len来发现;和数组不同的是,这个长度可能会在执行过程中改变。元素可以被从 0 索引到 len(s) - 1 的整数所寻址到。一个给定元素的分片索引可能比其底层数组的相同元素的索引要小。

分片一旦初始化便始终关联到存放其元素的底层数组。因此分片会与其数组和其它相同数组的分片共享存储区;相比之下,不同的数组总是代表不同的存储区域。

分片底层的数组可以延伸超过分片的末端。 容量 便是对这个范围的测量:它是分片长度和数组内除了该分片以外的长度的和;不大于其容量长度的分片可以从原始分片再分片新的来创建。分片 a 的容量可以使用内置函数 cap(a) 来找到。

对于给定元素类型 T 的新的初始化好的分片值的创建是使用的内置函数 make,它需要获取分片类型、指定的长度和可选的容量作为参数。使用 make 创建的分片总是分配一个新的隐藏的数组给返回的分片值去引用。也就是,执行

make([]T, length, capacity)

就像分配个数组然后再分片它一样来产生相同的分片,所以如下两个表达式是相等的:

make([]int, 50, 100)
new([100]int)[0:50]

如同数组一样,分片总是一维的但可以通过组合来构造高维的对象。数组间组合时,被构造的内部数组总是拥有相同的长度;但分片与分片(或数组与分片)组合时,内部的长度可能是动态变化的。此外,内部分片必须单独初始化。

3.3 - 声明

Golang 声明

3.3.1 - Block

Block 声明

备注:摘录自 Golang语言规范 https://golang.org/ref/spec#Blocks

Blocks

Block/块是在匹配的大括号内的声明和语句序列(可能为空):

Block = "{" StatementList "}" .
StatementList = { Statement ";" } .

除了源码中的显性块,还有隐性块:

  • universe block (宇宙块)包含了所有的go源代码文本。
  • 每个包都有一个 package block (包块),包含该包的所有go源代码文本。
  • 每个文件都有一个 file block (文件块),包含该文件中的所有go源代码文本。
  • 每个 “if”、“for “和 “switch “语句都被认为是在自己的隐含块中。
  • 在 “switch “或 “select “语句中的每个子句都作为一个隐式块。

块可以嵌套,会影响scope/范围。

3.3.2 - 常量

常量声明

Constant declaration

https://golang.org/ref/spec#Constant_declarations

常量声明将标识符列表(常量的名称)与常量表达式列表的值绑定。标识符的数量必须等于表达式的数量,左边的第n个标识符与右边的第n个表达式的值绑定。

ConstDecl      = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec      = IdentifierList [ [ Type ] "=" ExpressionList ] .

IdentifierList = identifier { "," identifier } .
ExpressionList = Expression { "," Expression } .

如果存在类型,所有常量都采用指定的类型,并且表达式对于该类型必须是 assignable /可分配的。如果类型被省略,则常量取对应表达式各自的类型。如果表达式的值是无类型的常量,则声明的常量保持无类型,而常量标识符表示常量值。例如,如果表达式是浮点字面量,常量标识符表示浮点常量,即使字面量的小数部分为零。

const Pi float64 = 3.14159265358979323846
const zero = 0.0         // untyped floating-point constant
const (
	size int64 = 1024
	eof        = -1  // untyped integer constant
)
const a, b, c = 3, 4, "foo"  // a = 3, b = 4, c = "foo", untyped integer and string constants
const u, v float32 = 0, 3    // u = 0.0, v = 3.0

在带括号的const声明列表中,除了第一个ConstSpec之外,表达式列表可以省略。这样的空列表相当于前面第一个非空的表达式列表及其类型(如果有的话)的文本替换。因此,省略表达式列表相当于重复前面的列表。标识符的数量必须等于前一个列表中表达式的数量。与iota常量生成器一起,这种机制允许轻量级的顺序值声明。

const (
	Sunday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Partyday
	numberOfDays  // this constant is not exported
)

3.3.3 - 变量

变量声明

Variable declaration

https://golang.org/ref/spec#Variable_declarations

变量声明创建一个或多个变量,将相应的标识符绑定到它们上面,并给每个变量一个类型和初始值。

VarDecl     = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec     = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
	i       int
	u, v, s = 2.0, 3.0, "bar"
)
var re, im = complexSqrt(-1)
var _, found = entries[name]  // map lookup; only interested in "found"

如果给定了一个表达式列表,则按照赋值规则用表达式初始化变量。否则,每个变量初始化为零值。

如果存在类型,每个变量被赋予该类型。否则,每个变量被赋予赋值中相应初始化值的类型。如果该值是一个无类型的常量,则首先隐式转换为其默认类型;如果是一个无类型的布尔值,则首先隐式转换为类型bool。预先声明的值nil不能用于初始化一个没有显式类型的变量。

var d = math.Sin(0.5)  // d is float64
var i = 42             // i is int
var t, ok = x.(T)      // t is T, ok is bool
var n = nil            // illegal

实现限制:如果一个变量从未被使用,编译器可能会规定在函数体中声明一个变量是非法的。

短变量声明

短变量声明使用这样的语法:

ShortVarDecl = IdentifierList ":=" ExpressionList .

它是有初始化表达式但没有类型的正则变量声明的简写:

"var" IdentifierList = ExpressionList .
i, j := 0, 10
f := func() int { return 7 }
ch := make(chan int)
r, w, _ := os.Pipe()  // os.Pipe() returns a connected pair of Files and an error, if any
_, y, _ := coord(p)   // coord() returns three values; only interested in y coordinate

与普通变量声明不同,短变量声明可以重新声明变量,但前提是这些变量原来在同一个块(如果块是函数体,则在参数列表中)中早先声明过,类型相同,而且至少有一个非空变量是新的。因此,重声明只能出现在多变量的短声明中。重新声明并不引入一个新的变量,它只是给原来的变量分配一个新的值。

field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset)  // redeclares offset
a, a := 1, 2                              // illegal: double declaration of a or no new variable if a was declared elsewhere

短变量声明只能出现在函数内部。在某些情况下,例如 “if”、“for “或 “switch “语句的初始化器,它们可以用来声明本地临时变量。

备注:详见 “重新声明” 一节

3.3.4 - iota

iota 声明

iota

https://golang.org/ref/spec#Iota

在常量声明中,预先声明的标识符 iota 代表连续的非类型整数常量。它的值是该常量声明中各自ConstSpec的索引,从0开始。它可以用来构造一组相关的常量。

const (
	c0 = iota  // c0 == 0
	c1 = iota  // c1 == 1
	c2 = iota  // c2 == 2
)

const (
	a = 1 << iota  // a == 1  (iota == 0)
	b = 1 << iota  // b == 2  (iota == 1)
	c = 3          // c == 3  (iota == 2, unused)
	d = 1 << iota  // d == 8  (iota == 3)
)

const (
	u         = iota * 42  // u == 0     (untyped integer constant)
	v float64 = iota * 42  // v == 42.0  (float64 constant)
	w         = iota * 42  // w == 84    (untyped integer constant)
)

const x = iota  // x == 0
const y = iota  // y == 0

根据定义,iota在同一个ConstSpec中的多次使用都具有相同的值:

const (
	bit0, mask0 = 1 << iota, 1<<iota - 1  // bit0 == 1, mask0 == 0  (iota == 0)
	bit1, mask1                           // bit1 == 2, mask1 == 1  (iota == 1)
	_, _                                  //                        (iota == 2, unused)
	bit3, mask3                           // bit3 == 8, mask3 == 7  (iota == 3)
)

最后一个例子利用了最后一个非空表达式列表的隐式重复。

3.3.5 - 函数

函数声明

Function declaration

https://golang.org/ref/spec#Function_declarations

函数声明将标识符,即函数名,与函数绑定。

FunctionDecl = "func" FunctionName Signature [ FunctionBody ] .
FunctionName = identifier .
FunctionBody = Block .

如果函数的签名声明了结果参数,那么函数体的语句列表必须以终止语句结束。

func IndexRune(s string, r rune) int {
	for i, c := range s {
		if c == r {
			return i
		}
	}
	// invalid: missing return statement
}

函数声明可以省略主体。这样的声明提供了在Go之外实现的函数的签名,例如汇编例程。

func min(x int, y int) int {
	if x < y {
		return x
	}
	return y
}

func flushICache(begin, end uintptr)  // implemented externally

3.3.6 - 类型

类型声明

Type declaration

https://golang.org/ref/spec#Type_declarations

类型声明将标识符,即类型名称,绑定到类型上。类型声明有两种形式:别名声明和类型定义。

TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = AliasDecl | TypeDef .

Alias declarations

别名声明将标识符绑定到给定的类型上:

AliasDecl = identifier "=" Type .

在标识符的范围内,它作为类型的别称:

type (
	nodeList = []*Node  // nodeList and []*Node are identical types
	Polar    = polar    // Polar and polar denote identical types
)

Type definitions

类型定义创建了一个新的、独特的类型,它与给定的类型具有相同的底层类型和操作,并为它绑定了一个标识符。

TypeDef = identifier Type .

新类型被称为 defined type / 定义类型。它不同于任何其他类型,包括它创建时来源的类型。

type (
	Point struct{ x, y float64 }  // Point and struct{ x, y float64 } are different types
	polar Point                   // polar and Point denote different types
)

type TreeNode struct {
	left, right *TreeNode
	value *Comparable
}

type Block interface {
	BlockSize() int
	Encrypt(src, dst []byte)
	Decrypt(src, dst []byte)
}

defined type / 定义类型可以有与之相关的方法。它不继承任何与给定类型绑定的方法,但接口类型的方法集或复合类型的元素保持不变。

// A Mutex is a data type with two methods, Lock and Unlock.
type Mutex struct         { /* Mutex fields */ }
func (m *Mutex) Lock()    { /* Lock implementation */ }
func (m *Mutex) Unlock()  { /* Unlock implementation */ }

// NewMutex has the same composition as Mutex but its method set is empty.
type NewMutex Mutex

// The method set of PtrMutex's underlying type *Mutex remains unchanged,
// but the method set of PtrMutex is empty.
type PtrMutex *Mutex

// The method set of *PrintableMutex contains the methods
// Lock and Unlock bound to its embedded field Mutex.
type PrintableMutex struct {
	Mutex
}

// MyBlock is an interface type that has the same method set as Block.
type MyBlock Block

类型定义可以用来定义不同的布尔、数值或字符串类型,并与它们相关联的方法:

type TimeZone int

const (
	EST TimeZone = -(5 + iota)
	CST
	MST
	PST
)

func (tz TimeZone) String() string {
	return fmt.Sprintf("GMT%+dh", tz)
}

3.3.7 - 方法

方法声明

Method declaration

https://golang.org/ref/spec#Method_declarations

方法是一个带有 receiver (接收者)的函数。方法声明将标识符,即方法名,绑定到一个方法上,并将该方法与接受者的基本类型联系起来。

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

接收者是通过方法名前的额外参数部分指定的。这个参数部分必须声明一个非变量参数,即 receiver(接收者)。它的类型必须是一个 defined type(定义类型)T或者一个指向定义类型T的指针。T被称为接收者的基本类型。接收者基类类型不能是指针或接口类型,它必须与方法定义在同一个包中。该方法被称为绑定到它的接收者基本类型上,并且该方法名称只有在类型T或*T的选择器中才可见。

非空白的接收者标识符必须在方法签名中是唯一的。如果接收者的值没有在方法主体中引用,那么它的标识符可以在声明中省略。一般来说,这也适用于函数和方法的参数。

对于基础类型来说,与它绑定的方法的非空名称必须是唯一的。如果基础类型是结构体类型,则非空的方法和字段名必须是不同的。

给定定义类型Point,声明:

func (p *Point) Length() float64 {
	return math.Sqrt(p.x * p.x + p.y * p.y)
}

func (p *Point) Scale(factor float64) {
	p.x *= factor
	p.y *= factor
}

将方法 Length 和 Scale(接收者类型为*Point)绑定到基本类型Point上。

方法的类型是以接收者为第一参数的函数类型。例如,方法Scale的类型是:

func(p *Point, factor float64)

当然,这样声明的函数不是方法。

备注:可以参考文章 函数——go世界中的一等公民 中的 “方法的本质” 一节

“go里面其实方法就是语法糖,实际上Method就是将receiver作为函数的第一个参数输入的语法糖而已,本质上和函数没有区别”

3.3.8 - 重新声明

重新声明

Redeclaration and reassignment

https://golang.org/doc/effective_go.html#redeclaration

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

旁白一下。上一节的最后一个例子演示了 := 短声明形式的工作细节。调用os.Open的声明如下。

f, err := os.Open(name)

这条语句声明了两个变量,f和err。几行之后,对f.Stat的调用为:

d, err := f.Stat()

这看起来就像它声明了d和err。但请注意,两个语句中都出现了err。这种重复是合法的:err 是由第一条语句声明的,但只是在第二条语句中重新赋值。这意味着对 f.Stat 的调用使用了上面声明的现有err变量,只是给它一个新的值。

在 := 声明中,即使已经声明了一个变量 v,也可以出现,但前提是:

  • 这个声明与v的现有声明在同一个作用域中(如果v已经在外部作用域中声明了,则声明将创建一个新的变量§)。
  • 初始化中的相应值可分配给v,并且
  • 至少还有一个变量是由声明创建的。

这个不寻常的属性是纯粹的实用主义,使得它很容易使用一个单一的err值,例如,在一个长的if-else链中。你会看到它经常被使用。

§ 这里值得注意的是,在Go中,函数参数和返回值的作用域与函数主体相同,尽管它们在词法上出现在包围主体的括号之外。

3.4 - 作用域

Golang 中的作用域

3.4.1 - 标签

标签作用域

Label scope

https://golang.org/ref/spec#Label_scopes

标签由标签语句声明,并在 “break”、“continue “和 “goto “语句中使用。定义一个从未使用过的标签是非法的。与其他标识符不同的是,标签没有块范围,也不会与非标签的标识符冲突。

标签的作用域是它声明时所在的函数的主体,不包括任何嵌套函数的主体。

3.5 - 类型

Golang 的类型

3.5.1 - 概述

Golang 类型概述

Type

https://golang.org/ref/spec#Types

类型决定了一组值,以及对这些值的特定操作和方法。如果有类型名称,可以用类型名称来表示,也可以用类型字面量来指定,由现有的类型组成一个类型。

Type      = TypeName | TypeLit | "(" Type ")" .
TypeName  = identifier | QualifiedIdent .
TypeLit   = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
	    SliceType | MapType | ChannelType .

语言预先声明某些类型名称。其他类型是通过类型声明引入的。复合类型–数组、结构体、指针、函数、接口、切片、映射和通道类型–可以使用类型字面量来构造。

每个类型T都有一个底层类型(underlying type)。如果T是预定义的布尔类型、数值类型、字符串类型之一,或者是类型字面量,那么对应的底层类型就是T本身。否则,T的底层类型是T在其类型声明中引用的类型的底层类型。

type (
	A1 = string
	A2 = A1
)

type (
	B1 string
	B2 B1
	B3 []B1
	B4 B3
)

string、A1、A2、B1、B2的底层类型是string。[]B1、B3和B4的底层类型是[]B1。

方法集

类型可以有一个与之相关的方法集。接口类型的方法集就是它的接口。任何其他类型T的方法集由所有用接收者类型T声明的方法组成。相应的指针类型*T的方法集是用接收者 *TT声明的所有方法的集合(也就是说,它也包含T的方法集)。更多的规则适用于包含嵌入式字段的结构体,如结构体类型一节所述。任何其他类型的方法集都是空的。在方法集中,每个方法必须有一个唯一的非空方法名。

类型的方法集决定了该类型实现的接口和可以使用该类型的接收者调用的方法。

类型和值的属性

https://golang.org/ref/spec#Properties_of_types_and_values

类型一致性

两个类型要么相同,要么不同。

已定义类型(defined type)总是不同于其他类型。否则,如果它们的底层类型字面量在结构上是等价的,那么两个类型就是相同的;也就是说,它们具有相同的字面量结构,相应的组件具有相同的类型。详言之。

  • 如果两个数组类型具有相同的元素类型和相同的数组长度,那么它们就是相同的。
  • 如果两个分片类型具有相同的元素类型,则它们是相同的。
  • 如果两个结构体类型具有相同的字段序列,并且对应的字段具有相同的名称、相同的类型和相同的标签,则两个结构体类型是相同的。来自不同包的非导出字段名总是不同的。
  • 如果两个指针类型具有相同的基础类型,那么它们就是相同的。
  • 如果两个函数类型具有相同数量的参数和结果值,对应的参数和结果类型相同,并且两个函数都是变量,或者都不是变量,那么这两个函数类型就是相同的。参数和结果名称不需要匹配
  • 如果两个接口类型具有相同的方法集,名称相同,函数类型相同,则两个接口类型是相同的。来自不同包的非导出方法名总是不同的。方法的顺序无关紧要
  • 如果两个映射类型有相同的键和元素类型,那么它们就是相同的。
  • 如果两个通道类型具有相同的元素类型和相同的方向,那么它们就是相同的。

给定声明:

type (
	A0 = []string
	A1 = A0
	A2 = struct{ a, b int }
	A3 = int
	A4 = func(A3, float64) *A0
	A5 = func(x int, _ float64) *[]string
)

type (
	B0 A0
	B1 []string
	B2 struct{ a, b int }
	B3 struct{ a, c int }
	B4 func(int, float64) *B0
	B5 func(x int, y float64) *A1
)

type	C0 = B0

这些类型是相同的:

A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5

B0 and C0
[]int and []int
struct{ a, b *T5 } and struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5

B0和B1是不同的,因为它们是由不同的类型定义创建的新类型;func(int,float64) *B0和func(x int,y float64) *[]string是不同的,因为B0与[]string不同。

可分配性

如果以下条件之一适用,则值x可分配给类型T的变量("x is assignable to T")。

  • x的类型与T相同。
  • x的类型V和T的底层类型相同,并且V或T中至少有一个不是定义类型。
  • T是一个接口类型,x实现了T。
  • x是双向通道值,T是通道类型,x的类型V和T具有相同的元素类型,V或T中至少有一个不是定义类型。
  • x是预先声明的标识符nil,T是指针、函数、片、映射、通道或接口类型。
  • x是一个可由T类型的值表示的非类型常量。

可表示性

如果下列条件之一适用,则常数x可由类型为T的值表示:

  • x在由T决定的值集合中。
  • T是一个浮点类型,并且x可以被四舍五入到T的精度而不会溢出。四舍五入使用IEEE 754四舍五入到偶数的规则,但将IEEE负零进一步简化为无符号零。注意,常量值永远不会产生IEEE负零、NaN或无穷大。
  • T是复数类型,x的分量real(x)和 imag(x)可以用T的分量类型(float32或float64)的值表示。
x                   T           x is representable by a value of T because

'a'                 byte        97 is in the set of byte values
97                  rune        rune is an alias for int32, and 97 is in the set of 32-bit integers
"foo"               string      "foo" is in the set of string values
1024                int16       1024 is in the set of 16-bit integers
42.0                byte        42 is in the set of unsigned 8-bit integers
1e10                uint64      10000000000 is in the set of unsigned 64-bit integers
2.718281828459045   float32     2.718281828459045 rounds to 2.7182817 which is in the set of float32 values
-1e-1000            float64     -1e-1000 rounds to IEEE -0.0 which is further simplified to 0.0
0i                  int         0 is an integer value
(42 + 0i)           float32     42.0 (with zero imaginary part) is in the set of float32 values
x                   T           x is not representable by a value of T because

0                   bool        0 is not in the set of boolean values
'a'                 string      'a' is a rune, it is not in the set of string values
1024                byte        1024 is not in the set of unsigned 8-bit integers
-1                  uint16      -1 is not in the set of unsigned 16-bit integers
1.1                 int         1.1 is not an integer value
42i                 float32     (0 + 42i) is not in the set of float32 values
1e1000              float64     1e1000 overflows to IEEE +Inf after rounding

3.5.2 - 布尔类型

Golang 布尔类型

Boolean types

https://golang.org/ref/spec#Boolean_types

布尔类型表示由预先声明的常量 true 和 false 表示的布尔真值的集合。预先声明的布尔类型是bool;它是一个已定义类型。

3.5.3 - 数字类型

Golang 数字类型

Numeric types

https://golang.org/ref/spec#Numeric_types

数值类型表示整数或浮点值的集合。预先声明的与架构无关的数值类型有:

uint8       the set of all unsigned  8-bit integers (0 to 255)
uint16      the set of all unsigned 16-bit integers (0 to 65535)
uint32      the set of all unsigned 32-bit integers (0 to 4294967295)
uint64      the set of all unsigned 64-bit integers (0 to 18446744073709551615)

int8        the set of all signed  8-bit integers (-128 to 127)
int16       the set of all signed 16-bit integers (-32768 to 32767)
int32       the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64       the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

float32     the set of all IEEE-754 32-bit floating-point numbers
float64     the set of all IEEE-754 64-bit floating-point numbers

complex64   the set of all complex numbers with float32 real and imaginary parts
complex128  the set of all complex numbers with float64 real and imaginary parts

byte        alias for uint8
rune        alias for int32

一个n位整数的值是n位宽,用二补码算术(two’s complement arithmetic)表示。

还有一组预先声明的数值类型,其大小与实现有关。

uint     either 32 or 64 bits
int      same size as uint
uintptr  an unsigned integer large enough to store the uninterpreted bits of a pointer value

为了避免可移植性问题,所有的数值类型都是已定义类型,因此除了byte(uint8的别名)和rune(int32的别名)之外,其他类型都是不同的。当在表达式或赋值中混合使用不同的数值类型时,需要进行显式转换。例如,int32和int不是同一类型,即使它们在特定架构上可能具有相同的大小。

3.5.4 - string类型

Golang string类型

String types

https://golang.org/ref/spec#String_types

字符串类型表示字符串值的集合。一个字符串值是一个(可能是空的)字节序列。字节数称为字符串的length/长度,永远不会是负数。字符串是不可改变的:一旦创建,就不可能改变字符串的内容。预先声明的字符串类型是字符串,它是已定义类型。

字符串s的长度可以通过内置函数len来发现。如果字符串是常量,那么长度就是编译时常量。字符串的字节可以通过整数索引0到len(s)-1来访问。取这种元素的地址是非法的;如果 s[i] 是字符串的第 i’th 个字节,&s[i] 是无效的。

参考资料

3.5.5 - 数组类型

Golang 数组类型

go语言实战

类型 [n]T 是一个数组,有 n 个类型为 T 的值。

数组定义访问如下,需要指定类型和数组大小:

var a [10]int

注意:数组的长度是其类型的一部分,因此数组不能改变大小

也可以在定义时直接创建数组,数组大小的设置可以有多种方式:

a := [2]string{"a", "b"}
a := []string{"a", "b"}
a := [...]string{"a", "b"}

通过下标访问单个元素:

var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)

golang语言规范

https://golang.org/ref/spec#Array_types

数组是一个单一类型的元素的计数序列,称为元素类型。元素的数量称为数组的长度,绝不是负数。

ArrayType   = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .

长度是数组类型的一部分;它必须计算为一个非负常数,用int类型的值表示。数组a的长度可以通过内置函数len来计算。元素可以用0到len(a)-1的整数索引来寻址。数组类型总是一维的,但可以组成多维类型。

[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64  // same as [2]([2]([2]float64))

Effective Go

https://golang.org/doc/effective_go.html#arrays

在规划内存的详细布局时,数组很有用,有时可以帮助避免分配,但主要是它们是切片的构建模块,也就是下一节的主题。为了给这个主题打下基础,下面说说关于数组的一些情况。

在Go和C中,数组的工作方式有很大的不同,在Go中:

  • 数组就是值。将一个数组赋值给另一个数组会复制所有的元素。
  • 特别是,如果你把一个数组传递给一个函数,它将收到一个数组的副本,而不是一个指向它的指针。
  • 数组的大小是其类型的一部分。类型 [10]int 和 [20]int 是不同的。

值属性可能很有用,但也很昂贵;如果你想要类似C的行为和效率,你可以传递一个指针给数组。

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Note the explicit address-of operator

但即使这种风格也不是 go 的习惯用法。请用分片(slice)代替。

实现原理

参考文章: go语言数组的实现原理

初始化

Go 语言中的数组有两种不同的创建方式,一种是显式的指定数组的大小,另一种是使用 [...]T 声明数组,Go 语言会在编译期间通过源代码对数组的大小进行推断:

arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}

编译器会对第二种数组的大小进行推导,通过遍历元素的方式来计算数组中元素的数量。

这两种初始化方式在运行时是完全等价的,[...]T 这种初始化方式只是 Go 语言提供的一种语法糖,当不想计算数组中的元素个数时可以减少一些工作。

对于一个由字面量组成的数组,根据数组元素数量的不同,编译器会做两种不同的优化:

  1. 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
  2. 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;

访问和赋值

无论是在栈上还是静态存储区,数组在内存中其实就是一连串的内存空间。

表示数组的方法就是:

  1. 一个指向数组开头的指针
  2. 数组中元素的数量
  3. 数组中元素类型占的空间大小

数组访问越界是非常严重的错误,Go 语言中对越界的判断:

  1. 可以在编译期间由静态类型检查完成的,函数会对访问数组的索引进行验证。数组和字符串的一些简单越界错误都会在编译期间发现,比如我们直接使用整数或者常量访问数组。
  2. 使用变量去访问数组或者字符串时,需要Go 语言运行时在发现数组、切片和字符串的越界操作触发程序的运行时错误

访问数组:

  • 在使用字面量整数访问数组下标时就会生成非常简单的中间代码
  • 当编译器无法对数组下标是否越界无法做出判断时才会加入 PanicBounds 指令交给运行时进行判断

参考资料

3.5.6 - Slice类型

Golang Slice类型

Slice

摘录自 go语言实战

slice 指向一序列的值,并且包含了长度信息。

[]T 是一个元素类型为 T 的 slice。

p := []int{2, 3, 5, 7, 11, 13}

对 slice 切片

slice 可以重新切片,创建一个新的 slice 值指向相同的数组。

表达式s[lo:hi]表示从 lohi-1 的 slice 元素,含两端。因此s[lo:lo]是空的,而s[lo:lo+1]有一个元素。

p := []int{2, 3, 5, 7, 11, 13}
fmt.Println("p ==", p)
fmt.Println("p[1:4] ==", p[1:4])

// 省略下标代表从 0 开始
fmt.Println("p[:3] ==", p[:3])

// 省略上标代表到 len(s) 结束
fmt.Println("p[4:] ==", p[4:])

构造 slice

slice 由函数 make 创建,第二个参数为数组长度:

a := make([]int, 5)  // len(a)=5,cap(a)=5

可以通过第三个参数来指定容量:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

注意:slice的长度可以在构造时通过参数明确指定,也可以在切片时通过上下两个下标计算而来。而容量则需要考虑左下标开始位置。

func main() {
	a := make([]int, 5)	// 指定长度为5,容量没有设置,则和长度相同:len=5 cap=5
	printSlice("a", a)
	b := make([]int, 0, 5) // 指定长度为0,容量为5:len=0 cap=5
	printSlice("b", b)
	c := b[:2] // 切片时长度为下表差,容量计算时需要考虑左下标开始位置,这里的左下标从0开始:len=2 cap=5
	printSlice("c", c)
	d := c[2:5] // 切片时长度为下表差,容量计算时需要考虑左下标开始位置,这里的左下标从2开始:len=3 cap=5
	printSlice("d", d)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %v\n",
		s, len(x), cap(x), x)
}

slice 的零值是 nil。一个 nil 的 slice 的长度和容量是 0。

var z []int
fmt.Println(z, len(z), cap(z))

向 slice 添加元素

Go 提供了一个内建函数 append 向 slice 添加元素:

func append(s []T, vs ...T) []T
  • append 的第一个参数 s 是一个类型为 T 的数组,其余类型为 T 的值将会添加到 slice。
  • append 的结果是一个包含原 slice 所有元素加上新添加的元素的 slice。
  • 如果 s 的底层数组太小,而不能容纳所有值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。
func main() {
	var a []int
	printSlice("a", a)

	// append works on nil slices.
	a = append(a, 0)
	printSlice("a", a)

	// the slice grows as needed.
	a = append(a, 1)
	printSlice("a", a)

	// we can add more than one element at a time.
	a = append(a, 2, 3, 4)
	printSlice("a", a)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %v\n",
		s, len(x), cap(x), x)
}

Slice types

https://golang.org/ref/spec#Slice_types

分片是底层数组的连续段的描述符,它提供了对该数组中元素的编号序列的访问。分片类型表示其元素类型的数组的所有分片的集合。元素的数量称为分片的长度,并且永远不会是负数。未初始化的分片的值为零。

SliceType = "[" "]" ElementType .

片段s的长度可以通过内置函数len发现;与数组不同,它可能在执行过程中发生变化。元素可以通过0到len(s)-1的整数索引来寻址。一个给定元素的分片索引可能小于底层数组中相同元素的索引。

分片一旦被初始化,总是与持有其元素的底层数组相关联。因此,一个分片与它的数组和同一数组的其他分片共享存储;相反,不同的数组总是代表不同的存储。

分片的底层数组可以延伸到分片的末端。capacity/容量是对该范围的衡量:它是切片的长度和切片之外的数组的长度之和;一个长度不大于该容量的切片可以通过从原始切片中切出一个新的切片来创建。切片的capacity/容量可以通过内置函数cap(a)来发现。

一个新的、初始化的给定元素类型T的分片值是使用内置函数make制作的,它需要一个分片类型和指定长度和可选容量的参数。用make创建的分片总是分配一个新的、隐藏的数组,返回的分片值指向这个数组。也就是说,执行

make([]T, length, capacity)

产生的分片与分配一个数组并对其进行分片一样,所以这两个表达式是等价的:

make([]int, 50, 100)
new([100]int)[0:50]

像数组一样,切片总是一维的,但可以组成更高维的对象。对于数组的数组,根据结构,内部数组总是相同的长度;但是对于切片的切片(或切片的数组),内部长度可能会动态变化。此外,内部切片必须单独初始化。

Slice

https://golang.org/doc/effective_go.html#slices

Slices 包裹了数组,为数据序列提供了一个更通用、更强大、更方便的接口。除了具有显式维度的项目(如变换矩阵),Go 中的大多数数组编程都是通过切片而不是简单的数组来完成的。

切片持有对底层数组的引用,如果您将一个切片分配给另一个切片,则两者都指向同一个数组。如果函数接受切片参数,那么它对切片元素的改变将对调用者可见,类似于传递一个指向底层数组的指针。因此,Read函数可以接受切片参数,而不是一个指针和一个计数;切片中的长度设置了一个读取数据的上限。这里是包os中File类型的Read方法的签名。

func (f *File) Read(buf []byte) (n int, err error)

该方法返回读取的字节数和一个错误值(如果有的话)。要读入一个较大的缓冲区buf的前32个字节,需要对缓冲区进行分片(这里用作动词)。

n, err := f.Read(buf[0:32])

这样的分片很常见,也很高效。事实上,暂且不说效率,下面的代码段也会读取缓冲区的前32个字节。

    var n int
    var err error
    for i := 0; i < 32; i++ {
        nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
        n += nbytes
        if nbytes == 0 || e != nil {
            err = e
            break
        }
    }

分片的长度可以改变,只要它仍然适合于底层数组的限制;只要把它分配给自己的一个分片即可。分片的容量,可以通过内置函数cap访问,报告了分片可能承担的最大长度。这里是一个将数据追加到分片的函数。如果数据超过了容量,分片将被重新分配。返回结果的分片。该函数利用len和cap应用于nil slice时是合法的,并返回0。

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

我们必须在事后返回slice,因为虽然Append可以修改slice的元素,但slice本身(持有指针、长度和容量的运行时数据结构)是通过值传递的。

对 slice 进行追加的想法非常有用,它被 append 内置函数所捕获。不过要理解那个函数的设计,我们需要更多的信息,所以我们稍后会回到它。

Two-dimensional slices

https://golang.org/doc/effective_go.html#two_dimensional_slices

TODO:后面再细看

参考资料

3.5.7 - 结构体类型

Golang 结构体类型

Struct

摘录自 go语言实战

结构体是字段的集合。结构体定义的语法:

type Vertex struct {
	X int
	Y int
}

访问范围通过结构体和字段名的首字母大小写来体现:大写为public,小写为private。

构建结构体

可以通过值列表给结构体的各个字段赋值,新分配一个结构体,顺序和结构体定义的字段顺序一致。也可以通过使用 Name: 语法为单个字段赋值,未明确赋值的字段则取缺省值。

var (
	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)

也可以通过指针访问:

v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)

Struct types

https://golang.org/ref/spec#Struct_types

结构体是命名元素(称为字段)的序列,每个字段都有name/名称和type/类型。字段名可以显式(IdentifierList)或隐式(EmbeddedField)指定。在结构体中,非空白的字段名必须是唯一的。

StructType    = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl     = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag           = string_lit .
// An empty struct.
struct {}

// A struct with 6 fields.
struct {
	x, y int
	u float32
	_ float32  // padding
	A *[]int
	F func()
}

一个声明了类型但没有显式字段名的字段称为嵌入式字段。嵌入式字段必须指定为类型名T或指向非接口类型名*T的指针,T本身不能是指针类型。非限定的类型名作为字段名。

// A struct with four embedded fields of types T1, *T2, P.T3 and *P.T4
struct {
	T1        // field name is T1
	*T2       // field name is T2
	P.T3      // field name is T3
	*P.T4     // field name is T4
	x, y int  // field names are x and y
}

以下声明是非法的,因为字段名在结构类型中必须是唯一的。

struct {
	T     // conflicts with embedded field *T and *P.T
	*T    // conflicts with embedded field T and *P.T
	*P.T  // conflicts with embedded field T and *T
}

如果x.f是表示该字段或方法f的合法选择器,那么结构体x中嵌入字段的字段或方法f被称为promoted(提升)。

被提升的字段与结构体的普通字段一样,只是它们不能在结构体的复合字面量中作为字段名使用。

给定一个结构类型S和一个已定义类型T,被提升的方法被包含在结构体的方法集中,具体如下:

  • 如果S包含一个嵌入字段T,那么S和*S的方法集都包含有接收者T的提升方法。*S的方法集还包括具有接收者*T的提升方法。
  • 如果S包含一个内嵌字段*T,那么S和*S的方法集都包含有接收者T或*T的被提升方法。

字段声明后面可以有一个可选的字符串字面量标签(tag),它成为对应字段声明中所有字段的属性。空的标签字符串相当于一个不存在的标签。标签通过反射接口变得可见,并参与结构体的类型识别,但在其他方面被忽略。

struct {
	x, y float64 ""  // an empty tag string is like an absent tag
	name string  "any string is permitted as a tag"
	_    [4]byte "ceci n'est pas un champ de structure"
}

// A struct corresponding to a TimeStamp protocol buffer.
// The tag strings define the protocol buffer field numbers;
// they follow the convention outlined by the reflect package.
struct {
	microsec  uint64 `protobuf:"1"`
	serverIP6 uint64 `protobuf:"2"`
}

参考资料

3.5.8 - 指针类型

Golang 指针类型

Pointer types

https://golang.org/ref/spec#Pointer_types

PointerType = "*" BaseType .
BaseType    = Type .
*Point
*[4]int

参考资料

3.5.9 - 函数类型

Golang 函数类型

Function types

https://golang.org/ref/spec#Function_types

函数类型表示具有相同参数和结果类型的所有函数的集合。未初始化的函数类型变量的值为零。

FunctionType   = "func" Signature .
Signature      = Parameters [ Result ] .
Result         = Parameters | Type .
Parameters     = "(" [ ParameterList [ "," ] ] ")" .
ParameterList  = ParameterDecl { "," ParameterDecl } .
ParameterDecl  = [ IdentifierList ] [ "..." ] Type .

在参数或结果的列表中,名称(IdentifierList)必须全部存在或全部不存在。如果存在,每个名称代表指定类型的一个项目(参数或结果),并且签名中所有非空白名称必须是唯一的。如果不存在,每个类型代表该类型的一个项目。参数和结果列表总是用括号表示,但如果正好有一个未命名的结果,则可以写成一个未括号的类型。

在函数签名中,最后一个输入的参数可以有一个以…为前缀的类型。带有这样参数的函数被称为 variadic 可变参数,可以用零或多个参数来调用该参数。

func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)

3.5.10 - 接口类型

Golang 接口类型

Interface types

https://golang.org/ref/spec#Interface_types

接口类型指定了一个称为 interface/接口的方法集。接口类型的变量可以存储任何类型的值,其方法集是接口的任何超集。这样的类型被称为实现了接口。未初始化的接口类型变量的值是nil。

InterfaceType      = "interface" "{" { ( MethodSpec | InterfaceTypeName ) ";" } "}" .
MethodSpec         = MethodName Signature .
MethodName         = identifier .
InterfaceTypeName  = TypeName .

接口类型可以通过方法规范明确地指定方法,也可以通过接口类型名称嵌入其他接口的方法。

// A simple File interface.
interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Close() error
}

每个显式指定方法的名称必须是唯一的,不能是空白的。

interface {
	String() string
	String() string  // illegal: String not unique
	_(x int)         // illegal: method must have non-blank name
}

可以有多个类型实现一个接口。例如,如果两个类型S1和S2的方法集为

func (p T) Read(p []byte) (n int, err error)
func (p T) Write(p []byte) (n int, err error)
func (p T) Close() error

(其中T代表S1或S2),那么File接口是由S1和S2同时实现的,不管S1和S2可能有什么其他方法或共享什么方法。

类型实现了由其方法的任何子集组成的任何接口,因此可以实现几个不同的接口。例如,所有类型都实现空接口。

interface{}

类似地,考虑这个接口规范,它出现在一个类型声明中,定义了一个叫做Locker的接口。

type Locker interface {
	Lock()
	Unlock()
}

如果S1和S2也实现:

func (p T) Lock() {  }
func (p T) Unlock() {  }

它们实现了Locker接口和File接口。

接口T可以使用一个(可能是限定的)接口类型名E来代替方法规范。这就是所谓的在T中嵌入(embedding)接口E。 T的方法集是T的显式声明方法和T的嵌入式接口的方法集的联合。

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

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

// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface {
	Reader  // includes methods of Reader in ReadWriter's method set
	Writer  // includes methods of Writer in ReadWriter's method set
}

方法集的联合体包含了每个方法集的(导出的和未导出的)方法,每个方法集只有一次,而且名称相同的方法必须有相同的签名。

type ReadCloser interface {
	Reader   // includes methods of Reader in ReadCloser's method set
	Close()  // illegal: signatures of Reader.Close and Close are different
}

接口类型T不得将自己或任何嵌入T的接口类型,递归地嵌入。

// illegal: Bad cannot embed itself
type Bad interface {
	Bad
}

// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {
	Bad2
}
type Bad2 interface {
	Bad1
}

Interface

https://golang.org/doc/effective_go.html#interfaces

Go中的接口提供了一种指定对象行为的方法:如果某个东西可以做这个,那么它就可以在这里使用。我们已经看到了几个简单的例子;自定义的打印机可以通过一个String方法实现,而Fprintf可以通过一个Write方法向任何东西生成输出。只有一个或两个方法的接口在Go代码中很常见,通常会被赋予一个由方法派生的名称,比如io.Writer用于实现Write方法的东西。

类型可以实现多个接口。例如,一个集合如果实现了 sort.Interface,就可以通过包 sort 中的例程进行排序,其中包含 Len()、Less(i, j int) bool 和 Swap(i, j int),它还可以有一个自定义的格式器。在这个人为的例子中,Sequence同时满足这两个条件。

type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    sort.Sort(s)
    str := "["
    for i, elem := range s { // Loop is O(N²); will fix that in next example.
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

3.5.11 - Map类型

Golang Map类型

Map

备注:内容摘录自 go语言实战

map 在使用之前必须用 make 而不是 new 来创建;值为 nil 的 map 是空的,并且不能赋值。

m := make(map[string]string) // 语法是 "map[key的类型]value的类型"
m["key1"] = "value1"
fmt.Println(m["key1"])

或者直接通过指定key、value来创建:

var m = map[string]string{
	"key1": "value1",
	"key2": "value2",
	"key3": "value3",	// 注意最后一行的结尾也必须有逗号
}

value的类型可以忽略,比如下面这种写法:

var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

可以简化为:

var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}

修改 map:

func main() {
	m := make(map[string]int)

	m["Answer"] = 42	//在 map 中插入或修改
	fmt.Println("The value:", m["Answer"])  // 通过key获取值

	m["Answer"] = 48 //在 map 中插入或修改
	fmt.Println("The value:", m["Answer"])

	delete(m, "Answer") // 从 map 中删除key
	fmt.Println("The value:", m["Answer"])

	v, ok := m["Answer"] // 双赋值检测某个键存在,如果存在则第二个参数返回true
	fmt.Println("The value:", v, "Present?", ok)
}

Map types

https://golang.org/ref/spec#Map_types

Map是由一种类型(称为元素类型)的元素组成的无序组,由另一种类型的一组唯一键(称为键类型)索引。未初始化的map的值为nil。

MapType     = "map" "[" KeyType "]" ElementType .
KeyType     = Type .

比较运算符 == 和 != 必须为键类型的操作数完全定义;因此键类型不能是函数、映射或分片。如果键类型是接口类型,必须为动态键值定义这些比较运算符,否则会引起运行时的恐慌。

map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}

Map元素的数量称为其长度。对于map m来说,它可以通过内置函数len来发现,并且在执行过程中可能会改变。在执行过程中可以使用赋值来添加元素,并使用索引表达式来检索;可以使用内置的 delete 函数来删除元素。

使用内置函数make制作一个新的、空的map值,该函数将map类型和一个可选的容量提示作为参数。

make(map[string]int)
make(map[string]int, 100)

初始容量并不约束其大小:为了容纳其中存储的项目数量map可以增长,但 nil map除外。nil map相当于一个空map,但不能添加任何元素。

Maps

https://golang.org/doc/effective_go.html#maps

Map是一种方便而强大的内置数据结构,它将一种类型的值(键)与另一种类型的值(元素或值)关联起来。键可以是任何定义了 equality 运算符的类型,如整数、浮点和复数、字符串、指针、接口(只要动态类型支持equality)、结构体和数组。切片不能用作映射键,因为在它们上面没有定义equality。和切片一样,map 也持有对底层数据结构的引用。如果你把一个map传给一个函数,该函数改变了map的内容,那么这些改变将在调用者中可见。

Map可以使用通常的复合文字语法和冒号分隔的键值对来构建,所以在初始化时很容易构建它们。

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

分配和获取映射值在语法上看起来就像对数组和切片做同样的事情一样,只是索引不需要是一个整数。

offset := timeZone["EST"]

试图用map中不存在的键来获取一个map值,将返回map中条目类型的0值。例如,如果map中包含整数,查找一个不存在的键将返回0。集合可以实现为一个值类型为bool的map。将map条目设置为true,将值放入集合中,然后通过简单的索引进行测试。

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

有时你需要区分缺失的条目和零值。是否有 “UTC “的条目,还是因为map中根本没有这个条目而为0?你可以用一种多重赋值的形式来区分。

var seconds int
var ok bool
seconds, ok = timeZone[tz]

出于明显的原因,这被称为 “逗号 ok “习惯用法。在这个例子中,如果tz存在,秒数将被适当设置,ok将为true;如果不存在,秒数将被设置为0,ok将为false。这里有一个函数,把它和一个漂亮的错误报告放在一起。

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

为了测试map中是否存在,而不用担心实际值,你可以使用空白标识符(_)来代替通常的变量的值。

_, present := timeZone[tz]

要删除一个map条目,使用delete内置函数,其参数是map和要删除的键。即使key已经不在map上,这样做也是安全的。

delete(timeZone, "PDT")  // Now on Standard Time

参考资料

3.5.12 - Channel类型

Golang Channel类型

Channel types

https://golang.org/ref/spec#Channel_types

通道为并发执行的函数提供了一种机制,通过发送和接收指定元素类型的值进行通信。未初始化的通道的值为nil。

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

可选的 <- 操作符指定通道方向,发送或接收。如果没有给出方向,则通道是双向的。通道可以通过赋值或显式转换来限制只能发送或只能接收。

chan T          // can be used to send and receive values of type T
chan<- float64  // can only be used to send float64s
<-chan int      // can only be used to receive ints

<-操作符与最左边的chan可能关联。

chan<- chan int    // same as chan<- (chan int)
chan<- <-chan int  // same as chan<- (<-chan int)
<-chan <-chan int  // same as <-chan (<-chan int)
chan (<-chan int)

可以使用内置的函数make制作一个新的、初始化的通道值,该函数将通道类型和一个可选的容量作为参数。

make(chan int, 100)

capacity,以元素数为单位,设置通道中缓冲区的大小。如果容量为零或不存在,则通道是无缓冲的,只有当发送方和接收方都准备好时,通信才会成功。否则,通道被缓冲,如果缓冲区没有满(发送)或不空(接收),则通信成功而不阻塞。nil通道永远不会为通信做好准备。

可以用内置函数close关闭通道。接收运算符的多值赋值形式报告在通道关闭前是否有接收值被发送。

单个通道可以用于发送语句、接收操作,以及任何数量的goroutine对内置函数cap和len的调用,而无需进一步同步。通道是先入先出(first-in-first-out)的队列。例如,如果goroutine在通道上发送数值,而第二个goroutine接收数值,那么数值将按照发送的顺序接收。

3.6 - 表达式

Golang 的表达式

Expressions

https://golang.org/ref/spec#Expressions

表达式指定了通过对操作数应用运算符和函数来计算值。

操作数

操作数表示表达式中的基本值。操作数可以是字面量,一个代表常量、变量、函数的非空标识符(可能是限定的),或者带括号内的表达式。

空白的标识符只能作为操作数出现在赋值的左侧。

Operand     = Literal | OperandName | "(" Expression ")" .
Literal     = BasicLit | CompositeLit | FunctionLit .
BasicLit    = int_lit | float_lit | imaginary_lit | rune_lit | string_lit .
OperandName = identifier | QualifiedIdent .

Qualified identifiers

限定的标识符是用包名前缀限定的标识符。包名和标识符都不能为空。

QualifiedIdent = PackageName "." identifier .

限定的标识符访问不同包中的标识符,这个标识符必须被导入。该标识符必须被导出,并在该包的包块中声明。

math.Sin	// denotes the Sin function in package math

3.7 - 语句

Golang 语法中的语句

3.7.1 - 语句概述

Golang 语法中的语句概述

https://golang.org/ref/spec#Statements

语句控制执行。

Statement =
	Declaration | LabeledStmt | SimpleStmt |
	GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
	FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
	DeferStmt .

SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

3.7.2 - 终止语句

Golang 的终止语句

终止语句

https://golang.org/ref/spec#Terminating_statements

终止语句可以防止执行同一块中在它之后出现的所有词法上的语句。以下语句是终止语句。

  1. “return “或 “goto “语句。
  2. 对内置函数panic的调用。
  3. 语句列表以终止语句结束的块。
  4. “if “语句,其中
    • “else “分支存在,并且
    • 两个分支都是终止语句。
  5. “for “语句,其中。
    • 没有指向 “for “语句的 “break “语句,并且:
    • 循环条件不存在。
  6. “switch “语句,其中。
    • 没有 “break “语句指的是 “switch “语句。
    • 有一个default case,和
    • 语句列表中的每一种情况,包括默认情况,都以终止语句结束,或可能标有 “fallthrough"语句。
  7. “select"语句,其中:
    • 没有指向 “select “语句的 “break “语句,并且:
    • 在每种情况下,包括默认情况下的语句列表,都以终止语句结束。
  8. 标签语句标注一个终止语句。

所有其他语句都不是终止语句。

如果语句列表不是空的,并且它的最后一条非空语句是终止语句,则该语句列表以终止语句结束。

3.7.3 - 表达式语句

Golang 的表达式语句

表达式语句

https://golang.org/ref/spec#Expression_statements

除了特定的内置函数外,函数和方法的调用以及接收操作都可以在语句上下文中出现。这种语句可以用括号。

ExpressionStmt = Expression .

语句上下文中不允许使用以下内置函数:

append cap complex imag len make new real
unsafe.Alignof unsafe.Offsetof unsafe.Sizeof
h(x+y)
f.Close()
<-ch
(<-ch)
len("foo")  // illegal if len is the built-in function

3.7.4 - Send语句

Golang 的 Send 语句

Send语句

https://golang.org/ref/spec#Send_statements

发送语句在通道上发送一个值。通道表达式必须是通道类型,通道方向必须允许发送操作,要发送的值的类型必须可以分配给通道的元素类型。

SendStmt = Channel "<-" Expression .
Channel  = Expression .

在通信开始之前,通道和值表达式都会被评估。通信会被阻塞,直到发送可以继续进行。如果接收者准备好了,在无缓冲通道上的发送就可以进行。在缓冲通道上的发送,如果缓冲区有空间,就可以进行。在已关闭通道上的发送会引起运行时恐慌。在nil通道上的发送会永远阻塞。

ch <- 3  // send value 3 to channel ch

3.7.5 - 自增自减语句

Golang 的自增自减语句

IncDec/自增自减语句

“++“和”–“语句以无类型常数1来递增或递减操作数。和赋值一样,操作数必须是可寻址的(addressable),或者是一个映射索引表达式。

IncDecStmt = Expression ( "++" | "--" ) .

下列赋值语句在语义上是等价的:

IncDec statement    Assignment
x++                 x += 1
x--                 x -= 1

3.7.6 - 赋值语句

Golang 的赋值语句

Assignment/赋值

https://golang.org/ref/spec#Assignments

Assignment = ExpressionList assign_op ExpressionList .

assign_op = [ add_op | mul_op ] "=" .

每个左操作数必须是可寻址的(addressable),是一个映射索引表达式,或者是(仅对于=赋值)空白标识符。操作数可以加括号。

x = 1
*p = f()
a[i] = 23
(k) = <-ch  // same as: k = <-ch

赋值操作 x op= y,其中op是一个二进制算术运算符,相当于 x = x op (y),但只对x进行一次评估。op= 结构是一个单一的Token。在赋值操作中,左手和右手的表达式列表必须正好包含一个单值表达式,而且左手表达式不能是空白标识符。

a[i] <<= 2
i &^= 1<<n

元组(tuple)赋值将多值操作的各个元素赋值到变量列表中。有两种形式。在第一种形式中,右手的操作数是一个单一的多值表达式,如函数调用、通道或map操作、或类型断言。左手操作数的数量必须与值的数量相匹配。例如,如果f是一个返回两个值的函数。

x, y = f()

将第一个值赋给x,第二个值赋给y,在第二种形式中,左边的操作数必须等于右边的表达式数量,每个表达式必须是单值,右边的第n个表达式被赋给左边的第n个操作数。

one, two, three = '一', '二', '三'

空白标识符提供了一种在赋值中忽略右侧值的方法。

_ = x       // evaluate x but ignore it
x, _ = f()  // evaluate f() but ignore second result value

赋值分两个阶段进行。首先,左边的索引表达式的操作数和指针直指(包括选择器中的隐式指针直指)以及右边的表达式都按照通常的顺序进行评估。第二,按照从左到右的顺序进行赋值。

a, b = b, a  // exchange a and b

x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2  // set i = 1, x[0] = 2

i = 0
x[i], i = 2, 1  // set x[0] = 2, i = 1

x[0], x[0] = 1, 2  // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)

x[1], x[3] = 4, 5  // set x[1] = 4, then panic setting x[3] = 5.

type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7  // set x[2] = 6, then panic setting p.x = 7

i = 2
x = []int{3, 5, 7}
for i, x[i] = range x {  // set i, x[2] = 0, x[0]
	break
}
// after this loop, i == 0 and x == []int{3, 5, 3}

在赋值中,每个值都必须可以赋给被赋值的操作数的类型,但有以下特殊情况:

  1. 任何类型的值都可以分配给空白标识符。
  2. 如果一个非类型的常量被赋值给接口类型的变量或空白标识符,那么该常量首先被隐式转换为其默认类型。
  3. 如果一个非类型的布尔值被分配给接口类型的变量或空白标识符,它首先被隐式转换为布尔类型。

3.8 - 控制流程

Golang 语法中的控制流程

流程控制语句属于特殊的语句。

3.8.1 - if 语句

Golang 中的 if 语句

If语句

类似for循环,go中的if语句也是同样,if后面不能有括号,而if里面必须要有花括号:

if x < 0 {
    return sqrt(-x) + "i"
}

for 一样,if 语句可以在条件之前执行一个简单的语句:

if v := math.Pow(x, n); v < lim {
    // v在这里可以访问
    return v
}
// v在这里不可以访问

注意:这个语句定义的变量的作用域仅在 if 范围之内,包括else:

if v := math.Pow(x, n); v < lim {
    return v
} else {
    // else这里可以访问
    fmt.Printf("%g >= %g\n", v, lim)
}

If statements

https://golang.org/ref/spec#If_statements

“if “语句根据布尔表达式的值指定两个分支的条件执行。如果表达式的值为真,则执行 “if “分支,否则,如果存在,则执行 “else “分支。

IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
if x > max {
	x = max
}

表达式前面可以有一个简单的语句,在表达式被评估之前执行。

if x := f(); x < y {
	return x
} else if x > z {
	return z
} else {
	return y
}

if

https://golang.org/doc/effective_go.html#if

在 go 中,一个简单的if是这样的:

if x > 0 {
    return y
}

强制括号鼓励在多行上写简单的if语句。无论如何这样做都是好的风格,特别是当正文中包含一个控制语句,如返回或break时。

由于if和switch接受一个初始化语句,所以通常会看到一个用来设置局部变量的语句。

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

在 Go 库中,你会发现,当一个 if 语句没有流入下一个语句时–也就是说,正文以 break、continue、goto 或 return 结尾时,不必要的 else 会被省略。

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

这是一个常见情况的例子,代码必须防范一连串的错误情况。如果成功的控制流向下运行,在出现错误情况时消除错误情况,那么代码的阅读效果就会很好。由于错误情况往往以return语句结束,因此产生的代码不需要 else语句。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

3.8.2 - switch 语句

Golang 中的 switch 语句

go语言实战

switch的语法和for、if类似,同样的括号和花括号使用规则,同样的容许在switch前执行一个简单的语句,同样的变量访问范围限制:

switch os := runtime.GOOS; os {
    case "darwin":
    	fmt.Println("OS X.")
    case "linux":
    	fmt.Println("Linux.")
    default:
    	fmt.Printf("%s.", os)
}

特别需要支出的是,和c、java中的switch语句不同,golang中的switch在命中某个case子语句并执行完成之后,会自动终结分支并结束switch语句。这是默认行为,和c,java中会自动继续下一个分支匹配,需要明确break才能退出不同。

如果想继续执行后面的case子语句,需要在case子语句最后使用 fallthrough 语句。

执行顺序

switch 的条件从上到下顺序执行,当匹配成功的时候终止。

switch i {
    case 0:
    case f():
}

i==0 时不会调用 f

if变体

没有条件的 switch 等同于 switch true ,这个变体可以用更清晰的形式来编写多个判断条件的 if 语句:

t := time.Now()
// 等同于 switch true {
switch {
    case t.Hour() < 12:
    	fmt.Println("Good morning!")
    case t.Hour() < 17:
    	fmt.Println("Good afternoon.")
    default:
    	fmt.Println("Good evening.")
}

golang语言规范

https://golang.org/ref/spec#Switch_statements

“switch “语句提供多向执行。表达式或类型指定符与 “switch “内的 “case “进行比较,以确定执行哪个分支。

SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .

有两种形式:表达式switch和类型switch。在表达式switch中,case包含表达式,与switch表达式的值进行比较。在类型switch中,case中包含的类型是与特别注释的switch表达式的类型进行比较的。switch表达式在一个switch语句中只被评估一次。

表达式switch

在表达式switch中,对switch表达式进行评估,并从左到右、从上到下评估case表达式,case表达式不一定是常量,第一个等于switch表达式的会触发执行相关case的语句,其他case则跳过。如果没有匹配的情况,并且有一个 “default"情况,则执行它的语句。最多只能有一个 default case,它可能出现在 “switch “语句的任何地方。缺少的switch表达式相当于布尔值true。

ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .

如果switch表达式的值是一个无类型常量,则首先隐式转换为其默认类型;如果是一个无类型布尔值,则首先隐式转换为布尔类型。预先声明的无类型值nil不能作为switch表达式使用。

如果一个case表达式是无类型的,它首先被隐式转换为switch表达式的类型。对于每个(可能转换的)case表达式x和switch表达式的值t,x == t必须是一个有效的比较。

换句话说,switch表达式被当作是用来声明和初始化一个没有显式类型的临时变量t;正是t的那个值与每个case表达式x进行了相等性测试。

在一个case或default子句中,最后一个非空语句可以是一个(可能被标记为)“fallthrough “语句,以表明控制权应该从这个子句的末尾流向下一个子句的第一个语句。否则控制流向 “switch “语句的末尾。“fallthrough “语句可以作为一个表达式switch的所有分句的最后一条语句出现,但最后一个分句除外。

switch表达式之前可以有一个简单的语句,该语句在表达式被评估之前执行。

switch tag {
default: s3()
case 0, 1, 2, 3: s1()
case 4, 5, 6, 7: s2()
}

switch x := f(); {  // missing switch expression means "true"
case x < 0: return -x
default: return x
}

switch {
case x < y: f1()
case x < z: f2()
case x == 4: f3()
}

实现限制。编译器可能不允许多个case表达式求值于同一个常量。例如,目前的编译器不允许在case表达式中使用重复的整数、浮点或字符串常量。

类型switch

类型switch比较的是类型而不是值。在其他方面,它类似于表达式switch。它由一个特殊的switch表达式标记,它具有使用保留字类型而不是实际类型的类型断言形式。

switch x.(type) {
// cases
}

然后,case将实际类型T与表达式x的动态类型进行匹配,与类型断言一样,x必须是接口类型,案例中列出的每个非接口类型T必须实现x的类型,类型switch的case中列出的类型必须全部不同。

TypeSwitchStmt  = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause  = TypeSwitchCase ":" StatementList .
TypeSwitchCase  = "case" TypeList | "default" .
TypeList        = Type { "," Type } .

TypeSwitchGuard可以包括一个简短的变量声明。当使用这种形式时,变量被声明在每个子句隐含块的TypeSwitchCase的末尾。在有一个case的子句中,正好列出一个类型,变量具有该类型;否则,变量具有TypeSwitchGuard中表达式的类型。

case可以使用预先声明的标识符nil代替类型;当TypeSwitchGuard中的表达式是一个nil接口值时,就会选择该case。最多可以有一个nil case。

给定一个类型为interface{}的表达式x,下面的类型switch。

switch i := x.(type) {
case nil:
	printString("x is nil")                // type of i is type of x (interface{})
case int:
	printInt(i)                            // type of i is int
case float64:
	printFloat64(i)                        // type of i is float64
case func(int) float64:
	printFunction(i)                       // type of i is func(int) float64
case bool, string:
	printString("type is bool or string")  // type of i is type of x (interface{})
default:
	printString("don't know the type")     // type of i is type of x (interface{})
}

可以重写:

v := x  // x is evaluated exactly once
if v == nil {
	i := v                                 // type of i is type of x (interface{})
	printString("x is nil")
} else if i, isInt := v.(int); isInt {
	printInt(i)                            // type of i is int
} else if i, isFloat64 := v.(float64); isFloat64 {
	printFloat64(i)                        // type of i is float64
} else if i, isFunc := v.(func(int) float64); isFunc {
	printFunction(i)                       // type of i is func(int) float64
} else {
	_, isBool := v.(bool)
	_, isString := v.(string)
	if isBool || isString {
		i := v                         // type of i is type of x (interface{})
		printString("type is bool or string")
	} else {
		i := v                         // type of i is type of x (interface{})
		printString("don't know the type")
	}
}

在类型switch保护之前可以有一个简单的语句,在保护被评估之前执行。

在类型转换中不允许使用 “fallthrough “语句。

Effective Go

https://golang.org/doc/effective_go.html#switch

go的switch比C语言的switch更通用。表达式不需要是常数,甚至不需要是整数,case从上到下进行评估,直到找到匹配的情况,如果switch没有表达式,它就会切换到true。因此,把一个if-else-if-else链写成switch是可能的,也是很习惯的。

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

没有自动跌破,但 case 可以用逗号分隔的列表呈现。

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

虽然它们在Go中并不像其他一些类似C语言那样常见,但break语句可以用来提前终止一个开关。不过有时候,需要脱离周围的循环,而不是switch,在Go中,可以通过给循环加上一个标签,然后对这个标签进行 “break “来实现。这个例子展示了这两种用法。

Loop:
	for n := 0; n < len(src); n += size {
		switch {
		case src[n] < sizeOne:
			if validateOnly {
				break
			}
			size = 1
			update(src[n])

		case src[n] < sizeTwo:
			if n+1 >= len(src) {
				err = errShortInput
				break Loop
			}
			if validateOnly {
				break
			}
			size = 2
			update(src[n] + src[n+1]<<shift)
		}
	}

当然,continue语句也接受一个可选的标签,但它只适用于循环。

在本节的最后,这里有一个 byte slice 的比较例程,它使用了两个switch语句。

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

类型switch

也可以使用swtich来发现接口变量的动态类型。这样的类型切换使用了类型断言的语法,括号内有关键字type。如果switch在表达式中声明了一个变量,那么该变量在每个子句中都会有相应的类型。在这种情况下重用名称也是一种习惯,实际上是在每个情况下声明一个新的变量,名称相同,但类型不同。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

3.8.3 - for 语句

Golang 中的 for 语句

go语言实战

for loop

Go 只有一种循环结构 for 循环。

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

和Java的语法相反:

  1. for 后面没有括号(),注意是强制一定不能有
  2. 循环体必须有{}

跟 C 或者 Java 中一样,可以让前置、后置语句为空:

sum := 1
for ; sum < 1000; {
    sum += sum
}

这就非常类似C、Java中的while循环了,因此干脆继续简写,省略掉分括号:

sum := 1
for sum < 1000 {
    sum += sum
}

更绝一点,死循环:

for {
}

golang语言规范

https://golang.org/ref/spec#For_statements

“for “语句指定重复执行块。有三种形式。迭代可以由一个条件(condition)、一个 “for “子句或一个 “range “子句控制。

ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .

单一条件的 for 语句

在最简单的形式中,“for “语句指定重复执行一个块,只要一个布尔条件评估为真。该条件在每次迭代前都会被评估。如果条件不存在,则相当于布尔值为true。

for a < b {
	a *= 2
}

带有for子句的for语句

带有 ForClause 的 “for “语句也是由它的条件(condition)控制的,但除此之外,它还可以指定一个 init 和一个post语句,如赋值、增量或减量语句。init 语句可以是一个简短的变量声明,但post语句不能。init语句声明的变量在每次迭代中都会被重复使用。

ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
PostStmt = SimpleStmt .
for i := 0; i < 10; i++ {
	f(i)
}

如果非空,则在评估第一次迭代的条件之前,init语句会被执行一次;post语句会在每次执行块之后执行(而且只有在块被执行的情况下)。ForClause 的任何元素都可以是空的,但分号是必须的,除非只有一个条件。如果条件不存在,则相当于布尔值为true。

for cond { S() }    is the same as    for ; cond ; { S() }
for      { S() }    is the same as    for true     { S() }

Effective Go

https://golang.org/doc/effective_go.html#for

Go for循环与C的循环类似-但不一样。它统一了for和while,而且没有do-while。有三种形式,其中只有一种有分号。

// Like a C for (除了不容许用括号)
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

短声明使其很容易在循环中直接声明索引变量。

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

最后,Go没有逗号运算符,++和–是语句而不是表达式。因此,如果你想在for中运行多个变量,你应该使用并行赋值(尽管这排除了++和–)。

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

3.8.4 - for range 语句

Golang 中的 for range 语句

go语言规范

For statements with range clause

https://golang.org/ref/spec#For_statements

带有 “range"子句的 “for” 语句会遍历数组、切片、字符串或map的所有条目,或通道(channel)上接收的值。对于每个条目,如果存在,它将迭代值分配给相应的迭代变量,然后执行该块。

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

“range"子句中右边的表达式称为range表达式,它可以是数组、数组指针、分片、字符串、map或允许接收操作的通道。与赋值一样,如果存在,左边的操作数必须是可寻址或映射索引表达式;它们表示迭代变量。如果范围表达式是通道,最多允许一个迭代变量,否则最多可以有两个。如果最后一个迭代变量是空白标识符,那么范围子句就相当于没有该标识符的同一个子句。

范围表达式x在开始循环之前被评估一次,但有一个例外:如果最多存在一个迭代变量,并且len(x)是常数,则不评估范围表达式。

左边的函数调用在每次迭代时都会被评估一次。对于每一次迭代,如果存在各自的迭代变量,就会产生如下的迭代值。

Range expression                          1st value          2nd value

array or slice  a  [n]E, *[n]E, or []E    index    i  int    a[i]       E
string          s  string type            index    i  int    see below  rune
map             m  map[K]V                key      k  K      m[k]       V
channel         c  chan E, <-chan E       element  e  E
  1. 对于数组、数组指针或切片值a,从元素索引0开始,按递增顺序产生索引迭代值。如果最多存在一个迭代变量,则 range 循环产生从0到 len(a)-1 的迭代值,并且不对数组或分片本身进行索引。对于nil slice,迭代次数为0. 对于字符串值,“range循环"会产生从 0 到 len(a)-1 的迭代值,并且不对数组或分片本身进行索引。

  2. 对于字符串值,“range"子句从字节索引 0 开始对字符串中的Unicode code point 进行迭代。在连续迭代时,索引值将是字符串中连续的UTF-8编码 code point 的第一个字节的索引,第二个值,类型为 rune,将是对应code point的值。如果迭代遇到无效的UTF-8序列,第二个值将是0xFFFD,即Unicode replacement(替换)字符,下一次迭代将推进字符串中的一个字节。

  3. 对map的迭代顺序没有指定,也不能保证每次迭代的顺序相同。如果在迭代过程中删除了一个尚未到达的map条目,将不会产生相应的迭代值。如果在迭代过程中创建了一个map条目,该条目可能在迭代过程中产生,也可能被跳过。对于每个创建的条目,以及从一个迭代到下一个迭代,选择可能不同。如果map为nil,则迭代次数为0。

  4. 对于通道,产生的迭代值是通道上连续发送的值,直到通道关闭。如果通道为nil,则range表达式永远阻塞。

迭代值像赋值语句一样被赋值到相应的迭代变量中。

迭代变量可以由 “range “子句使用短变量声明的形式(:=)来声明。在这种情况下,它们的类型被设置为各自迭代值的类型,它们的作用域是 “for"语句的块;它们在每次迭代中被重复使用。如果迭代变量是在 “for"语句之外声明的,那么在执行后它们的值将是最后一次迭代的值。

var testdata *struct {
	a *[7]int
}
for i, _ := range testdata.a {
	// testdata.a is never evaluated; len(testdata.a) is constant
	// i ranges from 0 to 6
	f(i)
}

var a [10]string
for i, s := range a {
	// type of i is int
	// type of s is string
	// s == a[i]
	g(i, s)
}

var key string
var val interface{}  // element type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
	h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]

var ch chan Work = producer()
for w := range ch {
	doWork(w)
}

// empty a channel
for range ch {}

Effective Go

https://golang.org/doc/effective_go.html#for

如果你正在循环数组、切片、字符串或map,或者从一个通道读取,range子句可以管理循环。

for key, value := range oldMap {
    newMap[key] = value
}

如果你只需要range内的第一项(键或索引),就放弃第二项。

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

如果你只需要范围中的第二项(值),使用空白的标识符(下划线)来丢弃第一项。

sum := 0
for _, value := range array {
    sum += value
}

对于字符串,range 为你做了更多的工作,通过解析UTF-8分解出各个Unicode code point。错误的编码会消耗一个字节,并产生替换符U+FFFD。(名称(与相关的内置类型) rune是单个Unicode code point的go术语。详情请参见语言规范。) 。循环

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

打印:

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7

参考资料

3.8.5 - range 语句

Golang 中的 range 语句

rang遍历

range 关键字用来遍历 listarray 或者 map。为了方便理解,可以认为 range 等效于 for earch index of

对于 arrays 或者 slices, 将会返回整型的下标;

// 对于数组,rang返回index
a := [...]string{"a", "b", "c", "d"}
for i := range a {
    fmt.Println("Array item", i, "is", a[i])
}

支持返回单值或者两个值, 如果返回一个值,那么为下标,否则为下标和下标所对应的值。

a := [...]string{"a", "b", "c", "d"}
for i, v := range a {
    fmt.Println("Array item", i, "is", v)
}

对于 map,将会返回下一个键值对的 key

// 对于map, range 返回 key 
capitals := map[string] string {"France":"Paris", "Italy":"Rome", "Japan":"Tokyo" }
for key := range capitals {
    fmt.Println("Map item: Capital of", key, "is", capitals[key])
}

同样支持返回两个值, 直接拿到key和对应的value:

capitals := map[string] string {"France":"Paris", "Italy":"Rome", "Japan":"Tokyo" }
for key, value := range capitals {
    fmt.Println("Map item: Capital of", key, "is", value)
}

3.8.6 - go 语句

Golang 中的 go 语句

golang语言规范

https://golang.org/ref/spec#Go_statements

“go"语句在同一地址空间内,以独立的并发控制线程或goroutine的形式开始执行一个函数调用。

GoStmt = "go" Expression .

表达式必须是函数或方法调用,不能用括号。内建函数的调用与表达式语句一样受到限制。

函数值和参数在调用goroutine中像往常一样被评估,但与常规调用不同的是,程序的执行不会等待被调用的函数完成,而是在新的goroutine中开始独立执行。相反,函数在新的goroutine中开始独立执行。当函数终止时,它的goroutine也会终止。如果函数有任何返回值,则在函数完成时将其丢弃。

go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)

3.8.7 - select 语句

Golang 中的 select 语句

go语言实战

select是Golang中的控制语句,语法类似于switch语句。但是select只用于通信,要求每个case必须是IO操作。

不带default语句的select会阻塞直到某个case满足:

ch1 := make (chan int, 1)
ch2 := make (chan int, 1)

select {
case <-ch1:
    fmt.Println("ch1 pop one element")
case <-ch2:
    fmt.Println("ch2 pop one element")
}

如果两个case同时满足,则随机执行某个case语句,其他case语句不会执行。

如果不想阻塞,则可以带上default子语句:

select {
case <-ch1:
    fmt.Println("ch1 pop one element")
case <-ch2:
    fmt.Println("ch2 pop one element")
default:
    fmt.Println("not ready yet")
}

如果两个case条件都不满足,则直接跳到 default 流程而不阻塞。

golang语言规范

https://golang.org/ref/spec#Select_statements

“select"语句选择一组可能的发送或接收操作中的某一个进行。它看起来类似于 “switch"语句,但情况都是指通信操作(指IO操作)。

SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt   = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr   = Expression .

带有RecvStmt的情况下,可以将RecvExpr的结果分配给一个或两个变量,这些变量可以使用短变量声明。RecvExpr必须是一个(可能是括号)接收操作。最多只能有一个default缺省情况,它可以出现在case列表中的任何地方。

“select “语句的执行分几个步骤进行:

  1. 对于语句中的所有case,在进入 “select “语句后,接收操作的通道操作数和发送语句的通道和右侧表达式都会按照源代码顺序精确地评估一次。其结果是一组可以接收或发送的通道,以及相应的发送值。无论选择哪种(如果有的话)通信操作进行,该评估中的任何副作用都会发生。RecvStmt左侧带有短变量声明或赋值的表达式尚未被评估。

  2. 如果有一个或多个通信可以进行,则通过统一的伪随机选择选择一个可以进行的单一通信。否则,如果有默认case,则选择该case。如果没有default case,“select “语句就会阻塞,直到至少有一个通信可以继续进行。

  3. 除非选择的case是默认情况,否则会执行相应的通信操作。

  4. 如果选择的case是一个带有短变量声明或赋值的RecvStmt,则左手边的表达式被评估,接收到的值(或数值)被赋值。

  5. 所选case的语句列表被执行。

由于在nul通道上的通信永远无法进行,所以只有nil通道而没有 default case 的 select 永远阻塞。

var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
	print("received ", i1, " from c1\n")
case c2 <- i2:
	print("sent ", i2, " to c2\n")
case i3, ok := (<-c3):  // same as: i3, ok := <-c3
	if ok {
		print("received ", i3, " from c3\n")
	} else {
		print("c3 is closed\n")
	}
case a[f()] = <-c4:
	// same as:
	// case t := <-c4
	//	a[f()] = t
default:
	print("no communication\n")
}

for {  // send random sequence of bits to c
	select {
	case c <- 0:  // note: no statement, no fallthrough, no folding of cases
	case c <- 1:
	}
}

select {}  // block forever

3.8.8 - return 语句

Golang 中的 return 语句

golang语言规范

https://golang.org/ref/spec#Return_statements

函数F中的 “return"语句会终止F的执行,并可选地提供一个或多个结果值。在F返回给调用者之前,任何被F deferred 的函数都会被执行。

ReturnStmt = "return" [ ExpressionList ] .

在没有结果类型的函数中,“return"语句不能指定任何结果值。

func noResult() {
	return
}

有三种方法可以从一个具有结果类型的函数中返回值:

  1. 返回值可以在 “return"语句中明确列出。每个表达式必须是单值的,并且可以分配给函数结果类型的相应元素。

    func simpleF() int {
    	return 2
    }
    
    func complexF1() (re float64, im float64) {
    	return -7.0, -4.0
    }
    
  2. “return"语句中的表达式列表可以是对一个多值函数的单次调用。其效果就好比该函数返回的每个值都被分配到一个临时变量中,其类型为相应的值,然后用 “return"语句列出这些变量,这时就适用前一种情况的规则。

    func complexF2() (re float64, im float64) {
    	return complexF1()
    }
    
  3. 如果函数的结果类型为其结果参数指定了名称,那么表达式列表可以为空。结果参数作为普通的局部变量,函数可以根据需要为它们赋值。return 语句返回这些变量的值。

    func complexF3() (re float64, im float64) {
    	re = 7.0
    	im = 4.0
    	return
    }
    
    func (devnull) Write(p []byte) (n int, _ error) {
    	n = len(p)
    	return
    }
    

无论如何声明,所有的结果值在进入函数时都初始化为其类型的零值。指定结果的 “return” 语句会在执行任何deferred 函数之前设置结果参数。

实现限制:如果在返回的地方有不同的实体(常量、类型或变量)与结果参数同名,编译器可以不允许在 “return” 语句中使用空表达式列表。

3.8.9 - break 语句

Golang 中的 break 语句

golang语言规范

https://golang.org/ref/spec#Break_statements

“break “语句终止了同一函数中最内层的 “for”、“switch “或 “select “语句的执行。

BreakStmt = "break" [ Label ] .

如果有标签,则标签必须包围住 “for”、“switch “或 “select “语句,而且标签是执行终止。

3.8.10 - continue 语句

Golang 中的 continue 语句

golang语言规范

https://golang.org/ref/spec#Continue_statements

“continue"语句在其post语句处开始最里面的 “for"循环的下一次迭代。“for"循环必须在同一个函数内。

ContinueStmt = "continue" [ Label ] .

如果有标签,则标签必须包含 “for” 语句。

RowLoop:
	for y, row := range rows {
		for x, data := range row {
			if data == endOfRow {
				continue RowLoop
			}
			row[x] = data + bias(x, y)
		}
	}

3.8.11 - goto 语句

Golang 中的 goto 语句

golang语言规范

“goto” 语句将控制权转移到同一函数中带有相应标签的语句。

GotoStmt = "goto" Label .
goto Error

执行 “goto"语句不能导致在goto之后有任何变量进入它还没有进行的范围。例如,这个例子:

	goto L  // BAD
	v := 3
L:

是错误的,因为跳转到标签L时跳过了v的创建。

块外的 “goto"语句不能跳转到该块内的标签。例如,这个例子:

if n%2 == 1 {
	goto L1
}
for n > 0 {
	f()
	n--
L1:
	f()
	n--
}

是错误的,因为标签L1在 “for"语句的块内,而goto却不在。

3.8.12 - fallthrough 语句

Golang 中的 fallthrough 语句

golang语言规范

https://golang.org/ref/spec#Fallthrough_statements

“fallthrough"语句将控制权转移到 “switch” 语句中下一个case子句的第一条语句。它只能作为这种子句中的最后一个非空语句使用。

参考资料

  • go语言fallthrough的用法心得
    • Go里面 switch 默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。
    • fallthrough不能用在switch的最后一个分支
    • fallthrough到下一个case块时,不执行case匹配检查!不执行case匹配检查!不执行case匹配检查!

特别注意最后一条,有点和常识不符合(我原本理解的fallthrough只是取消break,然后继续做下一个case的匹配,但实际fallthrough把case匹配检查也取消了):

switch {
case true:
   fmt.Println("The integer was <= 5")
   fallthrough
case false:
   fmt.Println("The integer was <= 6")
   fallthrough
default:
   fmt.Println("default case")
}

打印结果:

The integer was <= 5
The integer was <= 6
default case

3.8.13 - defer 语句

Golang 中的 defer 语句

go语言实战

defer 语句会延迟函数的执行直到外层函数返回,通常用于执行清理操作。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

其他使用场景,如释放mutex:

mu.Lock()
defer mu.Unlock()

打印footer:

printHeader()
defer printFooter()
printContent1()
printContent2()

使用事项:

  1. 调用所需的参数会立刻评估

    func a() {
        i := 0
        defer fmt.Println(i)
        i++
        return
    }
    

    这里会打印0,因为 defer fmt.Println(i) 执行时i为0,参数在此时确定,后面的改动不会影响defer语句的参数。

  2. 多个defer调用会入栈,后进先出

    func b() {
        for i := 0; i < 4; i++ {
            defer fmt.Print(i)
        }
    }
    

    这里会打印3210,顺序和defer语句的顺序相反。

  3. defer语句有机会修改函数返回值

    func c() (i int) {
        defer func() { i++ }()
        return 1
    }
    

    这里的函数返回值会被defer修改,从而返回2。

golang语言规范

“defer"语句调用函数,该函数的执行被推迟到外围函数返回的那一刻,这可能是因为外围函数执行了一个返回语句,到达了其函数体的终点,或者是因为相应的goroutine发生panic/恐慌。

DeferStmt = "defer" Expression .

表达式必须是函数或方法调用,不能用括号。内置函数的调用与表达式语句一样受到限制。

每次执行 “defer"语句时,函数值和调用的参数都会像往常一样被评估并重新保存,但实际函数不会被调用。相反,deferred的函数在外围函数return之前立即被调用,其顺序与defer的顺序相反。也就是说,如果外围函数通过一个显式return语句返回,则在该return语句设置了任何结果参数之后,但在函数return给调用者之前,defer函数会被执行。如果一个defer函数的值评价为nil,则在函数被调用时,而不是在 “defer"语句被执行时,执行就会panic/慌乱。

例如,如果defer函数是一个函数字面量,而外围的函数有命名的结果参数,这些结果参数在字面量的范围内,那么defer函数可以在结果参数被返回之前访问和修改它们。如果defer函数有任何返回值,那么当函数完成时,它们将被丢弃。(也请参见关于处理恐慌的章节。)

lock(l)
defer unlock(l)  // unlocking happens before surrounding function returns

// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
	defer fmt.Print(i)
}

// f returns 42
func f() (result int) {
	defer func() {
		// result is accessed after it was set to 6 by the return statement
		result *= 7
	}()
	return 6
}

Effective Go

https://golang.org/doc/effective_go.html#defer

Go 的 defer 语句安排在执行 defer 的函数返回之前立即运行函数调用(defer函数)。这是一种不寻常但有效的方法,用于处理一些情况,例如无论函数走哪条路径返回,都必须释放资源。规范的例子是解锁mutex或关闭文件。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

defer对Close这样的函数的调用有两个好处。首先,它保证你永远不会忘记关闭文件,如果你后来编辑函数添加了新的返回路径,就很容易犯这个错误。第二,它意味着close坐在open的附近,这比把它放在函数的最后要清楚得多。

defer函数的参数(如果函数是方法,则包括接收方)是在defer执行时评估的,而不是在调用执行时评估的。除了避免担心变量在函数执行时改变值之外,这意味着单个defer调用点可以defer多个函数的执行。下面是一个例子:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

defer函数是按照 LIFO 顺序执行的,所以这段代码会在函数返回时导致 4 3 2 1 0 被打印出来。更有价值的例子是通过程序跟踪函数执行的简单方法。我们可以写几个简单的跟踪例程,比如这样:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

我们可以更好地利用 defer 函数的参数在 defer 执行时被评估这一事实。追踪例程可以设置未追踪例程的参数。这个例子:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

打印:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

对于习惯了其他语言的块级资源管理的程序员来说,defer可能看起来很奇特,但它最有趣和最强大的应用恰恰来自于它不是基于块而是基于函数。

参考资料

4 - 语言特性

Golang 语言特性

4.1 - 函数

Golang 语言特性中的函数

4.1.1 - 函数概述

golang 函数语言特性概述

在go的世界中,函数是一等公民,可以给变量赋值,可以作为参数传递,也可以直接赋值。

多值返回

https://golang.org/doc/effective_go.html#multiple-returns

Go的一个不同寻常的特点是,函数和方法可以返回多个值。这种形式可以用来改进C程序中的几个笨拙的习语:带内错误返回,如-1代表EOF和修改按地址传递的参数。

在C语言中,写错误的信号是一个负数,错误代码被秘密存放在一个易失性的位置。在Go中,Write可以返回计数(count)和错误(error)。“是的,你写了一些字节,但不是全部,因为你填满了设备”. 来自包os的文件上的Write方法的签名是。

func (file *File) Write(b []byte) (n int, err error)

和文档中说的一样,当 n != len(b) 时,它返回写入的字节数和一个非nil错误。这是一种常见的风格;更多的例子请参见错误处理一节。

类似的方法避免了传递指针到返回值以模拟引用参数的需要。下面是一个简单的函数,用于从字节片中的某个位置抓取一个数字,返回数字和下一个位置。

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

你可以用它来扫描输入切片b中的数字,像这样。

    for i := 0; i < len(b); {
        x, i = nextInt(b, i)
        fmt.Println(x)
    }

命名结果参数

Go函数的返回或结果 “参数 “可以被赋予名称,并作为常规变量使用,就像传入参数一样。当命名时,它们在函数开始时被初始化为其类型的零值;如果函数执行一个没有参数的返回语句,结果参数的当前值被用作返回值。

这些名称并不是强制性的,但它们可以使代码更短、更清晰:它们是文档。如果我们给 nextInt 的结果命名,就会很明显地知道哪个返回的 int 是哪个。

func nextInt(b []byte, pos int) (value, nextPos int) {

因为被命名的结果是初始化的,并且与一个不加修饰的返回相联系,所以它们可以简化以及澄清。下面是一个很好地使用它们的 io.ReadFull 版本。

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

defer

详见 defer 语句。

函数的本质

摘录自 函数——go世界中的一等公民 “函数的本质” 一节

在go的世界中,函数是一等公民,可以给变量赋值,可以作为参数传递,也可以直接赋值。

函数在go语言里的本质其实就是指向 __TEXT 段内存地址的一个指。

参考资料

4.1.2 - init 函数

Golang 中的 init 函数

Effective Go

https://golang.org/doc/effective_go.html#init

最后,每个源文件都可以定义自己的niladic init函数来设置任何需要的状态。(其实每个文件都可以有多个init函数。) 而finally的意思是:init是在包中的所有变量声明都评估了初始化器之后才被调用的,而那些初始化器只有在所有导入的包都有初始化器之后才被评估。(其实每个文件都可以有多个init函数。)而finally的意思是最后:init是在包中所有的变量声明都评估了它们的初始化器之后才被调用的,而这些初始化器只有在所有的导入包都被初始化之后才被评估。

除了不能用声明来表达的初始化之外,init函数的一个常见用途是在真正执行开始之前验证或修复程序状态的正确性。

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

参考资料

五分钟理解golang的init函数

https://zhuanlan.zhihu.com/p/34211611

init函数的主要作用:

  • 初始化不能采用初始化表达式初始化的变量。
  • 程序运行前的注册。
  • 实现 sync.Once 功能。
  • 其他

init函数的主要特点:

  • init函数先于main函数自动执行,不能被其他函数调用;
  • init函数没有输入参数、返回值;
  • 每个包可以有多个init函数;
  • 包的每个源文件也可以有多个init函数,这点比较特殊;
  • 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
  • 不同包的init函数按照包导入的依赖关系决定执行顺序。

golang程序初始化:

golang程序初始化先于main函数执行,由runtime进行初始化,初始化顺序如下:

  1. 初始化导入的包(包的初始化顺序并不是按导入顺序(“从上到下”)执行的,runtime需要解析包依赖关系,没有依赖的包最先初始化,与变量初始化依赖关系类似
  2. 初始化包作用域的变量(该作用域的变量的初始化也并非按照“从上到下、从左到右”的顺序,runtime解析变量依赖关系,没有依赖的变量最先初始化);
  3. 执行包的init函数;

几个值得注意的地方:

  • 初始化顺序:变量初始化 -> init() -> main()
  • 同一个包不同源文件的init函数执行顺序,golang spec没做说明,以上述程序输出来看,执行顺序是源文件名称的字典序。
  • init函数不可以被调用,上面代码会提示:undefined: init
  • init函数比较特殊,可以在包里被多次定义。
  • init函数的主要用途:初始化不能使用初始化表达式初始化的变量
  • golang对没有使用的导入包会编译报错,但是有时我们只想调用该包的init函数,不使用包导出的变量或者方法:
    • import _ "net/http/pprof"

4.1.3 - 函数调用

Golang 中的函数调用

理解Go语言中的函数调用

https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-function-call/

调用惯例

调用惯例是调用方和被调用方对于参数和返回值传递的约定。

  • c

    当我们在 x86_64 的机器上使用 C 语言中调用函数时,参数都是通过寄存器和栈传递的,其中:

    • 六个以及六个以下的参数会按照顺序分别使用 edi、esi、edx、ecx、r8d 和 r9d 六个寄存器传递;
    • 六个以上的参数会使用栈传递,函数的参数会以从右到左的顺序依次存入栈中;

    而函数的返回值是通过 eax 寄存器进行传递的,由于只使用一个寄存器存储返回值,所以 C 语言的函数不能同时返回多个值。

  • Go 语言使用栈传递参数和接收返回值,所以它只需要在栈上多分配一些内存就可以返回多个值。

思考:

C 语言和 Go 语言在设计函数的调用惯例时选择也不同的实现。C 语言同时使用寄存器和栈传递参数,使用 eax 寄存器传递返回值;而 Go 语言使用栈传递参数和返回值。我们可以对比一下这两种设计的优点和缺点:

  • C 语言的方式能够极大地减少函数调用的额外开销,但是也增加了实现的复杂度;
    • CPU 访问栈的开销比访问寄存器高几十倍3
    • 需要单独处理函数参数过多的情况;
  • Go 语言的方式能够降低实现的复杂度并支持多返回值,但是牺牲了函数调用的性能;
    • 不需要考虑超过寄存器数量的参数应该如何传递;
    • 不需要考虑不同架构上的寄存器差异;
    • 函数入参和出参的内存空间需要在栈上进行分配;

Go 语言使用栈作为参数和返回值传递的方法是综合考虑后的设计,选择这种设计意味着编译器会更加简单、更容易维护。

参数传递

除了函数的调用惯例之外,Go 语言在传递参数时是传值还是传引用也是一个有趣的问题,这个问题影响的是当我们在函数中对入参进行修改时会不会影响调用方看到的数据。我们先来介绍一下传值和传引用两者的区别:

  • 传值:函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据;
  • 传引用:函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方。

不同语言会选择不同的方式传递参数,Go 语言选择了传值的方式,无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝

备注:Go的方式和Java是类似的。Java也是传值,如果是对象也传递对象句柄的值)。

总结:

  • Go 语言中对于整型和数组类型的参数都是值传递的
    • 如果数组很大,传值方式(拷贝)会对性能造成比较大的影响
  • 传递结构体时:会对结构体中的全部内容进行拷贝;
  • 传递结构体指针时:会对结构体指针进行拷贝;

Go 语言在传递参数时其实使用的就是传值的方式,接收方收到参数时会对这些参数进行复制;

摘要:函数调用的过程

摘录自 函数——go世界中的一等公民 “函数调用的过程” 一节

在go语言中,每一个goroutine持有一个连续栈,栈基础大小为2kb,当栈大小超过预分配大小后,会触发栈扩容,也就是分配一个大小为当前栈2倍的新栈,并且将原来的栈拷贝到新的栈上。使用连续栈而不是分段栈的目的是,利用局部性优势提升执行速度,原理是CPU读取地址时会将相邻的内存读取到访问速度比内存快的多级cache中,地址连续性越好,L1、L2、L3 cache命中率越高,速度也就越快。

在go中,和其他一些语言有所不同,函数的返回值、参数都是由被caller保存。每次函数调用时,会在caller的栈中压入函数返回值列表、参数列表、函数返回时的PC地址,然后更改bp和pc为新函数,执行新函数,执行完之后将变量存到caller的栈空间中,利用栈空间中保存的返回地址和caller的栈基地址,恢复pc和sp回到caller的执行过程。

对于栈变量的访问是通过bp+offset的方式来访问,而对于在堆上分配的变量来说,就是通过地址来访问。在go中,变量被分配到堆上还是被分配到栈上是由编译器在编译时根据逃逸分析决定的,不可以更改,只能利用规则尽量让变量被分配到栈上,因为局部性优势,栈空间的内存访问速度快于堆空间访问。

4.1.4 - 方法

Golang 中的方法

一般语言中,比如c/c++/Java/python等,函数和方法是等同的。但是在Go语言中,函数和方法有明确的区分,是两个不同的概念:

  • 函数/Function:不属于任何结构体、类型,没有接收者

    •   func Add(a, b int) int {
        	return a + b
        }
      
  • 方法/Method:和特定的结构体、类型关联,带有接收者

    •   type person struct {
        	name string
        }
      
        func (p person) String() string{
        	return "the person name is "+p.name
        }
      

简单总结:

Remember: a method is just a function with a receiver argument.

请记住:方法只是一个带有接收者参数的函数。

摘要:Methods vs Functions in Golang

https://medium.com/@ishagirdhar/methods-vs-functions-in-golang-c60586bfa6b4

方法还是函数?函数还是方法?

对于从Java或者其他面向对象的语言背景过来的人看来,第一直觉是处处都用结构体(struct)和方法(method),因为对象的行为总是由方法定义的。但在Golang中,我们既有函数又有方法,这种做法正确吗?

哪些地方我们需要使用方法,哪些地方我们需要函数?

我们先来看看Golang中什么是函数,什么是方法。

**函数(function)**接受一些参数作为输入,并产生一些输出。对于相同的输入,函数总是会产生相同的输出。这意味着它不依赖于状态。类型是作为函数的参数传递的。

Go中的**方法(Method)**是带有特定 receiver(type) 参数上的函数。它定义了类型的行为,它应该使用类型的状态。

但是,如果我们在结构体里面没有状态,那么我们是不是根本就不要定义方法呢?答案是我们可以定义,但这主要是为了对该特定类型的方法进行逻辑分组。

不定义方法的规则是,如果

  • 不需要依赖状态
  • 可以在任何实现特定接口的类型上执行这个函数,这意味着不需要限制这个函数属于某个特定的类型。

接收者的按值传递

摘录自 Go语言实战笔记(八)| Go 函数方法

Go语言里有两种类型的接收者:值接收者和指针接收者。

  • 使用值接收者: 在调用的时候,方法使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的值接收者(或者说类型变量)。

  • 使用指针接收者:因为指针接收者传递的是一个指向原值指针的副本(即指针的副本),其指向的还是原来类型的值,所以修改时,同时也会影响原来值接收者(类型变量)的值。

总结:

在调用方法的时候,传递的接收者本质上都是副本,只不过可以是值的副本,也可以是指向这个值的指针的副本。指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。

Effective Go

Pointers vs. Values

https://golang.org/doc/effective_go.html#pointers_vs_values

正如我们在ByteSize中看到的那样,可以为任何命名类型(除了指针或接口)定义方法;接收者不一定是结构体。

在上面关于切片的讨论中,我们写了一个Append函数。我们可以把它定义为一个关于切片的方法。要做到这一点,我们首先声明一个可以绑定方法的命名类型,然后使该方法的接收者是该类型的值。

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // Body exactly the same as the Append function defined above.
}

这仍然需要该方法返回更新后的切片。我们可以通过重新定义该方法来消除这种笨拙,将一个指向ByteSlice的指针作为它的接收器,这样该方法就可以覆盖调用者的切片。

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Body as above, without the return.
    *p = slice
}

事实上,我们还可以做得更好。如果我们修改函数,使它看起来像一个标准的写方法,像这样。

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Again as above.
    *p = slice
    return len(data), nil
}

那么*ByteSlice类型满足标准接口io.Writer,这很方便。例如,我们可以打印成一个。

    var b ByteSlice
    fmt.Fprintf(&b, "This hour has %d days\n", 7)

关于接收者的指针与值的规则是:值方法可以在指针和值上被调用,但指针方法只能在指针上被调用。

这个规则的产生是因为指针方法可以修改接收者;在值上调用它们会导致该方法接收到一个值的副本,所以任何修改都会被丢弃。因此,语言不允许这种错误。不过有一个方便的例外。当值是可寻址的时候,语言通过自动插入地址操作符来处理在值上调用指针方法的常见情况。在我们的例子中,变量b是可寻址的,所以我们可以只用b.Write来调用它的Write方法。编译器会帮我们改写成 (&b).Write。

顺便说一下,在字节切片上使用Write的想法是实现bytes.Buffer的核心。

摘要:方法的本质

摘录自 函数——go世界中的一等公民 “方法的本质” 一节

go的方法就是语法糖:实际上Method就是将receiver作为函数的第一个参数输入的语法糖而已,本质上和函数没有区别

参考资料

4.1.5 - 可变参数

Golang 函数中的可变参数

golang语言规范

Passing arguments to ... parameters

https://golang.org/ref/spec#Passing_arguments_to_..._parameters

如果 f 是变数(variadic),其最终参数p的类型为…T,那么在 f 中,p的类型等同于类型 []T。如果调用f时,p没有实际参数,那么传递给p的值就是nil。否则,传递的值是一个类型为 []T 的新的底层数组的分片,其连续的元素是实际的参数,这些参数都必须是可以分配给T的,因此分片的长度和容量是与p绑定的参数数字,对于每个调用点来说可能会有所不同。

给定函数和调用:

func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")

Greeting在第一次调用时的值为nil,第二次调用时的值为 []string{“Joe”, “Anna”, “Eileen”} 。

如果最后的参数可以分配给一个分片类型 []T,那么如果参数后面有 …T 参数,它将作为 …T 参数的值不变地传递。在这种情况下,不会创建新的分片。

给定分片s并调用

s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...)

在Greeting,它的值将与s的底层数组相同。

摘要:Go 语言“可变参数函数”终极指南

https://studygolang.com/articles/11965

可变参数函数即其参数数量是可变的 —— 0 个或多个。声明可变参数函数的方式是在其参数类型前带上省略符(三个点)前缀。

可变参数的使用场景:

  • 避免创建仅作传入参数用的临时切片
  • 当参数数量未知
  • 传达你希望增加可读性的意图

可变参数函数会在其内部创建一个”新的切片”。事实上,可变参数是一个简化了切片类型参数传入的语法糖。

当不传入参数的时候,可变参数会成为一个空值切片( nil ):

所有的非空切片都有内建的数组,而 nil 切片则没有。

然而,当你向 nil 切片添加元素时,它会自动内建一个包含该元素的数组。这个切片也就再也不是一个 nil 切片了。

可以通过向一个已有的切片添加可变参数运算符 ”…“ 后缀的方式将其传入可变参数函数。

names := []string{"carl", "sagan"}

toFullname(names...)

这就好比通常的传参方式:

toFullname("carl", "sagan")

**不过,这里还是有一点差异:**函数会在内部直接使用这个传入的切片,并不会创建一个的新的。

可以像下面这样将数组转化成切片后传入可变参数函数:

names := [2]string{"carl", "sagan"}

toFullname(names[:]...)

传入的切片和函数内部使用的切片共享同一个底层数组,因此在函数内部改变这个数组的值同样会影响到传入的切片:

参考资料

4.1.6 - 闭包

Golang 中的闭包

闭包相关的经典总结:

  • 闭包=函数+引用环境

  • 对象是附有行为的数据,而闭包是附有数据的行为

https://gobyexample-cn.github.io/closures

Go 支持匿名函数, 并能用其构造闭包。 匿名函数在你想定义一个不需要命名的内联函数时是很实用的。

intSeq 函数返回一个在其函数体内定义的匿名函数。 返回的函数使用闭包的方式 隐藏 变量 i。 返回的函数 隐藏 变量 i 以形成闭包。

func intSeq() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

我们调用 intSeq 函数,将返回值(一个函数)赋给 nextInt。 这个函数的值包含了自己的值 i,这样在每次调用 nextInt 时,都会更新 i 的值。

func main() {

   nextInt := intSeq()

   // 通过多次调用 nextInt 来看看闭包的效果:
   fmt.Println(nextInt())  // 1
   fmt.Println(nextInt())  // 2
   fmt.Println(nextInt())  // 3

   // 为了确认这个状态对于这个特定的函数是唯一的,我们重新创建并测试一下。
   newInt2 := intSeq()
   fmt.Println(newInt2())  // 1
}

参考资料

4.1.7 - 递归函数

Golang 中的递归函数

什么是递归函数:

Technically, a recursive function is a function that makes a call to itself. To prevent infinite recursion, you need an if-else statement (of some sort) where one branch makes a recursive call, and the other branch does not. The branch without a recursive call is usually the base case (base cases do not make recursive calls to the function).

从技术上讲,递归函数是一个对自己进行调用的函数。为了防止无限递归,你需要一个 if-else 语句(或它的某种形式),其中一个分支进行递归调用,而其他分支不进行递归调用。没有递归调用的分支通常是基础案例(基础案例不对函数进行递归调用)。

参考资料

4.2 - 内建函数

Golang 语言特性中的内建函数

4.3 - 接口

Golang 接口的语言特性

4.3.1 - 接口概述

Golang 接口的语言特性概述

学习资料

4.3.2 - 接口定义

Golang 的接口定义

go语言实战

接口定义和实现

接口类型是由一组方法定义的集合。

// 定义一个interface和它的方法
type Abser interface {
	Abs() float64
}

type Vertex struct {
	X, Y float64
}

// 让结构体实现interface要求的方法
func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

接口类型的值可以存放实现这些方法的任何值。

var a Abser
v := Vertex{3, 4}
a = &v // *Vertex 实现了 Abser

任意接口类型

在go中,如果要表示类型为任意类型,包括基础类型,可以这样做:

type Value struct {
	v interface{}
}

有点类似java中的Object,但是go没有对象继承,也没有Object这种单根继承的root对象,为了表示所有类型,就需要使用interface关键字,而interface在go中是关键字,不是类型,因此要加{} 后缀。这个语法相对java有点特别。

Effective Go

接口命名

https://golang.org/doc/effective_go.html#interface-names

按照惯例,单方法接口的命名是由方法名加上 -er 后缀或类似的修饰来构造一个代理名词:Reader, Writer, Formatter, CloseNotifier等等。

这样的名字有很多,尊重它们和它们所使用的函数名是很有成效的。Read、Write、Close、Flush、String等都有规范的标志和含义。为了避免混淆,不要给你的方法起这些名字,除非它有相同的签名和含义。反过来说,如果你的类型实现了一个与一个著名类型上的方法具有相同含义的方法,就给它相同的名称和签名;调用你的字符串转换方法String而不是ToString。

Generality/通用性

https://golang.org/doc/effective_go.html#generality

如果一个类型只是为了实现一个接口而存在,并且永远不会有超出该接口的导出方法,那么就没有必要导出类型本身。只导出接口,就可以清楚地知道该值除了接口中描述的内容之外没有其他需要注意的行为。这也避免了在一个普通方法的每个实例上重复文档的需要。

在这种情况下,构造函数应该返回一个接口值而不是实现类型。举个例子,在哈希库中,crc32.NewIEEE和adler32.New都返回接口类型hash.Hash32。在Go程序中用CRC-32算法代替Adler-32算法,只需要改变构造函数调用,其余代码不受算法改变的影响。

类似的方法使得各种加密包中的流式密码算法与它们链在一起的块密码算法分离。crypto/cipher包中的Block接口指定了块密码的行为,它提供了单个数据块的加密。那么,通过与bufio包类比,实现这个接口的密码包可以用来构造流密码,用Stream接口表示,而不知道块加密的细节。

加密/密文接口是这样的。

type Block interface {
    BlockSize() int
    Encrypt(dst, src []byte)
    Decrypt(dst, src []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

这里是计数器模式(CTR)流的定义,它将一个块密码变成了一个流密码;注意,块密码的细节被抽象掉了。

// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream

NewCTR不仅适用于一种特定的加密算法和数据源,而且适用于任何Block接口和任何Stream的实现。因为它们返回的是接口值,所以用其他加密模式替换CTR加密是一个局部的变化。构造函数调用必须被编辑,但由于周围的代码必须只将结果视为Stream,所以它不会注意到这种差异。

接口与方法

https://golang.org/doc/effective_go.html#interface_methods

由于几乎任何东西都可以附加方法,所以几乎任何东西都可以满足接口。一个说明性的例子是在http包中,它定义了Handler接口。任何实现Handler的对象都可以服务于HTTP请求。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter 本身就是一个接口,它提供了对返回响应给客户端所需方法的访问。这些方法包括标准的 Write 方法,所以 http.ResponseWriter 可以在任何可以使用 io.Writer 的地方使用。Request是一个包含客户端请求的解析表示的结构。

为了简洁起见,我们忽略 POSTs,并假设 HTTP 请求总是 GETs;这种简化并不影响处理程序的设置方式。下面是一个琐碎但完整的处理程序的实现,用来统计页面被访问的次数。

// Simple counter server.
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

(为了配合我们的主题,请注意 Fprintf 如何打印到 http.ResponseWriter。) 作为参考,下面是如何将这样的服务器连接到 URL 树上的一个节点。

import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)

但为什么要把Counter做成一个结构体呢?一个整数就可以了。接收器需要是一个指针,所以增量对调用者来说是可见的)。

// Simpler counter server.
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

如果你的程序有一些内部状态,需要通知你有一个页面被访问了怎么办?给网页绑定一个频道。

// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

最后,假设我们想在/args上显示调用服务器二进制时使用的参数。很容易写一个函数来打印参数。

func ArgServer() {
    fmt.Println(os.Args)
}

我们如何把它变成一个HTTP服务器?我们可以让ArgServer成为某个类型的方法,我们忽略它的值,但是有一个更干净的方法。因为除了指针和接口,我们可以为任何类型定义一个方法,我们可以为一个函数写一个方法。http包中包含了这样的代码。

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers.  If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

HandlerFunc是一个带方法的类型,ServeHTTP,所以该类型的值可以服务于HTTP请求。看看方法的实现:接收者是一个函数f,方法调用f,这看起来很奇怪,但这和比如说,接收者是一个通道,方法在通道上发送并没有什么不同。

为了使ArgServer成为一个HTTP服务器,我们首先要修改它的签名,使其具有正确的签名。

// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

现在ArgServer与HandlerFunc具有相同的签名,因此可以将其转换为该类型来访问其方法,就像我们将Sequence转换为IntSlice来访问IntSlice.Sort一样。设置它的代码很简洁。

http.Handle("/args", http.HandlerFunc(ArgServer))

当有人访问/args页面时,安装在该页面的处理程序的值为ArgServer,类型为HandlerFunc。HTTP服务器将调用该类型的ServeHTTP方法,ArgServer作为接收方,而接收方又会调用ArgServer(通过HandlerFunc.ServeHTTP内部的调用f(w, req))。然后,参数将被显示出来。

在这一节中,我们从一个结构、一个整数、一个通道和一个函数制作了一个HTTP服务器,这都是因为接口只是方法的集合,它可以为(几乎)任何类型定义。

4.3.3 - 接口实现

Golang 中的接口实现

go语言实战

隐式接口

类型通过实现那些方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implements“。

隐式接口解藕了实现接口的包和定义接口的包:互不依赖。

因此,也就无需在每一个实现上增加新的接口名称。

常见的接口:

  1. fmt 包中定义的 Stringer
type Stringer interface {
    String() string
}

Stringer 是一个可以用字符串描述自己的类型。fmt包 (还有许多其他包)使用这个来进行输出。

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

Effective Go

https://golang.org/doc/effective_go.html#interfaces

Go中的接口提供了一种指定对象行为的方法:如果某个东西可以做这个,那么它就可以在这里使用。我们已经看到了几个简单的例子;自定义的打印机可以通过一个String方法实现,而Fprintf可以通过一个Write方法向任何东西生成输出。只有一个或两个方法的接口在Go代码中很常见,通常会被赋予一个由方法派生的名称,比如io.Writer用于实现Write方法的东西。

一个类型可以实现多个接口。例如,一个集合如果实现了 sort.Interface,就可以通过包 sort 中的例程进行排序,其中包含 Len()、Less(i, j int) bool 和 Swap(i, j int),它还可以有一个自定义的格式器。在这个人为的例子中,Sequence同时满足这两个条件。

type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    sort.Sort(s)
    str := "["
    for i, elem := range s { // Loop is O(N²); will fix that in next example.
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

4.3.4 - 接口检查

Golang 中的接口检查

Effective Go

接口检查

https://golang.org/doc/effective_go.html#blank_implements

正如我们在上面关于接口的讨论中所看到的,类型不需要明确声明它实现了接口。相反,类型只要实现了接口的方法就实现了接口。在实践中,大多数接口转换都是静态的,因此在编译时进行检查。例如,将一个 *os.File 传给一个期望有 io.Reader 的函数,除非 *os.File 实现了 io.Reader 接口,否则不会被编译。

但有些接口检查确实是在运行时进行的。其中一个例子是在 encoding/json 包中,它定义了一个 Marshaler 接口。当JSON编码器接收到一个实现该接口的值时,编码器会调用该值的 marshaling 方法将其转换为JSON,而不是进行标准转换。编码器在运行时用一个类型断言(type assertion)检查这个属性,比如。

m, ok := val.(json.Marshaler)

如果只需要询问一个类型是否实现了一个接口,而不实际使用接口本身,或许作为错误检查的一部分,使用空白标识符来忽略类型假定的值。

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

出现这种情况的一个地方是,当需要在实现类型的包内保证它确实满足接口的时候。如果一个类型–例如,json.RawMessag–需要自定义的JSON表示,它应该实现 json.Marshaler,但没有静态转换会导致编译器自动验证这一点。如果该类型无意中没有满足接口,JSON编码器仍然会工作,但不会使用自定义的实现。为了保证实现的正确性,可以在包中使用空白标识符的全局声明。

var _ json.Marshaler = (*RawMessage)(nil)

在这个声明中,涉及到将RawMessage转换为Marshaler的赋值,需要RawMessage实现Marshaler,并且在编译时将检查该属性。如果json.Marshaler接口发生变化,这个包将不再编译,我们会被通知需要更新。

在这个构造中出现空白标识符,说明声明的存在只是为了类型检查,而不是为了创建一个变量。不过不要对每个满足接口的类型都这样做。按照惯例,这种声明只有在代码中已经没有静态转换时才会使用,这是很罕见的事件。

4.3.5 - type switch语句

Golang 中的type switch语句

type switch语句的语法详情见:switch 语句

Effective Go

转换

https://golang.org/doc/effective_go.html#conversions

Sequence的String方法重现了Sprint已经为slices做的工作。(它的复杂度也是O(N²),这很差。) 如果我们在调用Sprint之前将Sequence转换为一个普通的[]int,我们就可以分担这项工作(也可以加快它的速度)。

func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

这个方法是另一个从String方法安全调用Sprintf的转换技术的例子。因为两个类型(Sequence和[]int)是一样的,如果我们忽略类型名,那么在它们之间进行转换是合法的。这个转换并没有创建一个新的值,它只是暂时把现有的值当作一个新的类型。(还有其他合法的转换,比如从整数到浮点,确实会创建一个新的值。)

这是Go程序中的一个习惯做法,用来转换表达式的类型以访问不同的方法集。举个例子,我们可以使用现有的 sort.IntSlice 类型,将整个例子简化为这样。

type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

现在,我们不再让Sequence实现多个接口(排序和打印),而是利用一个数据项转换为多种类型(Sequence、sort.IntSlice和[]int)的能力,每种类型都能完成一部分工作。这在实践中比较少见,但可以很有效。

接口转换和类型断言

https://golang.org/doc/effective_go.html#interface_conversions

type switch 是一种转换形式:它们接受一个接口,并在某种意义上,对于switch中的每一个case,将其转换为该case的类型。下面是fmt.Printf下的代码如何使用类型转换将一个值变成一个字符串的简化版本。如果它已经是一个字符串,我们要的是接口所持有的实际字符串值,而如果它有一个String方法,我们要的是调用该方法的结果。

type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

第一个case是找到一个具体的值;第二个case是将接口转换成另一个接口。这样混合类型是完全可以的。

如果我们只关心一种类型呢?如果我们知道这个值持有一个字符串,而我们只想提取它?一个one-case类型switch就可以了,但类型断言(type assertion)也可以。类型断言接受一个接口值,并从中提取一个指定显式类型的值。语法借鉴了打开类型转换的子句,但用的是显式类型而不是类型关键字。

value.(typeName)

结果是一个静态类型typeName的新值。该类型必须是接口所持有的具体类型,或者是该值可以转换为的第二个接口类型。为了提取我们知道的值中的字符串,我们可以写。

str := value.(string)

但如果发现值不包含字符串,程序就会因运行时错误而崩溃。为了防止这种情况,可以使用 “comma, ok” 这个习惯用法来安全地测试值是否是字符串。

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

如果类型断言失败,str仍将存在,并且类型为string,但它的值为零,是一个空字符串。

为了说明这种能力,这里有一个if-else语句,相当于本节开头的类型切换。

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

4.3.6 - 接口嵌入

Golang 中的接口嵌入

Effective Go

https://golang.org/doc/effective_go.html#embedding

Go并没有提供典型的、类型驱动的子类概念,但它确实可以通过在结构体或接口中嵌入类型来 “借用"实现的一部分。

接口嵌入非常简单。我们之前已经提到了 io.Reader 和 io.Writer 接口,下面是它们的定义。

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

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

io包还导出了其他一些接口,这些接口指定了可以实现若干这样方法的对象。例如,有io.ReadWriter,一个包含Read和Write的接口。我们可以通过显式列出这两个方法来指定io.ReadWriter,但像这样把这两个接口嵌入形成新的接口,会更容易,也更有感召力。

// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

如代码所示: ReadWriter可以做Reader做的事情,也可以做Writer做的事情;它是一个嵌入接口(必须是不相干的方法集)的联合体。只有接口可以嵌入到接口中。

同样的基本思想也适用于结构体,但其影响更为深远。bufio包有两个结构类型,bufio.Reader和bufio.Writer,当然每个结构都实现了包io中的类似接口。而且bufio还实现了一个缓冲的读/写器,它是通过使用嵌入的方式将一个读器和一个写器合并到一个结构中来实现的:它列出了结构中的类型,但没有给它们起字段名:

// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

嵌入的元素是指向结构体的指针,当然在使用之前必须初始化为指向有效的结构。ReadWriter结构可以写成:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

但这样一来,为了提升字段的方法以满足io接口,我们还需要提供转发方法,比如这样:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

通过直接嵌入结构,我们避免了这种方式。嵌入类型的方法直接附加,这意味着bufio.ReadWriter不仅拥有bufio.Reader和bufio.Writer的方法,还满足了所有三个接口:io.Reader、io.Writer和io.ReadWriter。

嵌入和子类有一个重要的区别。当我们嵌入一个类型时,该类型的方法会成为外部类型的方法,但当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的例子中,当调用一个bufio.ReadWriter的Read方法时,它的效果和上面写出来的转发方法完全一样,接收者是ReadWriter的reader字段,而不是ReadWriter本身。

嵌入也可以是一种简单的方便。这个例子显示了一个嵌入字段与一个常规的、命名的字段并列。

type Job struct {
    Command string
    *log.Logger
}

现在,Job类型有了*log.Logger的Print、Printf、Println等方法。当然,我们可以给Logger取一个字段名,但没有必要这么做。而现在,一旦初始化,我们就可以将日志记录到Job中。

job.Println("starting now...")

Logger是Job结构的一个常规字段,所以我们可以在Job的构造函数中以通常的方式初始化它,像这样:

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

或用组合字面量:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

如果我们需要直接引用一个嵌入的字段,那么字段的类型名,忽略包的限定符,作为字段名,就像在我们的ReadWriter结构的Read方法中一样。在这里,如果我们需要访问一个Job变量job的*log.Logger,我们会写job.Logger,如果我们想完善Logger的方法,这将是非常有用的。

func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

嵌入类型引入了名称冲突的问题,但解决这些问题的规则很简单。首先,一个字段或方法X将任何其他项目X隐藏在类型的更深嵌套部分。如果log.Logger包含一个名为Command的字段或方法,那么Job的Command字段就会支配它。

其次,如果相同的名称出现在相同的嵌套层次,通常是一个错误;如果Job结构包含另一个名为Logger的字段或方法,那么嵌入log.Logger将是错误的。但是,如果重复的名称在程序中从未在类型定义之外提及,则是可以的。这个限定提供了一些保护,防止从外部对嵌入的类型进行修改;如果添加的字段与另一个子类型中的另一个字段发生冲突,如果两个字段都没有使用过,那么就没有问题。

4.4 - 错误处理

Golang 的错误处理

4.4.1 - 错误处理概述

Golang 的错误处理概述

go by example

https://gobyexample-cn.github.io/errors

符合 Go 语言习惯的做法是使用一个独立、明确的返回值来传递错误信息。

这与 Java、Ruby 使用的异常(exception) 以及在 C 语言中有时用到的重载 (overloaded) 的单返回/错误值有着明显的不同。

Go 语言的处理方式能清楚的知道哪个函数返回了错误,并使用跟其他(无异常处理的)语言类似的方式来处理错误。

// 按照惯例,错误通常是最后一个返回值并且是 error 类型,它是一个内建的接口。
func f1(arg int) (int, error) { // 
    if arg == 42 {
        // errors.New 使用给定的错误信息构造一个基本的 error 值。
        return -1, errors.New("can't work with 42")
    }
    // 返回错误值为 nil 代表没有错误。
    return arg + 3, nil
}

你还可以通过实现 Error() 方法来自定义 error 类型。 这里使用自定义错误类型来表示上面例子中的参数错误:

// 使用自定义错误类型来表示上面例子中的参数错误
type argError struct {
    arg  int
    prob string
}
// 通过实现 Error() 方法来自定义 error 类型
func (e *argError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
    if arg == 42 {
         // 使用 &argError 语法来建立一个新的结构体
         // 并提供了 arg 和 prob 两个字段的值
         return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}

Effective Go

https://golang.org/doc/effective_go.html#errors

类库例程必须经常向调用者返回某种错误指示。如前所述,Go的多值返回使得在返回正常返回值的同时很容易返回一个详细的错误说明。利用这个特性来提供详细的错误信息是一种很好的风格。例如,正如我们将看到的,os.Open 并不只是在失败时返回一个 nil 指针,它还返回一个错误值来描述出了什么问题。

按照惯例,错误的类型为error,是一个简单的内置接口。

type error interface {
    Error() string
}

类库编写者可以自由地用更丰富的模型来实现这个接口,使其不仅可以看到错误,还可以提供一些上下文。如前所述,除了通常的*os.File返回值之外,os.Open还返回一个错误值。如果文件被成功打开,错误值将为nil,但当出现问题时,它将持有一个os.PathError。

// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // The associated file.
    Err error    // Returned by the system call.
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

PathError的Error会生成这样一个字符串:

open /etc/passwx: no such file or directory

这样的错误包括有问题的文件名、操作和它所触发的操作系统错误,即使打印出来的时候离引起它的调用很远也是有用的;它比单纯的 “没有这样的文件或目录 “信息量大得多。

在可行的情况下,错误字符串应该识别它们的来源,例如通过有一个前缀来命名产生错误的操作或包。例如,在包image中,由于未知格式导致的解码错误的字符串表示是 “image: unknown format”。

关心精确错误细节的调用者可以使用类型开关(type switch)或类型断言(type assertion)来查找特定错误并提取细节。对于PathErrors来说,这可能包括检查内部Err字段是否存在可恢复的故障。

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // Recover some space.
        continue
    }
    return
}

这里的第二个 if 语句是另一种类型的断言。如果它失败了,ok将为false,e将为nil. 如果它成功了,ok将为true,这意味着错误类型为*os.PathError,那么e也是如此,我们可以检查更多信息。

4.4.2 - [博客] 错误处理与go

golang 官方 blog 文章 Error handling and Go

备注:golang官方blog文章 Error handling and Go

介绍

只要写过任何Go代码,就可能遇到过内置的 error 类型。Go代码使用错误值来表示异常状态。例如,os.Open函数在打开文件失败时,会返回一个非零的错误值:

func Open(name string) (file *File, err error)

下面的代码使用 os.Open 来打开一个文件,如果发生错误,则调用 log.Fatal 来打印错误信息并停止。

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

只知道错误类型这一点,你就可以在Go中完成很多工作,但在这篇文章中,我们将仔细研究错误,并讨论一些在Go中处理错误的好做法。

error 类型

error 类型是一种接口类型。error 变量代表任何可以描述为字符串的值。下面是接口的声明。

type error interface {
    Error() string
}

与所有内置类型一样,error 类型在宇宙块中预先声明。

最常用的 error 实现是 error package 的未导出的 errorString 类型。

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

可以使用 errors.New 函数来构造这些值。它接收一个字符串,并将其转换为 errors.errorString,然后作为一个错误值返回。

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

下面是如何使用 errors.New:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

传递负参数给Sqrt的调用者会收到一个非零的错误值(其具体表示是一个error.errorString值)。调用者可以通过调用错误的Error方法来访问错误字符串(“math: square root of…"),或者直接打印它。

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

fmt包通过调用 Error() 字符串方法来格式化错误值。

错误实现的责任是总结上下文。os.Open 返回的错误格式为 “open /etc/passwd: permission denied”,而不仅仅是 “permission denied."。我们的Sqrt返回的错误缺少了无效参数的信息。

要添加这些信息,一个有用的函数是fmt包的Errf。它根据Printf的规则格式化一个字符串,并将其作为一个由error.New创建的错误返回。

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

在很多情况下,fmt.Errorf已经足够好了,但是由于error是一个接口,你可以使用任意的数据结构作为错误值,以允许调用者检查错误的细节。

例如,我们假设的调用者可能想恢复传递给Sqrt的无效参数。我们可以通过定义一个新的错误实现而不是使用 errors.errorString 来实现。

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

然后,复杂的调用者可以使用类型断言来检查NegativeSqrtError,并对其进行特殊处理,而只是将错误传递给fmt.Println或log.Fatal的调用者则不会看到行为的改变。

另一个例子是,json包指定了一个SyntaxError类型,当json.Decode函数在解析JSON blob时遇到语法错误时,它会返回这个类型。

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

Offset字段甚至没有显示在错误的默认格式中,但调用者可以使用它来为他们的错误信息添加文件和行信息。

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

这是Camlistore项目中一些实际代码的略微简化版本)。

错误接口只需要一个Error方法;特定的错误实现可能有额外的方法。例如,net包按照通常的惯例返回类型为error的错误,但一些错误实现有net.Error接口定义的附加方法。

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

客户端代码可以通过类型断言来测试net.Error,然后区分暂时性的网络错误和永久性的错误。例如,网络爬虫可能会在遇到暂时性错误时休眠并重试,否则就会放弃。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

简化重复性错误处理

在Go中,错误处理很重要。该语言的设计和约定鼓励您在错误发生时明确地检查错误(与其他语言中的抛出异常和有时捕获错误的约定不同)。在某些情况下,这使得Go代码变得啰嗦,但幸运的是,您可以使用一些技术来减少重复的错误处理。

考虑一个带有 HTTP 处理程序的 App Engine 应用程序,该处理程序从数据存储中检索一条记录,并使用模板对其进行格式化。

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

这个函数处理由datastore.Get函数和viewTemplate的Execute方法返回的错误。在这两种情况下,它都会向用户呈现一个简单的错误信息,并给出HTTP状态码500(“内部服务器错误”)。这看起来是一个可管理的代码量,但增加一些HTTP处理程序,你很快就会得到许多相同错误处理代码的副本。

为了减少重复,我们可以定义自己的HTTP appHandler类型,其中包括一个错误返回值:

type appHandler func(http.ResponseWriter, *http.Request) error

然后,我们可以改变我们的viewRecord函数来返回错误。

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

这比原来的版本更简单,但http包并不理解返回错误的函数。为了解决这个问题,我们可以在appHandler上实现http.Handler接口的ServeHTTP方法。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

ServeHTTP方法调用appHandler函数,并向用户显示返回的错误(如果有的话)。请注意,该方法的接收器 fn 是一个函数,(Go 可以做到这一点!)该方法通过调用表达式 fn(w) 中的接收者来调用函数。(Go 可以做到这一点!) 方法通过调用表达式 fn(w, r) 中的接收者来调用函数。

现在,当我们在http包中注册viewRecord时,我们使用Handle函数(而不是HandleFunc),因为appHandler是一个http.Handler(而不是http.HandlerFunc)。

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

有了这个基本的错误处理基础架构,我们可以让它变得更加友好。与其仅仅显示错误字符串,不如给用户一个简单的错误信息,并附上适当的HTTP状态代码,同时将完整的错误记录到App Engine开发者控制台,以便进行调试。

为此,我们创建一个appError结构,包含一个错误和一些其他字段。

type appError struct {
    Error   error
    Message string
    Code    int
}

接下来我们修改appHandler类型来返回*appError值。

type appHandler func(http.ResponseWriter, *http.Request) *appError

(通常情况下,传回 error 的具体类型而不是 error 是错误的,原因在Go FAQ中讨论过,但在这里是正确的,因为ServeHTTP是唯一能看到该值并使用其内容的地方。)

并让appHandler的ServeHTTP方法将appError的Message以正确的HTTP状态码显示给用户,并将完整的Error记录到开发者控制台。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

最后,我们将viewRecord更新为新的函数签名,并让它在遇到错误时返回更多的上下文。

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

这个版本的viewRecord和原来的长度是一样的,但现在每一行都有特定的意义,我们提供的是更友好的用户体验。

这并没有结束,我们还可以进一步改进我们应用程序中的错误处理。一些想法。

  • 给错误处理程序一个漂亮的HTML模板。

  • 当用户是管理员时,通过将堆栈跟踪写入HTTP响应,使调试变得更容易。

  • 为appError写一个构造函数,存储堆栈跟踪以方便调试。

  • 从appHandler内部的恐慌中恢复,将错误记录到控制台中,称为 “Critical”,同时告诉用户 “发生了一个严重的错误”。这是一个很好的触动,避免了让用户暴露在编程错误引起的不可捉摸的错误信息中。更多细节请参见Defer、Panic和Recover文章。

结束语

正确的错误处理是优秀软件的基本要求。通过运用本篇文章中描述的技术,你应该能够写出更可靠、更简洁的Go代码。

4.4.3 - [博客] 错误是值

golang 官方 blog 文章 Errors are values

备注:golang官方blog文章 Errors are values

在Go程序员中,尤其是那些刚接触这门语言的程序员,经常讨论的一个问题就是如何处理错误。谈话往往变成了对这个序列的次数的哀叹:

if err != nil {
    return err
}

显示出来。我们最近扫描了所有能找到的开源项目,发现这个片段每一两页只出现一次,比一些人认为的要少。不过,如果人们仍然认为必须键入:

if err != nil

一直以来,一定有什么地方出了问题,而明显的目标是 go 本身。

这是不幸的,误导性的,而且很容易纠正。也许发生的情况是,刚接触 go 的程序员会问:“如何处理错误?"。学会了这种模式,就止步于此。在其他语言中,人们可能会使用 try-catch 块或其他类似机制来处理错误。因此,程序员就会想,我在以前的语言中会使用 try-catch,在Go中我就直接输入 if err != nil。随着时间的推移,Go代码收集了很多这样的片段,结果感觉很笨拙。

不管这种解释是否合适,很明显,这些Go程序员忽略了一个关于错误的基本点:Errors are values (错误是值)。

值可以被编程,既然错误是值,那么错误也可以被编程。

当然,涉及错误值的常见语句是测试它是否为nil,但还有无数其他的事情可以用错误值来做,应用这些其他的一些事情可以让你的程序变得更好,消除了很多如果每个错误都用死板的 if 语句来检查所产生的模板。

下面是一个简单的例子,来自 bufio 包的 Scanner 类型。它的 Scan 方法执行了底层的I/O,这当然会导致错误。然而Scan方法根本没有暴露错误。相反,它返回一个布尔值,并在扫描结束时运行一个单独的方法,报告是否发生错误。客户端代码是这样的。

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
if err := scanner.Err(); err != nil {
    // process the error
}

当然,有一个错误的nil检查,但它只出现和执行一次。扫描方法可以被定义为:

func (s *Scanner) Scan() (token []byte, error)

然后示例用户代码可能是(取决于如何检索令牌):

scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    if err != nil {
        return err // or maybe break
    }
    // process token
}

这并没有什么不同,但有一个重要的区别。在这段代码中,客户端必须在每次迭代时检查错误,但在真正的 Scanner API 中,错误处理是从关键的 API 元素中抽象出来的,而关键的 API 元素就是迭代 tokens。因此,使用真正的API,客户端的代码感觉更自然:循环直到完成,然后再担心错误。错误处理不会掩盖控制流。

当然,在掩盖之下发生的事情是,一旦Scan遇到一个I/O错误,它就会记录下来并返回false。当客户端询问时,一个单独的方法Err会报告错误值。虽然这很微不足道,但它和把

if err != nil

放的遍地都是,或者要求客户端在每个token之后检查错误。这就是带有错误值的编程。简单的编程,是的,但还是编程。

值得强调的是,无论设计如何,程序检查错误是至关重要的,无论它们是如何暴露的。这里讨论的不是如何避免检查错误,而是如何使用语言优雅地处理错误。

当我参加2014年秋季在东京举行的GoCon时,就出现了重复查错代码的话题。一位在Twitter上化名为@jxck_的热心地鼠对错误检查发出了熟悉的感叹。他有一些代码的示意是这样的。

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

这是非常重复的。在真实的代码中,这段代码比较长,发生的事情比较多,所以不容易只用帮助函数重构,但在这种理想化的形式下,在错误变量上关联一个函数字面量会有帮助:

var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

这种模式很好用,但需要在每个做write的函数中都有一个闭包;单独的帮助函数使用起来比较笨拙,因为err变量需要在不同的调用中维护(试试)。

我们可以借鉴上面Scan方法的思想,使之更干净、更通用、更可重用。我在我们的讨论中提到了这个技术,但@jxck_并没有看到如何应用它。经过长时间的交流,由于语言障碍,我问是否可以借用他的笔记本,通过输入一些代码给他看。

我定义了一个叫 errWriter 的对象,类似这样。

type errWriter struct {
    w   io.Writer
    err error
}

并给了它一个方法,write。它不需要有标准的 Write 签名,而且它的小写部分是为了突出区别。write方法会调用底层Writer的Write方法,并记录第一个错误,供以后参考。

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

一旦发生错误,写方法就会变成无操作,但错误值会被保存。

给定errWriter类型和它的写法,上面的代码可以重构。

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

这样做更干净,甚至比使用闭包更干净,也使实际的写入顺序更容易在页面上看到。再也没有杂乱无章的东西了。用错误值(和接口)编程让代码变得更漂亮了。

很有可能在同一个包中的其他代码可以建立在这个想法上,甚至直接使用errWriter。

另外,一旦errWriter存在,它还可以做更多的事情来帮助我们,尤其是在不太人为的例子中。它可以累积字节数。它可以将写入的内容凝聚成一个单一的缓冲区,然后可以原子化地传输。还有更多。

事实上,这种模式经常出现在标准库中。archive/zip和net/http包都使用了它。更突出的是,bufio包的Writer实际上是errWriter思想的一个实现。虽然bufio.Writer.Write会返回一个错误,但那主要是为了尊重io.Writer接口。bufio.Writer的Write方法的行为就像我们上面的errWriter.write方法一样,Flush会报告错误,所以我们的例子可以这样写。

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
    return b.Flush()
}

这种方法有一个重大的缺点,至少对某些应用来说是这样:无法知道错误发生前完成了多少处理。如果该信息很重要,就需要采用更细化的方法。不过,通常情况下,在最后进行全有或全无的检查就足够了。

我们只看了一种避免重复性错误处理代码的技术。请记住,使用 errWrite r或 bufio.Writer 并不是简化错误处理的唯一方法,而且这种方法并不适合所有情况。然而,关键的经验是,错误是值,可以利用Go编程语言的全部能力来处理它们。

使用该语言来简化你的错误处理。

但请记住。无论你做什么,总是要检查你的错误!

4.4.4 - [博客] go1.13中的错误处理

golang 官方 blog 文章 Working with Errors in Go 1.13

备注:golang官方blog文章 Working with Errors in Go 1.13

介绍

在过去的十年里,Go 将错误处理为值(errors as values)的做法对我们很有帮助。虽然标准库对错误的支持很少–只有 errors.New 和 fmt.Errorf 函数,它们产生的错误只包含一条消息–但内置的 error 接口允许 Go 程序员添加他们想要的任何信息。它所需要的只是一个实现 Error 方法的类型。

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }

像这样的错误类型无处不在,它们所存储的信息也千差万别,从时间戳到文件名到服务器地址。通常,这些信息包括另一个低级错误,以提供额外的上下文。

一个错误包含另一个错误的模式在 Go 代码中是如此普遍,以至于经过广泛的讨论,Go 1.13 增加了对它的明确支持。这篇文章描述了标准库中提供这种支持的新增内容:错误包中的三个新函数,以及 fmt.Errorf 的新格式动词。

在详细描述这些变化之前,让我们先回顾一下在以前的语言版本中是如何检查和构造错误的。

go 1.13之前的错误

检查错误

Go error 是值。程序根据这些值以几种方式做出决定。最常见的是将错误与nil进行比较,以确定操作是否失败。

if err != nil {
    // something went wrong
}

有时我们会将错误与已知的 sentinel 值进行比较,看看是否发生了特定的错误。

var ErrNotFound = errors.New("not found")

if err == ErrNotFound {
    // something wasn't found
}

错误值可以是满足语言定义的 error 接口的任何类型。程序可以使用类型断言或类型转换来将错误值视为更具体的类型。

type NotFoundError struct {
    Name string
}

func (e *NotFoundError) Error() string { return e.Name + ": not found" }

if e, ok := err.(*NotFoundError); ok {
    // e.Name wasn't found
}

添加信息

经常有函数在调用堆栈中传递错误,同时在其中添加信息,比如错误发生时的简要描述。一个简单的方法是构造新的错误,其中包括前一个错误的文本。

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

使用 fmt.Errorf 创建一个新的错误,会丢弃原始错误中除文本以外的所有内容。正如我们在上面的QueryError中所看到的,我们有时可能希望定义一个新的错误类型,其中包含底层错误,保留它以便于代码检查。这里又是QueryError。

type QueryError struct {
    Query string
    Err   error
}

程序可以在 *QueryError 值内部查看,根据底层 error 做出决定。你有时会看到这被称为 “解包 “错误。

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // query failed because of a permission problem
}

标准库中的os.PathError类型是另一个例子,它包含了一个错误。

Errors in Go 1.13

Unwrap 方法

Go 1.13 为 errors 和 fmt 标准库包引入了新特性,以简化对包含其他错误的错误的处理。其中最重要的是约定,而不是变化:包含另一个error的 error 可以实现一个返回底层 error 的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我们就说 e1 封装(wrap)了e2,可以解开 (unwrap) e1 得到 e2。

按照这个约定,我们可以给上面的 QueryError 类型一个 Unwrap 方法,返回其包含的错误。

func (e *QueryError) Unwrap() error { return e.Err }

使用 Is 和 As 检查错误

Go 1.13 errors 包包含了两个新的错误检查函数:Is 和 As。

errors.Is 函数将错误与值进行比较:

// Similar to:
//   if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
    // something wasn't found
}

As 函数测试 error 是否为特定类型:

// Similar to:
//   if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
    // err is a *QueryError, and e is set to the error's value
}

在最简单的情况下,errors.Is 函数的行为就像与 sentinel error 的比较,而 errors.As 函数的行为就像类型断言。然而,当对封装的错误进行操作时,这些函数会考虑链中的所有error。让我们再看看上面的例子,即解开一个 QueryError 来检查底层错误。

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // query failed because of a permission problem
}

使用 errors.Is 函数,我们可以把它写成:

if errors.Is(err, ErrPermission) {
    // err, or some error that it wraps, is a permission problem
}

errors 包还包括一个新的 Unwrap 函数,它返回调用 error 的 Unwrap 方法的结果,如果错误没有Unwrap方法,则返回 nil。但通常最好使用 errors.Is 或 errors.As,因为这些函数会在一次调用中检查整个链。

使用 %w 封装错误

如前所述,通常使用 fmt.Errorf 函数为错误添加附加信息。

if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

在Go 1.13中,fmt.Errorf 函数支持一个新的 %w 动词。当这个动词存在时,fmt.Errorf 返回的错误将有一个 Unwrap 方法返回 %w 的参数,它必须是一个error。在所有其他方面,%w 与 %v 相同:

if err != nil {
    // Return an error which unwraps to err.
    return fmt.Errorf("decompress %v: %w", name, err)
}

用 %w 包装 error ,使得它可以被 errors.Is 和 erros.As 使用。

err := fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...

是否封装

当向 error 添加额外的上下文时,无论是使用 fmt.Errorf 还是通过实现自定义类型,您都需要决定新的 error 是否应该封装原始 error。这个问题没有唯一的答案,它取决于创建新 error 的上下文。封装一个error 以将其暴露给调用者。如果这样做会暴露实现细节,则不要包装error。

举个例子,想象一个从 io.Reader.Reader 中读取复杂数据结构的 Parsse 函数。如果发生错误,我们希望报告发生错误的行号和列号。如果错误是在从 io.Reader 读取时发生的,我们希望对该错误进行包装,以便检查潜在的问题。由于调用者向函数提供了 io.Reader,所以暴露它所产生的错误是有意义的。

相反,一个对数据库进行多次调用的函数可能不应该返回一个对其中一次调用结果进行解包的错误。如果函数使用的数据库是一个实现细节,那么暴露这些错误就违反了抽象性。例如,如果你的包pkg的LookupUser函数使用了Go的数据库/sql包,那么它可能会遇到一个sql.ErrNoRows错误。如果你用fmt.Errorf(“accessing DB: %v”, err)来返回这个错误,那么调用者就不能在里面查找sql.ErrNoRows。但是如果函数返回的是fmt.Errorf(“accessing DB: %w”, err),那么调用者就可以合理地写道

err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) 

这时,如果你不想破坏你的客户端,即使你切换到不同的数据库包,函数必须总是返回 sql.ErrNoRows 。换句话说,包装一个错误使该错误成为你的API的一部分。如果你不想承诺在未来支持该错误作为你的API的一部分,你就不应该包装该错误。

重要的是要记住,无论你是否封装,错误文本都是一样的。试图理解该错误的人无论用哪种方式都会得到相同的信息;选择封装是为了给程序提供额外的信息,以便他们能够做出更明智的决定,还是为了保留抽象层而不提供该信息。

使用Is和As方法自定义错误测试

errors.Is 函数检查链中每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标值匹配。此外,链中的错误可以通过实现 Is 方法声明它与目标值匹配。

作为一个例子,考虑这个错误的灵感来自 Upspin 错误包,它将错误与模板进行比较,只考虑模板中非零的字段。

type Error struct {
    Path string
    User string
}

func (e *Error) Is(target error) bool {
    t, ok := target.(*Error)
    if !ok {
        return false
    }
    return (e.Path == t.Path || t.Path == "") &&
           (e.User == t.User || t.User == "")
}

if errors.Is(err, &Error{User: "someuser"}) {
    // err's User field is "someuser".
}

errors.As 函数同样在存在的情况下咨询 As 方法。

Errors 和包API

返回错误的包(大多数都是这样)应该描述这些错误的属性,程序员可以依赖这些属性。一个设计良好的包也会避免返回具有不应该依赖的属性的错误。

最简单的规范是说,操作要么成功,要么失败,分别返回一个 nil 或 non-nil 的错误值。在很多情况下,不需要进一步的信息。

如果我们希望函数返回一个可识别的错误条件,比如 “item not found”,我们可能会返回一个包裹着 sentinel 的 error。

var ErrNotFound = errors.New("not found")

// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
    if itemNotFound(name) {
        return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
    }
    // ...
}

还有其他现有的模式可以提供可以被调用者进行语义检查的错误,例如直接返回一个哨兵值、一个特定的类型或一个可以用谓词函数检查的值。

在所有情况下,都应该注意不要向用户暴露内部细节。正如我们在上面的 “是否封装” 中提到的,当你从另一个包中返回一个错误时,你应该将错误转换为不暴露底层错误的形式,除非你愿意承诺在将来返回那个特定的错误。

f, err := os.Open(filename)
if err != nil {
    // The *os.PathError returned by os.Open is an internal detail.
    // To avoid exposing it to the caller, repackage it as a new
    // error with the same text. We use the %v formatting verb, since
    // %w would permit the caller to unwrap the original *os.PathError.
    return fmt.Errorf("%v", err)
}

如果函数被定义为返回一个包裹着某个哨兵或类型的 error ,不要直接返回底层错误。

var ErrPermission = errors.New("permission denied")

// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() error {
    if !userHasPermission() {
        // If we return ErrPermission directly, callers might come
        // to depend on the exact error value, writing code like this:
        //
        //     if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
        //
        // This will cause problems if we want to add additional
        // context to the error in the future. To avoid this, we
        // return an error wrapping the sentinel so that users must
        // always unwrap it:
        //
        //     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
        return fmt.Errorf("%w", ErrPermission)
    }
    // ...
}

总结

虽然我们所讨论的变化仅仅是三个函数和一个格式化动词,但我们希望它们将大大改善Go程序中的错误处理方式。我们希望通过包装来提供额外的上下文会变得很普遍,帮助程序做出更好的决策,帮助程序员更快地找到错误。

正如Russ Cox在GopherCon 2019的主题演讲中所说,在通往Go 2的道路上,我们进行实验、简化和出货。现在我们已经出货了这些变化,我们期待着接下来的实验。

4.5 - panic

Golang 中的panic

4.5.1 - panic概述

Golang 的 panic 概述

go by example

https://gobyexample-cn.github.io/panic

panic 意味着有些出乎意料的错误发生。 通常我们用它来表示程序正常运行中不应该出现的错误, 或者我们不准备优雅处理的错误。

我们将使用 panic 来检查这个站点上预期之外的错误。 而该站点上只有一个程序:触发 panic。

panic 的一种常见用法是:当函数返回我们不知道如何处理(或不想处理)的错误值时,中止操作。 如果创建新文件时遇到意外错误该如何处理?这里有一个很好的 panic 示例。

package main

import "os"

func main() {

    panic("a problem")

    _, err := os.Create("/tmp/file")
    if err != nil {
        panic(err)
    }
}

运行程序将会导致 panic: 输出一个错误消息和协程追踪信息,并以非零的状态退出程序:

$ go run panic.go
panic: a problem
goroutine 1 [running]:
main.main()
    /.../panic.go:12 +0x47
...
exit status 2

注意,与某些使用 exception 处理错误的语言不同, 在 Go 中,通常会尽可能的使用返回值来标示错误。

Effective Go

panic

https://golang.org/doc/effective_go.html#panic

向调用者报告错误的通常方法是返回 error 作为一个额外的返回值。规范的 Read 方法是一个著名的实例;它返回一个字节数和一个错误。但如果错误无法恢复怎么办?有时,程序根本无法继续。

为此,有一个内置的函数panic,它实际上会产生一个运行时错误,使程序停止(但请看下一节)。这个函数接收一个任意类型的参数–通常是一个字符串–在程序死亡时打印出来。它也是一种指示不可能发生的事情的方法,比如退出一个无限循环。

// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
    z := x/3   // Arbitrary initial value
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // A million iterations has not converged; something is wrong.
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

这只是一个例子,但真正的库函数应该避免 panic。如果问题可以被 recover 或解决,让事情继续运行总比把整个程序拆掉要好。一个可能的反例是在初始化过程中:如果库真的无法自我创建,那么可以说,panic 是合理的。

var user = os.Getenv("USER")

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

Recover

https://golang.org/doc/effective_go.html#recover

当调用 panic 时,包括隐含的运行时错误,如索引分片出界或类型断言失败,它会立即停止执行当前函数,并开始解开(unwind) goroutine 的堆栈,沿途运行任何 defer 函数。如果该解卷(unwind)到达 goroutine 的栈顶,程序就会死亡。然而,可以使用内置函数 recover 来重新获得goroutine的控制权并恢复正常执行。

对 recover 的调用会停止解卷(unwind),并返回传递给 panic 的参数。因为在解卷(unwind)时只有在defer函数内部的代码才能运行,所以 recover 只在defer函数内部有用。

recover的一个应用是在服务器内部关闭一个失败的goroutine,而不杀死其他正在执行的goroutine。

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

在这个例子中,如果do(work) panic,结果会被记录下来,goroutine会干净利落地退出,而不会打扰到其他程序。在 defer 闭包中不需要做任何其他事情,调用recover就可以完全处理这个条件。

因为除非直接从 defer 函数中调用 recover,否则 recover 总是返回nil,所以 defer 代码可以调用本身使用panic和recover的库例程而不会失败。举个例子,safeDo中的 defer 函数可能会在调用 recover 之前调用一个日志函数,而这个日志代码的运行不会受到 panic 状态的影响。

有了我们的 recovery 模式,do函数(以及它所调用的任何东西)可以通过调用 panic 来干净利落地摆脱任何糟糕的情况。我们可以用这个想法来简化复杂软件中的错误处理。让我们看看一个理想化版本的 regexp 包,它通过调用 panic 与本地错误类型来报告解析错误。下面是Error的定义、错误方法和Compile函数。

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}

如果doParse panic,recover 块将把返回值设置为nil- defer 函数可以修改命名的返回值。然后,它将在对err的赋值中,通过断言它具有本地类型Error来检查问题是否是解析错误。如果没有,类型断言将失败,导致运行时错误,继续堆栈展开,就像什么都没有中断一样。这个检查意味着,如果发生了意外的事情,比如索引出界,即使我们使用panic和recover来处理解析错误,代码也会失败。

有了错误处理,错误方法(因为它是一个绑定到类型的方法,所以它的名字和内置的错误类型相同是很好的,甚至是很自然的)就可以很容易地报告解析错误,而不用担心手动解开解析栈。

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

虽然这个模式很有用,但它应该只在一个包内使用。Parse将其内部的 panic 调用转化为错误值;它不会将 panic 暴露给客户端。这是一个很好的规则。

顺便说一下,如果实际发生了错误,这个重新 panic 成语会改变panic值。然而,原始的和新的故障都会在崩溃报告中呈现,所以问题的根本原因仍然可见。因此,这种简单的重新panic方法通常已经足够了– 毕竟是崩溃,但如果你想只显示原始值,你可以多写一点代码来过滤意外的问题,并用原始错误重新panic。这就留给读者去练习了。

5 - 高级语言特性

Golang 高级语言特性

5.1 - 反射

Golang 的反射

5.2 - unsafe

Golang 的 unsafe

5.3 - 泛型

Golang 的泛型

6 - 并发

Golang 并发

6.1 - 并发原则

Golang 并发原则

6.1.1 - 并发原则概述

Golang 的并发原则概述

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存来通讯,而是通过通讯来共享内存。

Effective Go

https://golang.org/doc/effective_go.html#sharing

并发编程是一个很大的话题,这里仅有篇幅介绍一些Go特有的亮点。

在许多环境中,由于实现对共享变量的正确访问所需的微妙之处,使并发编程变得困难。Go鼓励一种不同的方法,其中共享值在通道上传递,事实上,从来没有被单独的执行线程主动共享。在任何时候,只有一个goroutine可以访问该值。通过设计,数据竞赛是不可能发生的。为了鼓励这种思维方式,我们将其简化为一句口号。

不要通过共享内存进行通信,而是通过通信来共享内存。

这种方法可能走得太远。比如说,引用计数可能最好的办法是在一个整数变量周围放一个mutex。但作为一种高级方法,使用通道来控制访问,可以更容易写出清晰、正确的程序。

思考这个模型的方法是考虑一个典型的单线程程序,它运行在一个CPU上。它不需要同步原语。现在运行另一个这样的实例;它也不需要同步。现在让这两个程序进行通信;如果通信的是同步器,那么仍然不需要其他同步。例如,Unix管道就完全符合这个模型。虽然Go的并发方法起源于Hoare的Communicating Sequential Processes(CSP),但它也可以被看作是Unix管道的类型安全泛化。

学习资料

6.1.2 - [博客] golang中的并发

博客文章 concurrency in golang

http://www.minaandrawos.com/2015/12/06/concurrency-in-golang/

昨天,我在Quora里回答了一个关于Go中并发模型的问题。现在,我觉得我想多说几句! Golang中的并发是该语言中最强大的特性之一。众多的人都涉及到了这个话题,他们的看法从非常简单到过于复杂不等。今天,轮到我说说我的看法了。

Golang中的并发是一种思维方式,不仅仅是语法。为了利用Go的力量 ,你需要先了解Go是如何处理代码的并发执行的。Go依赖于一个名为CSP ( Communicating Sequential Processes)的并发模型,在计算机科学中,CSP基本上是一个描述并发系统之间交互的模型。但由于这不是一篇科学论文,我将跳过这些形式化的东西,跳到它的实际描述中去。

在Golang中解释并发性时,很多Go讲座、演示和文档都会使用以下描述:

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存来通信,而是通过通信来共享内存。

听起来不错,不错。但这到底是什么意思呢。我花了一段时间才用脑子完全理解这个概念。但一旦我做到了,用Go编程对我来说就变得更加流畅了。阿尔伯特-爱因斯坦曾经说过:“如果你不能简单地解释它,你就不能很好地理解它。” ,所以这是我能想到的最简单的解释。

不要通过共享内存进行通信

在主流的编程语言中,当你想到代码的并发执行时,你大多会想到一堆线程并行运行,执行某种复杂的操作。那么,大多数情况下,你需要在不同的线程之间共享数据结构/变量/内存/什么的。你可以通过锁定这块内存来实现,这样就不会有两个线程同时访问/写入它,或者你只是让它自由漫游,并希望得到最好的结果。在很多流行的编程语言中,这通常是不同线程 “通信"的方式,这通常会导致各种问题,如竞争条件,内存管理 ,随机-奇怪-无法解释的异常-唤醒-你-整夜……等等。

而是通过通信来共享内存

那么Go是怎么做的呢?Go不锁定变量来共享内存,而是允许将存储在变量中的值从一个线程通信(或发送)到另一个线程(实际上,这并不完全是一个线程,但我们现在就把它当作一个线程)。默认的行为是发送数据的线程和接收数据的线程都会等待,直到值到达目的地。线程的 “等待"迫使线程之间在交换数据时进行适当的同步。在挽起袖子开始设计代码之前,先这样思考并发性问题;可以让软件更加稳定。

更明确的说:稳定性来自于这样一个事实 —— 默认情况下,发送线程和接收线程都不会做任何事情,直到值传输完成。意思是说,在另一个线程完成数据传输之前,任何一个线程对数据进行操作,都不会出现竞争条件或类似的问题。

Go提供了原生特性,你可以使用这些特性来实现这种行为,而不需要调用额外的类库或框架,这种行为只是简单地内置在语言中。如果你需要的话,Go还允许你拥有一个 “缓冲通道”。这意味着在某些情况下,你不希望两个线程都锁定或同步,直到一个值被传输,相反,你希望只有当你在两个线程之间的通道中填满了预定义数量的待处理值时,同步/锁定才会发生。

不过需要提醒的是,这种模式可能会被过度使用。你必须感觉到什么时候使用它,或者什么时候恢复到老式的共享内存模型。例如,引用计数最好在锁里面保护,文件访问也是如此。Go也会通过同步包在那里支持你。

golang中的并发编码

那么我们就来聊聊代码吧。我们如何实现逐个通信模式的共享呢? 请继续阅读

在Go中,“goroutine"服务于我们上面描述的线程的概念。实际上,它并不是真正的线程,它基本上是一个函数,它可以和其他goroutine在同一个地址空间中并发运行。它们在O.S.线程之间是多路复用的,所以如果一个线程阻塞,其他线程可以继续运行。所有的同步和内存管理都是由Go原生完成的。它们之所以不是真正的线程,是因为它们不一定是一直并行的。然而,由于多路复用和同步,你会得到并发行为。要启动一个新的goroutine,你只需要使用关键字 “go”。

go processdataFunction()

“go channel"是Go中实现并发的另一个关键概念。这是用于在goroutine之间进行内存通信的通道。要创建一个通道,可以使用 “make”。

myChannel := make(chan int64)

创建一个缓冲通道,以便在goroutines等待之前允许更多的值被排队,看起来像这样:

myBufferedChannel := make(chan int64,4)

在上面的两个例子中,我假设在这之前没有创建通道变量。这就是为什么我使用”:=“来创建具有推断类型的变量,而不是”=",因为”=“只会赋值,而且如果之前没有声明该变量,将导致编译错误。

现在要使用一个通道,你可以使用”<-“符号。goroutine发送的值会像这样分配给通道。

mychannel <- 54

接收数值的goroutine将从通道中提取数值,并将其分配到一个新的变量中,就像这样:

myVar := <- mychannel

现在让我们看一个例子来展示Golang中的并发性:

package main

import (
    "fmt"
    "time"
)

func main() {
	ch := make(chan int)
	//create a channel to prevent the main program from exiting before the done signal is received
	done := make(chan bool)
	go sendingGoRoutine(ch)
	go receivingGoRoutine(ch,done)
	//This will prevent the program from exiting till a value is sent over the "done" channel, value doesn't matter
	<- done
}

func sendingGoRoutine(ch chan int){
	//start a timer to wait 5 seconds
	t := time.NewTimer(time.Second*5)
	<- t.C
	fmt.Println("Sending a value on a channel")
    //this goroutine will wait till another goroutine received the value
    ch <- 45
}

func receivingGoRoutine(ch chan int, done chan bool){
	//this gourtine will wait till the channel received a value
    v := <- ch
	fmt.Println("Received value ", v)
	done <- true
}

输出将是这样的:

Sending a value on a channel
Received value  45

6.2 - goroutine

Golang 并发中的 goroutine

6.3 - channel

Golang 并发中的 channel