《Go入门指南》学习笔记

前言

本文主要为博主在学习《Go 入门指南》的过程中记录的笔记,秉持着书越读越薄的原则,博主在学习过程中记录了个人认为较为重要的知识点,以便后续查阅和翻看。

对于某些原文讲解不够明白之处,博主通过搜集资料和自行代码验证,增加了补充说明。

第1章:Go 语言的起源,发展与普及

1.1 起源与发展

Go 语言时间轴:

  • 2007 年 9 月 21 日:雏形设计
  • 2009 年 11 月 10日:首次公开发布
  • 2010 年 1 月 8 日:当选 2009 年年度语言
  • 2010 年 5 月:谷歌投入使用
  • 2011 年 5 月 5 日:Google App Engine 支持 Go 语言

Go 语言的官方网站: golang.org

1.2 语言的主要特性与发展的环境和影响因素

1.2.1 影响 Go 语言发展的早期编程语言

Go 语言并不是凭空而造的,而是和 C++、Java 和 C# 一样属于 C 系。不仅如此,设计者们还汲取了其它编程语言的精粹部分融入到 Go 语言当中。

1.2.2 为什么要创造一门编程语言

  • C/C++ 的发展速度无法跟上计算机发展的脚步。
  • 相比计算机性能的提升,软件开发领域不被认为发展得足够快或者比硬件发展得更加成功(有许多项目均以失败告终),同时应用程序的体积始终在不断地扩大,这就迫切地需要一门具备更高层次概念的低级语言来突破现状。
  • 在 Go 语言出现之前,开发者们总是面临非常艰难的抉择,究竟是使用执行速度快但是编译速度并不理想的语言(如:C++),还是使用编译速度较快但执行效率不佳的语言(如:.NET、Java),或者说开发难度较低但执行速度一般的动态语言呢?显然,Go 语言在这 3 个条件之间做到了最佳的平衡:快速编译,高效执行,易于开发。

1.2.3 Go 语言的发展目标

Go 语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡。

Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。

Go 语言的另一个目标是对于网络通信、并发和并行编程的极佳支持,从而更好地利用大量的分布式和多核的计算机。

同样作为静态语言的 Go 语言的构建速度(编译和链接到机器代码的速度)上远优于 C/C++ 。

Go 语言采用的包模型通过严格的依赖关系检查机制来加快程序构建的速度,提供了非常好的可量测性。

Go 语言在执行速度方面也可以与 C/C++ 相提并论。

Go 语言依旧运行在某种意义上的虚拟机,以此来实现高效快速的垃圾回收(使用了一个简单的标记-清除算法)。

Go 语言还能够在运行时进行反射相关的操作。

Go 语言还支持调用由 C 语言编写的海量库文件(第 3.9 节),从而能够将过去开发的软件进行快速迁移。

1.2.4 指导设计原则

Go 语言通过减少关键字的数量(25 个)来简化编码过程中的混乱和复杂度。干净、整齐和简洁的语法也能够提高程序的编译速度。

Go 语言有一套完整的编码规范,可在 Go 语言编码规范 页面进行查看。

1.2.5 语言的特性

Go 语言没有类和继承的概念,但是它通过接口(interface)的概念来实现多态性。

Go 语言并不使用面向对象编程技术,有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。

函数是 Go 语言中的基本构件。

Go 语言使用静态类型,所以它是类型安全的一门语言,加上通过构建到本地代码,程序的执行速度也非常快。

作为强类型语言,隐式的类型转换是不被允许的,记住一条原则:让所有的东西都是显式的。

Go 语言其实也有一些动态语言的特性(通过关键字 var)。

1.2.6 语言的用途

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。

Go 语言同时也是一门可以用于实现一般目标的语言,例如对于文本的处理,前端展现,甚至像使用脚本一样使用它。

因为垃圾回收和自动内存分配的原因,Go 语言不适合用来开发对实时性要求很高的软件。

Go 语言可以在 Intel 或 ARM 处理器上运行,因此它也可以在安卓系统下运行。

1.2.7 关于特性缺失

许多能够在大多数面向对象语言中使用的特性 Go 语言都没有支持,但其中的一部分可能会在未来被支持。

  • 为了简化设计,不支持函数重载和操作符重载
  • 为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
  • Go 语言通过另一种途径实现面向对象设计(第 10-11 章)来放弃类和类型的继承
  • 尽管在接口的使用方面(第 11 章)可以实现类似变体类型的功能,但本身不支持变体类型
  • 不支持动态加载代码
  • 不支持动态链接库
  • 不支持泛型
  • 通过 recoverpanic 来替代异常机制(第 13.2-3 节)
  • 不支持静态变量

1.2.8 小结

这里列举一些 Go 语言的必杀技:

  • 简化问题,易于学习
  • 内存管理,简洁语法,易于使用
  • 快速编译,高效开发
  • 高效执行
  • 并发支持,轻松驾驭
  • 静态类型
  • 标准类库,规范统一
  • 易于部署
  • 文档全面
  • 免费开源

第2章:安装与运行环境

2.1 平台与架构

目前有2个版本的编译器:Go 原生编译器 gc 和非原生编译器 gccgo,这两款编译器都是在类 Unix 系统下工作 。其中,gc 版本的编译器已经被移植到 Windows 平台上,并集成在主要发行版中,可以通过安装 MinGW 从而在 Windows 平台下使用 gcc 编译器。这两个编译器都是以单通道的形式工作。

2.1.1 Go 原生编译器 gc

这款编译器使用非分代、无压缩和并行的方式进行编译,它的编译速度要比 gccgo 更快,产生更好的本地代码,但编译后的程序不能够使用 gcc 进行链接。

2.1.2 gccgo 编译器

一款相对于 gc 而言更加传统的编译器,使用 GCC 作为后端。GCC 是一款非常流行的 GNU 编译器,它能够构建基于众多处理器架构的应用程序。编译速度相对 gc 较慢,但产生的本地代码运行要稍微快一点。它同时也提供一些与 C 语言之间的互操作性。

2.1.3 文件扩展名与包(package)

Go 语言源文件的扩展名很显然就是 .go

C 文件使用后缀名 .c,汇编文件使用后缀名 .s。所有的源代码文件都是通过包(packages)来组织。包含可执行代码的包文件在被压缩后使用扩展名 .a(AR 文档)。

注意: 在创建目录时,文件夹名称永远不应该包含空格,而应该使用下划线 “_” 或者其它一般符号代替。

2.2 Go 环境变量

Go 开发环境依赖于一些操作系统环境变量,最好在安装 Go 之前就已经设置好他们。如果你使用的是 Windows 的话,你完全不用进行手动设置,Go 将被默认安装在目录 c:/go 下。这里列举几个最为重要的环境变量:

  • $GOROOT 表示 Go 在你的电脑上的安装位置,它的值一般都是 $HOME/go,当然,你也可以安装在别的地方。
  • $GOARCH 表示目标机器的处理器架构,它的值可以是 386、amd64 或 arm。
  • $GOOS 表示目标机器的操作系统,它的值可以是 darwin、freebsd、linux 或 windows。
  • $GOBIN 表示编译器和链接器的安装位置,默认是 $GOROOT/bin,如果你使用的是 Go 1.0.3 及以后的版本,一般情况下你可以将它的值设置为空,Go 将会使用前面提到的默认值。

Go 编译器支持交叉编译,也就是说你可以在一台机器上构建运行在具有不同操作系统和处理器架构上运行的应用程序,也就是说编写源代码的机器可以和目标机器有完全不同的特性(操作系统与处理器架构)。

为了区分本地机器和目标机器,你可以使用 $GOHOSTOS$GOHOSTARCH 设置本地机器的操作系统名称和编译体系结构,这两个变量只有在进行交叉编译的时候才会用到,如果你不进行显示设置,他们的值会和本地机器($GOOS$GOARCH)一样。

  • $GOPATH 默认采用和 $GOROOT 一样的值,但从 Go 1.1 版本开始,你必须修改为其它路径。它可以包含多个 Go 语言源码文件、包文件和可执行文件的路径,而这些路径下又必须分别包含三个规定的目录:srcpkgbin,这三个目录分别用于存放源码文件、包文件和可执行文件。
  • $GOARM 专门针对基于 arm 架构的处理器,它的值可以是 5 或 6,默认为 6。
  • $GOMAXPROCS 用于设置应用程序可使用的处理器个数与核数,详见第 14.1.3 节。

2.5 在 Windows 上安装 Go

可以在 Go 官网 页面(需要翻墙)下载到 Windows 系统下的一键安装包。国内同学可通过中科大镜像下载。

在完成安装包的安装之后,你只需要配置 $GOPATH 这一个环境变量就可以开始使用 Go 语言进行开发了,其它的环境变量安装包均会进行自动设置。在默认情况下,Go 将会被安装在目录 c:\go 下,但如果你在安装过程中修改安装目录,则可能需要手动修改所有的环境变量的值。

如果你想要测试安装是否成功,在 cmd 中运行命令 go version 即可。

除此之外,还可将如下代码另存为 hello_world.go 文件,然后在命令行中使用指令 go run hello_world.go 运行。

// hello_world.go
package main

func main() {
	println("Hello", "world")
}

如果发生错误 fatal error: can’t find import: fmt 则说明你的环境变量没有配置正确。

如果想在 Windows 下使用 cgo(调用 C 语言写的代码),则需要安装 MinGW,一般推荐安装 TDM-GCC。如果你使用的是 64 位操作系统,请务必安装 64 位版本的 MinGW。安装完成进行环境变量等相关配置即可使用。

2.6 安装目录清单

Go 安装目录($GOROOT)的文件夹结构应该如下所示:

README.md, AUTHORS, CONTRIBUTORS, LICENSE

  • /bin:包含可执行文件,如:编译器,Go 工具
  • /doc:包含示例程序,代码工具,本地文档等
  • /lib:包含文档模版
  • /misc:包含与支持 Go 编辑器有关的配置文件以及 cgo 的示例
  • /os_arch:包含标准库的包的对象文件(.a
  • /src:包含源代码构建脚本和标准库的包的完整源代码(Go 是一门开源语言)
  • /src/cmd:包含 Go 和 C 的编译器和命令行脚本

2.7 Go 运行时(runtime)

Go 拥有简单却高效的标记-清除回收器。使用一门具有垃圾回收功能的编程语言不代表你可以避免内存分配所带来的问题,分配和回收内容都是消耗 CPU 资源的一种行为。

Go 的可执行文件都比相对应的源代码文件要大很多,这恰恰说明了 Go 的 runtime 嵌入到了每一个可执行文件当中。总的来说,Go 的部署工作还是要比 Java 和 Python 轻松得多。因为 Go 不需要依赖任何其它文件,它只需要一个单独的静态文件,这样你也不会像使用其它语言一样在各种不同版本的依赖文件之间混淆。

第3章:编辑器、集成开发环境与其它工具

3.5 格式化代码

Go 开发团队制作了一个工具:go fmtgofmt)。这个工具可以将你的源代码格式化成符合官方统一标准的风格,属于语法风格层面上的小型重构。遵循统一的代码风格是 Go 开发中无可撼动的铁律,因此你必须在编译或提交版本管理系统之前使用 gofmt 来格式化你的代码。

Go 对于代码的缩进层级方面使用 tab 还是空格并没有强制规定,一个 tab 可以代表 4 个或 8 个空格。在实际开发中,1 个 tab 应该代表 4 个空格。至于开发工具方面,一般都是直接使用 tab 而不替换成空格。

在命令行输入 gofmt –w program.go 会格式化该源文件的代码然后将格式化后的代码覆盖原始内容(如果不加参数 -w 则只会打印格式化后的结果而不重写文件);gofmt -w *.go 会格式化并重写所有 Go 源文件;gofmt map1 会格式化并重写 map1 目录及其子目录下的所有 Go 源文件。

gofmt 也可以通过在参数 -r 后面加入用双引号括起来的替换规则实现代码的简单重构,规则的格式:<原始内容> -> <替换内容>

实例:

gofmt -r '(a) -> a' –w *.go

上面的代码会将源文件中没有意义的括号去掉。

3.6 生成代码文档

go doc 工具会从 Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档。

一般用法

  • go doc package 获取包的文档注释,例如:go doc fmt 会显示使用 godoc 生成的 fmt 包的文档注释。
  • go doc package/subpackage 获取子包的文档注释,例如:go doc container/list
  • go doc package function 获取某个函数在某个包中的文档注释,例如:go doc fmt Printf 会显示有关 fmt.Printf() 的使用说明。

这个工具只能获取在 Go 安装目录下 ../go/src 中的注释内容。此外,它还可以作为一个本地文档浏览 web 服务器。在命令行输入 godoc -http=:6060,然后使用浏览器打开 http://localhost:6060 后,你就可以看到本地文档浏览服务器提供的页面。

3.8 Go 性能说明

根据 Go 开发团队和基本的算法测试,Go 语言与 C 语言的性能差距大概在 10%~20% 之间( 该数据应为 2013 年 3 月 28 日之前产生 )。虽然没有官方的性能标准,但是与其它各个语言相比已经拥有非常出色的表现。

保守估计在相同的环境和执行目标的情况下,Go 程序比 Java 或 Scala 应用程序要快上 2 倍,并比这两门语言占用的内存降低了 70% 。

Go 就要比任何非静态和编译型语言快 2 到 10 倍,并且能够更加高效地使用内存。

根据 Robert Hundt(2011 年 6 月8)的文章对 C++、Java、Go 和 Scala,以及 Go 开发团队的反应,可以得出以下结论:

  • Go 和 Scala 之间具有更多的可比性(都使用更少的代码),而 C++ 和 Java 都使用非常冗长的代码。
  • Go 的编译速度要比绝大多数语言都要快,比 Java 和 C++ 快 5 至 6 倍,比 Scala 快 10 倍。
  • Go 的二进制文件体积是最大的(每个可执行文件都包含 runtime)。
  • 在最理想的情况下,Go 能够和 C++ 一样快,比 Scala 快 2 至 3 倍,比 Java 快 5 至 10 倍。
  • Go 在内存管理方面也可以和 C++ 相媲美,几乎只需要 Scala 所使用的一半,是Java的五分之一左右。

3.9 与其它语言进行交互

3.9.1 与 C 进行交互

工具 cgo 提供了对 FFI(外部函数接口)的支持,能够使用 Go 代码安全地调用 C 语言库,你可以访问 cgo 文档主页。cgo 会替代 Go 编译器来产生可以组合在同一个包中的 Go 和 C 代码。在实际开发中一般使用 cgo 创建单独的 C 代码包。

如果你想要在你的 Go 程序中使用 cgo,则必须在单独的一行使用 import "C" 来导入,一般来说你可能还需要 import "unsafe"

然后,你可以在 import "C" 之前使用注释(单行或多行注释均可)的形式导入 C 语言库(甚至有效的 C 语言代码),它们之间没有空行,例如:

// #include <stdio.h>
// #include <stdlib.h>
import "C"

名称 “C” 并不属于标准库的一部分,这只是 cgo 集成的一个特殊名称用于引用 C 的命名空间。在这个命名空间里所包含的 C 类型都可以被使用,例如 C.uintC.long 等等,还有 libc 中的函数 C.random() 等也可以被调用。

当你想要使用某个类型作为 C 中函数的参数时,必须将其转换为 C 中的类型,反之亦然,例如:

var i int
C.uint(i) 		// 从 Go 中的 int 转换为 C 中的无符号 int
int(C.random()) // 从 C 中 random() 函数返回的 long 转换为 Go 中的 int

下面的 2 个 Go 函数 Random()Seed() 分别调用了 C 中的 C.random()C.srandom()

示例:

package rand

// #include <stdlib.h>
import "C"

func Random() int {
	return int(C.random())
}

func Seed(i int) {
	C.srandom(C.uint(i))
}

C 当中并没有明确的字符串类型,如果你想要将一个 string 类型的变量从 Go 转换到 C 时,可以使用 C.CString(s);同样,可以使用 C.GoString(cs) 从 C 转换到 Go 中的 string 类型。

Go 的内存管理机制无法管理通过 C 代码分配的内存。

开发人员需要通过手动调用 C.free 来释放变量的内存:

defer C.free(unsafe.Pointer(Cvariable))

这一行最好紧跟在使用 C 代码创建某个变量之后,这样就不会忘记释放内存了。下面的代码展示了如何使用 cgo 创建变量、使用并释放其内存:

示例:

package print

// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

func Print(s string) {
	cs := C.CString(s)
	defer C.free(unsafe.Pointer(cs))
	C.fputs(cs, (*C.FILE)(C.stdout))
}

第4章:基本结构和基本数据类型

4.1 文件名、关键字与标识符

Go 的源文件以 .go 为后缀名存储在计算机中,这些文件名均由小写字母组成,如 scanner.go 。如果文件名由多个部分组成,则使用下划线 _ 对它们进行分隔,如 scanner_test.go 。文件名不包含空格或其他特殊字符。

Go 语言也是区分大小写的,有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 _)开头,然后紧跟着 0 个或多个字符或 Unicode 数字,如:X56、group1、_x23、i、өԑ12。

_ 本身就是一个特殊的标识符,被称为空白标识符。任何类型都可以赋值给它,但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

程序的代码通过语句来实现结构化。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

4.2 Go 程序的基本结构和要素

示例代码:

package main

import "fmt"

func main() {
	fmt.Println("hello, world")
}

4.2.1 包的概念、导入与可见性

包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。

如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。

你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package mainpackage main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。

注意:源文件的所属的包不一定是其所在文件夹,所有的包名都应该使用小写字母。一个文件夹下一般只能有一个包名;一个包不可以存在于两个文件夹下,两个文件夹下的包是两个不同包;一个包所有的文件,必须位于同一个目录下;包名可以和文件夹名不一样,毕竟一个是导入路径,一个是包名。

如果你打算编译包名不是为 main 的源文件,如 pack1,编译后产生的对象文件将会是 pack1.a 而不是可执行程序。

标准库

在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。一般情况下,标准包会存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目录下。

属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。

如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。

Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。

如果 A.go 依赖 B.go,而 B.go 又依赖 C.go

  • 编译 C.go, B.go, 然后是 A.go.
  • 为了编译 A.go, 编译器读取的是 B.o 而不是 C.o.

这种机制对于编译大型的项目时可以显著地提升编译速度。

每一段代码只会被编译一次

一个 Go 程序是通过 import 关键字将一组包链接在一起。

import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 "" 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代码。

如果需要多个包,它们可以被分别导入:

import "fmt"
import "os"

或:

import "fmt"; import "os"

但是还有更短且更优雅的方法(被称为因式分解关键字,该方法同样适用于 constvartype 的声明或定义):

import (
   "fmt"
   "os"
)

它甚至还可以更短的形式,但使用 gofmt 后将会被强制换行:

import ("fmt"; "os")

当你导入多个包时,最好按照字母顺序排列包名,这样做更加清晰易读。

如果包名不是以 ./ 开头,如 "fmt" 或者 "container/list",则 Go 会在全局文件进行查找;如果包名以 ./ 开头,则 Go 会在相对目录中查找;如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。

可见性规则

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。

注意:大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母。

因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。

包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 pack1.Thingpack2.Thing

可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,格式为 import newPackageName "packageName",如:import fm "fmt"

注意:如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 imported and not used: os,这正是遵循了 Go 的格言:“没有不必要的代码!“。

包的分级声明和初始化

可以在使用 import 导入包之后定义或声明 0 个或多个常量(const)、变量(var)和类型(type),这些对象的作用域都是全局的(在本包范围内),所以可以被本包中所有的函数调用,然后声明一个或多个函数(func)。

4.2.2 函数

main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误 undefined: main.mainmain 函数既没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。

在程序开始执行并完成初始化后,第一个调用(程序的入口点)的函数是 main.main()(如:C 语言),该函数一旦返回就表示程序已成功执行并立即退出。

函数里的代码(函数体)使用大括号 {} 括起来。

左大括号 { 必须与方法的声明放在同一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现错误提示:

build-error: syntax error: unexpected semicolon or newline before {

右大括号 } 需要被放在紧接着函数体的下一行。如果你的函数非常简短,你也可以将它们放在同一行:

func Sum(a, b int) int { return a + b }

对于大括号 {} 的使用规则在任何时候都是相同的(如:if 语句等)。

因此,符合规范的函数一般写成如下的形式:

func functionName(parameter_list) (return_value_list) {}

其中:

  • parameter_list 的形式为 (param1 type1, param2 type2, …)
  • return_value_list 的形式为 (ret1 type1, ret2 type2, …)

只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法(与骆驼命名法类似,只不过骆驼命名法是首字母小写,而帕斯卡命名法是首字母大写);否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。

当函数没有声明返回值列表时,函数体中不需要使用 return 语句。

4.2.3 注释

单行注释://

多行注释(块注释):/* */

package 语句之前的块注释将被默认为是这个包的文档说明,其中应该提供一些相关信息并对整体功能做简要的介绍。一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。

4.2.4 类型

声明变量的一般形式是使用 var 关键字:

var varName varType

使用 var 声明的变量的值会自动初始化为该类型的零值。

类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface

结构化的类型没有真正的值,它使用 nil 作为默认值(如果是 struct 值类型,则不为 nil,会为结构体中的各个字段赋零值。在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是 NULL0)。值得注意的是,Go 语言中不存在类型继承。

函数可以作为返回类型,这种类型的声明要写在函数名和可选的参数列表之后,例如:

func FunctionName (a typea, b typeb) typeFunc

使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体,但是也可以定义一个已经存在的类型的别名,如:

type IZ int

这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。

然后我们可以使用下面的方式声明变量:

var a IZ = 5

如果你有多个类型需要定义,可以使用因式分解关键字的方式,例如:

type (
   IZ int
   FZ float64
   STR string
)

4.2.5 Go 程序的一般结构

一个 Go 程序的首选结构如下:

package main

import (
   "fmt"
)

const c = "C"

var v int = 5

type T struct{}

func init() { // initialization of package
}

func main() {
   var a int
   Func1()
   // ...
   fmt.Println(a)
}

func (t T) Method1() {
   //...
}

func Func1() { // exported function Func1
   //...
}

这种结构并没有被强制要求,编译器也不关心 main 函数在前还是变量的声明在前,但使用统一的结构能够在从上至下阅读 Go 代码时有更好的体验。Go 程序结构总体思路如下:

  • 在完成包的 import 之后,开始对常量、变量和类型的定义或声明。
  • 如果存在 init 函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。
  • 如果当前包是 main 包,则定义 main 函数。
  • 然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。

Go 程序的执行(程序启动)顺序如下:

  1. 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
  2. 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
  3. 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init 函数的话,则调用该函数。
  4. 在完成这一切之后,main 也执行同样的过程,最后调用 main 函数开始执行程序。

4.2.6 类型转换

Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样:

valueOfTypeB = typeB(valueOfTypeA)

类型 B 的值 = 类型 B(类型 A 的值)

示例:

a := 5.0
b := int(a)

说明:= 是赋值, := 是声明变量并赋值。

// 使用 = 赋值,前提是使用 var 声明了变量,在函数内外均可使用
var c int = 3

// 使用 := 赋值,不需要 var 声明变量,系统自动推断类型,且只能在函数中使用
func fun1(){
    d := 4
}

4.2.7 Go 命名规范

Go 语言中变量名、常量名、函数名等使用帕斯卡命名法或者驼峰命名法,一般不使用下划线分割多个名称。

4.3 常量

常量使用关键字 const 定义,用于存储不会改变的数据。

存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

常量的定义格式:const identifier [type] = value,例如:

const Pi = 3.14159

在 Go 语言中,可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

  • 显式类型定义: const b string = "abc"
  • 隐式类型定义: const b = "abc"

一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时刻根据上下文来获得相关类型。

var n int
f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 int

常量的值必须在编译时就能够确定;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

  • 正确的做法:const c1 = 2/3
  • 错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() used as value

因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()

数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出:

const Ln2 = 0.693147180559945309417232121458\
			176568075500134360255254120680009
const Log2E = 1/Ln2 // this is a precise reciprocal
const Billion = 1e9 // float constant
const hardEight = (1 << 100) >> 97

根据上面的例子我们可以看到,反斜杠 \ 可以在常量表达式中作为多行的连接符使用。

无需担心常量之间的类型转换问题,因为它们都是非常理想的数字。

不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。另外,常量也允许使用并行赋值的形式:

const beef, two, c = "eat", 2, "veg"
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
	Monday, Tuesday, Wednesday = 1, 2, 3
	Thursday, Friday, Saturday = 4, 5, 6
)

常量还可以用作枚举:

const (
	Unknown = 0
	Female = 1
	Male = 2
)

如果在代码中试图修改常量的值则会引发编译错误。

const 的声明中,iota 可以作为一个从 0 开始的行数索引器。需要注意的是,iota 只能用于 const 声明中,并且每遇到一次 const 关键字,iota 就重置为 0

const (
	a = iota // 0
	b = iota // 1
	c = iota // 2
)
const (
	a = iota
	b
	c
)

也可以使用某个类型作为枚举常量的类型:

type Color int

const (
	RED Color = iota 	// 0
	ORANGE 				// 1
	YELLOW 				// 2
	GREEN 				// 3
	BLUE				// 4
	INDIGO				// 5
	VIOLET 				// 6
)

4.4 变量

4.4.1 简介

声明变量的一般形式是使用 var 关键字:var identifier type

Go 语言将变量的类型放在变量的名称之后,是为了避免像 C 语言中那样含糊不清的声明形式,例如:int* a, b;,其中只有 a 是指针而 b 不是。而在 Go 中,可直接写成:

var a, b *int

连续声明多个变量同样可以使用因式分解关键字的写法,一般用于声明全局变量。

var (
	a 	int
	b 	bool
	str string
)

当一个变量被声明之后,系统自动赋予它该类型的零值:int0float0.0boolfalsestring 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。

如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后,即变量名首字母大写)使用。

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。

可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放)。

变量的声明与赋值(初始化)语句可以组合起来,格式为 var identifier [type] = value;也可以先声明,再赋值。

Go 编译器在编译时就可以根据变量的值来自动推断其类型(Ruby 和 Python 这类动态语言是在运行时进行推断的)。因此,可以使用下面的这些形式来声明及初始化变量:

var a = 15
var b = false
var str = "Go says hello to the world!"

或者可以使用因式分解的写法:

var (
	a 	= 15
	b 	= false
	str = "Go says hello to the world!"
	city string
)

自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,还是需要显式指定变量的类型,例如:

var n int64 = 2

然而,var a 这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据。

变量的类型也可以在运行时实现自动推断,例如:

var (
	HOME 	= os.Getenv("HOME")
	USER 	= os.Getenv("USER")
	GOROOT 	= os.Getenv("GOROOT")
)

这种写法主要用于声明包级别的全局变量。

在函数体内声明局部变量时,应使用简短声明语法 :=,例如:

a := 1

4.4.2 值类型和引用类型

值类型

所有像 intfloatboolstring 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。

值类型的变量的值存储在栈中,可以通过 &i 来获取变量 i 的内存地址。

引用类型

同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。

被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。

4.4.3 打印

格式化字符串输出:

func Printf(format string, list of variables to be printed)

使用方式同 C 语言中的 printf()。需要注意的是,格式化标识符 %v 代表使用类型的默认输出格式的标识符。

函数 fmt.SprintfPrintf 的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者。

函数 fmt.Printfmt.Println 可接收多个参数,会自动使用格式化标识符 %v 对参数进行格式化,并且在每个参数之间自动增加空格;唯一不同的是后者还会在字符串的最后加上一个换行符。

4.4.4 简短形式,使用 := 赋值操作符

在变量的初始化时可省略变量的类型而由系统自动推断,此时声明语句写上 var 关键字就显得有些多余了,因此我们可以将它们简写为 varName := value 形式。

这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。

注意事项

如果在相同的代码块中,不可以再次对于相同名称的变量使用初始化声明 :=,否则编译器会提示错误 no new variables on left side of :=,因为这相当于在同一代码块中再次声明已声明的变量;但是使用 = 给变量再次赋值是可以的。

如果声明了一个局部变量(包括赋值)却没有在相同的代码块中使用它,会得到编译错误 a declared and not used。但是全局变量是允许声明但不使用。

多变量可以在同一行进行赋值,如:

a, b, c = 5, 7, "abc"

上面这行假设了变量 abc 都已经被声明,否则的话应该这样使用:

a, b, c := 5, 7, "abc"

这被称为 并行同时 赋值。

如果想要交换两个变量的值,则可以简单地使用 a, b = b, a

空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。

_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。

并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:val, err = Func1(var1)

4.4.5 init 函数

变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。

每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 backend()

func init() {
   // setup preparations
   go backend()
}

4.5 基本类型和运算符

4.5.1 布尔类型 bool

布尔型的值只可以是常量 true 或者 false

两个类型相同的值可以使用相等 == 或者不等 != 运算符来进行比较并获得一个布尔型的值。

&&、或 ||、非 ! 使用方式同 C。

对于布尔值的好的命名能够很好地提升代码的可读性,例如以 is 或者 Is 开头的 isSortedisFinishedisVisible

4.5.2 数字类型

4.5.2.1 整型 int 和浮点型 float

Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。

Go 也有基于架构的类型,例如:intuintuintptr

这些类型的长度都是根据运行程序所在的操作系统类型所决定的:

  • intuint 在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。
  • uintptr 的长度被设定为足够存放一个指针即可。

Go 语言中没有 float 类型(只有 float32float64),没有 double 类型。

与操作系统架构无关的类型都有固定的大小,并在类型的名称中就可以看出来:

整数:

  • int8(-128 -> 127)
  • int16(-32768 -> 32767)
  • int32(-2,147,483,648 -> 2,147,483,647)
  • int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)

无符号整数:

  • uint8(0 -> 255)
  • uint16(0 -> 65,535)
  • uint32(0 -> 4,294,967,295)
  • uint64(0 -> 18,446,744,073,709,551,615)

浮点型(IEEE-754 标准):

  • float32(± 1e-45 -> ± 3.4 * 1e38)
  • float64(± 5 * 1e-324 -> 107 * 1e308)

int 型是计算最快的一种类型。

整型的零值为 0,浮点型的零值为 0.0

float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。由于精确度的缘故,在使用 == 或者 != 来比较浮点数时应当非常小心。

应该尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。

对于数值类型,前缀 0 表示 8 进制数(如:077),前缀 0x 表示 16 进制数(如:0xFF),前缀 0B0b 表示二进制数;使用 e 可以表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。

Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用,下面这个程序很好地解释了这个现象(该程序无法通过编译):

package main

func main() {
	var a int
	var b int32
	a = 15
	b = a + a	 // 编译错误
	b = b + 5    // 因为 5 是常量,所以可以通过编译
}

如果尝试编译该程序,则将得到编译错误 cannot use a + a (type int) as type int32 in assignment

同样地,int16 也不能够被隐式转换为 int32

4.5.2.2 复数

Go 拥有以下复数类型:

complex64 (32 位实数 + 32 位虚数)
complex128 (64 位实数 + 64 位虚数)

复数使用 re+imi 来表示,其中 re 代表实数部分,im 代表虚数部分,i 代表根号负 1。示例:

var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1)
// 输出: 5 + 10i

如果 reim 的类型均为 float32,那么类型为 complex64 的复数 c 可以通过以下方式来获得:

c = complex(re, im)

函数 real(c)imag(c) 可以分别获得相应的实数和虚数(imaginary number)部分。

复数支持和其它数字类型一样的运算。当使用等号 == 或者不等号 != 对复数进行比较运算时,注意对精确度的把握。cmath 包中包含了一些操作复数的公共方法。如果对内存的要求不是特别高,最好使用 complex128 作为计算类型,因为相关函数都使用这个类型的参数。

4.5.2.3 位运算

位运算只能用于整数类型的变量。

二元运算符

  • 按位与 &
  • 按位或 |
  • 按位异或 ^
  • 位清除 &^:如果运算符右边数据的二进制形式包含值为 1 的位,那么运算符左边数据二进制形式相应的位会被置零。
package main
import "fmt"

func main() {
	var a int = 0b10101010
	var b int = 0b10100101
	fmt.Printf( "%b\n", a &^ b)	// 输出 1010
}

一元运算符

  • 按位取反 ^,包括符号位
package main
import "fmt"

func main() {
	var a uint8 = 20
	fmt.Println(^a, ^20)	//输出:235 -21
}

之所以如此,是因为 a 为无符号数,按位取反仍然为无符号数;字面量 20 被默认为 int 类型,按位取反结果为有符号数。

一个有符号数按位取反的结果为这个数加一的相反数,例如 ^(-20) = 19

  • 位左移 <<

用法:bitP << nbitP 的位向左移动 n 位,右侧空白部分使用 0 填充;如果 n 等于 2,则结果是 2 的相应倍数,即 2n 次方。例如:

1 << 10 // 等于 1 KB
1 << 20 // 等于 1 MB
1 << 30 // 等于 1 GB
  • 位右移 >>

用法:bitP >> nbitP 的位向右移动 n 位,左侧空白部分使用 0 填充。

位左移常见实现存储单位的用例

使用位左移与 iota 计数配合可优雅地实现存储单位的常量枚举:

type ByteSize float64
const (
	_ = iota // 通过赋值给空白标识符来忽略值
	KB ByteSize = 1<<(10*iota)
	MB
	GB
	TB
	PB
	EB
	ZB
	YB
)

在通讯中使用位左移表示标识的用例

type BitFlag int
const (
	Active BitFlag = 1 << iota 	// 1 << 0 == 1
	Send 						// 1 << 1 == 2
	Receive 					// 1 << 2 == 4
)

flag := Active | Send // == 3
4.5.2.4 逻辑运算符

Go 中拥有以下逻辑运算符:==!=<<=>>=

4.5.2.5 算术运算符

Go 在进行字符串拼接时允许使用对运算符 + 的重载,但 Go 本身不允许开发者进行自定义的运算符重载。

/ 对于整数运算而言,结果依旧为整数,例如:9 / 4 -> 2

取余运算符只能作用于整数:9 % 4 -> 1

整数除以 0 可能导致程序崩溃,将会导致运行时的恐慌状态(如果除以 0 的行为在编译时就能被捕捉到,则会引发编译错误)。

浮点数除以 0.0 会返回一个无穷尽的结果,使用 +Inf 表示。

语句 b = b + a 可简写为 b += a,同样的写法也可用于 -=*=/=%=

对于整数和浮点数,可以使用一元运算符 ++(递增)和 --(递减),但只能用于后缀。同时,带有 ++-- 的只能作为语句,而非表达式。因此 n = i++ 这种写法是无效的,其它像 f(i++) 或者 a[i]=b[i++] 这些可以用于 C、C++ 和 Java 中的写法在 Go 中也是不允许的。

在运算时 溢出 不会产生错误,Go 会简单地将超出位数抛弃。

4.5.2.6 随机数

代码示例:

package main
import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	for i := 0; i < 10; i++ {
		a := rand.Int()
		fmt.Printf("%d / ", a)
	}
	fmt.Printf("\n\n")
	for i := 0; i < 5; i++ {
		r := rand.Intn(8)
		fmt.Printf("%d / ", r)
	}
	fmt.Printf("\n\n")
	timens := int64(time.Now().Nanosecond())
	rand.Seed(timens)
	for i := 0; i < 10; i++ {
		fmt.Printf("%2.2f / ", 100*rand.Float32())
	}
}

可能的输出:

816681689 / 1325201247 / 623951027 / 478285186 / 1654146165 / 1951252986 / 2029250107 / 762911244 / 1372544545 / 591415086 / 

3 / 0 / 6 / 4 / 2 

/22.10 / 65.77 / 65.89 / 16.85 / 75.56 / 46.90 / 55.24 / 55.95 / 25.58 / 70.61 /

函数 rand.Float32rand.Float64 返回介于 [0.0, 1.0) 之间的伪随机数,其中包括 0.0 但不包括 1.0。函数 rand.Intn 返回介于 [0, n) 之间的伪随机数。

可以使用 Seed(value) 函数来提供伪随机数的生成种子,一般情况下都会使用当前时间的纳秒级数字。

4.5.3 运算符与优先级

下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:

优先级 	运算符
 7 		^ !
 6 		* / % << >> & &^
 5 		+ - | ^
 4 		== != < <= >= >
 3 		<-
 2 		&&
 1 		||

4.5.4 类型别名

定义类型别名的语法如下:

type newType realType

例如,type myInt int

实际上,类型别名得到的新类型并非和原类型完全相同,新类型不会拥有原类型所附带的方法(第 10 章);新类型可以自定义方法。

4.5.5 字符类型

严格来说,这并不是 Go 语言的一个类型,字符只是整数的特殊用例。byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题。例如:var ch byte = 'A';字符使用单引号括起来。

在 ASCII 码表中,A 的值是 65,而使用 16 进制表示则为 41,所以下面的写法是等效的:

var ch1 byte = 65
var ch2 byte = '\x41'

\x 总是紧跟着长度为 2 的 16 进制数)

另外一种可能的写法是 \ 后面紧跟着长度为 3 的八进制数,例如:\377

Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。其实 rune 也是 Go 当中的一个类型,并且是 int32 的别名。

在书写 Unicode 字符时,需要在 16 进制数之前加上前缀 \u 或者 \U。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则会加上 \U 前缀;前缀 \u 则总是紧跟着长度为 4 的 16 进制数,前缀 \U 紧跟着长度为 8 的 16 进制数。

var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3)   // UTF-8 code point

输出:

65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

格式化说明符 %c 用于表示字符;当和字符配合使用时,%v%d 会输出用于表示该字符的整数;%U 输出格式为 U+hhhh (h 表示一个 16 进制数)的字符串。

unicode 包含了一些针对测试字符的非常有用的函数(其中 ch 代表字符):

  • 判断是否为字母:unicode.IsLetter(ch)
  • 判断是否为数字:unicode.IsDigit(ch)
  • 判断是否为空白符号:unicode.IsSpace(ch)

4.6 字符串

字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。由于该编码对占用字节长度的不定性,Go 中的字符串里面的字符也可能根据需要占用 1 至 4 个字节。

字符串是一种值类型,且值不可变,即创建某个文本后你无法再次修改这个文本的内容;更深入地讲,字符串是字节的定长数组。

Go 支持以下 2 种形式的字面值:

  • 解释字符串:

    该类字符串使用双引号括起来,其中的相关的转义字符将被替换,这些转义字符包括:

    • \n:换行符
    • \r:回车符
    • \t:tab 键
    • \u\U:Unicode 字符
    • \\:反斜杠自身
  • 非解释字符串:

    该类字符串使用反引号括起来,支持换行,例如:

    `This is a raw string \n` 中的 "\n"会被原样输出。
    

和 C/C++不一样,Go 中的字符串是根据长度限定,而非特殊字符 \0

string 类型的零值为长度为零的字符串,即空字符串 ""

一般的比较运算符(==!=<<=>=>)通过在内存中按字节比较来实现字符串的对比。可以通过函数 len() 来获取字符串所占的字节长度,例如:

package main
import "fmt"

func main() {
	var str1 string = "Mason"
	var str2 string = "梅森"
	fmt.Println(len(str1), len(str2))	// 输出:5 6
}

字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数:

  • 字符串 str 的第 1 个字节:str[0]
  • i 个字节:str[i - 1]
  • 最后 1 个字节:str[len(str)-1]

需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。

package main
import "fmt"

func main() {
	var str string = "胡同学"
	fmt.Printf("%c", str[0])	// 输出:è
}

注意事项:获取字符串中某个字节的地址的行为是非法的,例如:&str[i]

字符串拼接符 +

两个字符串 s1s2 可以通过 s := s1 + s2 拼接在一起。s2 追加在 s1 尾部并生成一个新的字符串 s

str := "Beginning of the string " +
	"second part of the string"

由于编译器行尾自动补全分号的缘故,加号 + 必须放在第一行。

在循环中使用加号 + 拼接字符串并不是最高效的做法,更好的办法是使用函数 strings.Join(),使用字节缓冲(bytes.Buffer)拼接更加给力!

使用 strings.Join() 函数示例:

package main
import (
	"fmt"
	"strings"
)

func main() {
	str := strings.Join([]string{"a", "b", "c", "d"}, "-")
	fmt.Print(str)	// 输出:a-b-c-d
}

使用 bytes.Buffer 函数示例:

package main
import (
	"fmt"
	"bytes"
)

func main() {
	var strs = []string{"a", "b", "c", "d"}
	// Buffer 是一个实现了读写方法的可变大小的字节缓冲
	var buffer bytes.Buffer
	for i := 0; i < len(strs); i++ {
		buffer.WriteString(strs[i])
	}
	str := buffer.String()
	fmt.Print(str)	// 输出:abcd
}

4.7 strings 和 strconv 包

Go 中使用 strings 包来完成对字符串的主要操作。

4.7.1 前缀和后缀

HasPrefix 判断字符串 s 是否以 prefix 开头:

strings.HasPrefix(s, prefix string) bool

HasSuffix 判断字符串 s 是否以 suffix 结尾:

strings.HasSuffix(s, suffix string) bool

4.7.2 字符串包含关系

Contains 判断字符串 s 是否包含 substr

strings.Contains(s, substr string) bool

4.7.3 判断子字符串或字符在父字符串中出现的位置(索引)

Index 返回字符串 str 在字符串 s 中的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str

strings.Index(s, str string) int

LastIndex 返回字符串 str 在字符串 s 中最后出现位置的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str

strings.LastIndex(s, str string) int

如果需要查询非 ASCII 编码的字符在父字符串中的位置,建议使用以下函数来对字符进行定位:

strings.IndexRune(s string, r rune) int

实际使用中的第二个参数 r 可以是 runeint,例如:

strings.IndexRune("chicken", 99) 
strings.IndexRune("chicken", rune('k'))

4.7.4 字符串替换

Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new

strings.Replace(str, old, new, n) string

示例:

package main
import (
	"fmt"
	"strings"
)

func main() {
	str := "11aa11aa11aa11"
	old := "11"
	new := "22"
	// n < 0, 用 new 替换所有匹配上的 old
	fmt.Println(strings.Replace(str, old, new, -1))
	// n = 0, 不替换任何匹配上的 old
	fmt.Println(strings.Replace(str, old, new, 0))
	// n = 2, 用 new 替换前两个匹配上的 old
	fmt.Println(strings.Replace(str, old, new, 2))
	// n = 5, 用 new 替换前五个(实际没有那么多)匹配上的 old, 效果等同于全部替换
	fmt.Println(strings.Replace(str, old, new, 5))
}

输出:

22aa22aa22aa22
11aa11aa11aa11
22aa22aa11aa11
22aa22aa22aa22

4.7.5 统计字符串出现次数

Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数:

strings.Count(s, str string) int

4.7.6 重复字符串

Repeat 用于重复 count 次字符串 s 并返回一个新的字符串:

strings.Repeat(s, count int) string

4.7.7 修改字符串大小写

ToLower 将字符串中的 Unicode 字符全部转换为相应的小写字符:

strings.ToLower(s) string

ToUpper 将字符串中的 Unicode 字符全部转换为相应的大写字符:

strings.ToUpper(s) string

注意:上面两个函数只会对 ASCII 字符起效。

4.7.8 修剪字符串

可以使用 strings.TrimSpace(s) 来剔除字符串开头和结尾的空白符号;如果想要剔除指定字符,则可以使用 strings.Trim(s, "cut") 来将开头和结尾的 cut 去除掉。该函数的第二个参数可以包含任何字符,如果只想剔除开头或者结尾的字符串,则可以使用 TrimLeft 或者 TrimRight 来实现。

4.7.9 分割字符串

strings.Fields(s) 将会利用 1 个或多个空白符号来作为动态长度的分隔符将字符串分割成若干小块,并返回一个 slice,如果字符串只包含空白符号,则返回一个长度为 0slice

strings.Split(s, sep) 用于自定义分割符号来对指定字符串进行分割,同样返回 slice

4.7.10 拼接 slice 到字符串

Join 用于将元素类型为 stringslice 使用分割符号来拼接组成一个字符串:

strings.Join(sl []string, sep string) string

示例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "The quick brown fox jumps over the lazy dog"
	sl := strings.Fields(str)
	fmt.Printf("Splitted in slice: %v\n", sl)
	for _, val := range sl {
		fmt.Printf("%s - ", val)
	}
	fmt.Println()
	str2 := "GO1|The ABC of Go|25"
	sl2 := strings.Split(str2, "|")
	fmt.Printf("Splitted in slice: %v\n", sl2)
	for _, val := range sl2 {
		fmt.Printf("%s - ", val)
	}
	fmt.Println()
	str3 := strings.Join(sl2,";")
	fmt.Printf("sl2 joined by ;: %s\n", str3)
}

输出:

Splitted in slice: [The quick brown fox jumps over the lazy dog]
The - quick - brown - fox - jumps - over - the - lazy - dog -
Splitted in slice: [GO1 The ABC of Go 25]
GO1 - The ABC of Go - 25 -
sl2 joined by ;: GO1;The ABC of Go;25

4.7.11 从字符串中读取内容

函数 strings.NewReader(str) 用于生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针,从其它类型读取内容的函数还有:

  • Read()[]byte 中读取内容。
  • ReadByte()ReadRune() 从字符串中读取下一个 byte 或者 rune

4.7.12 字符串与其它类型的转换

与字符串相关的类型转换都是通过 strconv 包实现的。

该包包含了一些变量用于获取程序运行的操作系统平台下 int 类型所占的位数,如:strconv.IntSize

任何类型 T 转换为字符串总是成功的。

针对从数字类型转换到字符串,Go 提供了以下函数:

  • strconv.Itoa(i int) string 返回数字 i 所表示的字符串类型的十进制数。

将字符串转换为其它类型 tp 并不总是可能的,可能会在运行时抛出错误 parsing "…": invalid argument

针对从字符串类型转换为数字类型,Go 提供了以下函数:

  • strconv.Atoi(s string) (i int, err error) 将字符串转换为 int 型。
  • strconv.ParseFloat(s string, bitSize int) (f float64, err error) 将字符串转换为 float64 型。

4.8 时间和日期

time 包提供了一个数据类型 time.Time(作为值使用)以及显示和测量时间和日期的功能函数。

当前时间可以使用 time.Now() 获取,或者使用 t.Day()t.Minute() 等等来获取时间的一部分;甚至可以自定义时间格式化字符串,例如: fmt.Printf("%02d.%02d.%4d\n", t.Day(), t.Month(), t.Year()) 将会输出 21.07.2011

Duration 类型表示两个连续时刻所相差的纳秒数,类型为 int64Location 类型映射某个时区的时间,UTC 表示通用协调世界时间。

4.9 指针

Go 语言为程序员提供了控制数据结构的指针的能力;但是,不能进行指针运算。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

如果我们想调用指针 ptr(指针变量的常用缩写形式),我们可以这样声明它:

var ptr *type

当一个指针被定义后没有分配到任何变量时,它的值为 nil。指针的格式化标识符为 %p

一个指针变量可以指向任何一个值的内存地址。它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。

可以在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。

指针的一个高级应用是可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。

注意事项

在书写表达式类似 var ptr *type 时,切记在 * 号和指针名称间留有一个空格,因为尽管 var ptr*type 是语法正确的,但是在更复杂的表达式中,它容易被误认为是一个乘法表达式!

对于任何一个变量 var1, 如下表达式都是正确的:var1 == *(&var1)

下面是使用指针操作字符串的例子:

package main
import "fmt"

func main() {
	s := "good bye"
	var p *string = &s
	*p = "welcome"
	fmt.Printf("%p\n", p) // 0xc00004a1f0
	fmt.Printf("%s\n", *p) // welcome
	fmt.Printf("%s\n", s) // welcome
}

注意,不能获取字面量或常量的地址,例如:

const i = 5
ptr := &i 	// error: cannot take the address of i
ptr2 := &10 // error: cannot take the address of 10

对一个空指针的反向引用是不合法的,并且会使程序崩溃:

package main
import "fmt"

func main() {
	var ptr *int = nil
	fmt.Println(*ptr)
	// panic: runtime error: invalid memory address or nil pointer dereference
}

第5章:控制结构

5.1 if-else 结构

if-else 的一般结构如下:

if condition1 {
	// do something	
} else if condition2 {
	// do something else	
} else {
	// catch-all or default
}

if 可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号):

if initialization; condition {
	// do something
}

例如:

val := 10
if val > max {
	// do something
}

可以这样写:

if val := 10; val > max {
	// do something
}

但要注意的是,使用简短方式 := 声明的变量的作用域只存在于 if 结构中(在 if 结构的大括号之间,如果使用 if-else 结构则在 else 代码块中变量也会存在)。

注意事项:

  • 即使当代码块之间只有一条语句时,大括号也不可被省略。
  • 关键字 ifelse 之后的左大括号 { 必须和关键字在同一行,如果使用了 else if 结构,则前段代码块的右大括号 } 必须和 else if 关键字在同一行。这两条规则都是被编译器强制规定的。
  • 在有些情况下,条件语句两侧的括号是可以被省略的;当条件比较复杂时,则可以使用括号让代码更易读。

5.2 测试多返回值函数的错误

Go 语言的函数经常使用两个返回值来表示执行是否成功:返回某个值以及 true 表示成功;返回零值(或 nil)和 false 表示失败。当不使用 truefalse 的时候,也可以使用一个 error 类型的变量来代替作为第二个返回值:成功执行的话,error 的值为 nil,否则就会包含相应的错误信息(Go 语言中的错误类型为 error: var err error)。

如果想要在错误发生的同时终止程序的运行,可以使用 os 包的 Exit 函数:

if err != nil {
	fmt.Printf("Program stopping with error %v", err)
	os.Exit(1)
}

5.3 switch 结构

5.3.1 常规 switch 形式

相比较 C 和 Java 等其它语言而言,Go 语言中的 switch 结构使用上更加灵活。它接受任意形式的表达式:

switch var1 {
	case val1:
		...
	case val2:
		...
	default:
		...
}

变量 var1 可以是任何类型,而 val1val2 则可以是同类型的任意值。前花括号 { 必须和 switch 关键字在同一行。

可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3

每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说不需要特别使用 break 语句来表示结束。

如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。例如:

package main
import "fmt"

func main() {
	var a int = 5
	switch a {
	case 1, 2, 3, 4: 
		fmt.Println("The integer is equal to 1 or 2 or 3 or 4")
	case 5: 
		fmt.Println("The integer is equal to 5")
		fallthrough
	case 6: 
		fmt.Println("The integer is equal to 6")
		fallthrough
	case 7: 
		fmt.Println("The integer is equal to 7")
	default: 
		fmt.Println("The integer is bigger than 7")
	}
}

输出:

The integer is equal to 5
The integer is equal to 6
The integer is equal to 7

case ...: 语句之后,不需要使用花括号将多行语句括起来,但可以在分支中进行任意形式的编码。当代码块只有一行时,可以直接放置在 case 语句之后。

同样可以使用 return 语句来提前结束代码块的执行。在 switch 语句块中使用 return 语句时,并且函数是有返回值的,那么还需要在 switch 之后添加相应的 return 语句以确保函数始终会返回。

可选的 default 分支可以出现在任何顺序,但最好将它放在最后。如果没有将 default 放在分支的最后, default 之后的 case ...: 语句也可以被匹配到。例如:

package main
import "fmt"

func main() {
	var a int = 6
	switch a {
	case 1, 2, 3, 4: fmt.Println("The integer is equal to 1 or 2 or 3 or 4")
	case 5: fmt.Println("The integer is equal to 5")
	default: fmt.Println("The integer is bigger than 7")
	case 6: fmt.Println("The integer is equal to 6")
	case 7: fmt.Println("The integer is equal to 7")
	}
}

输出:

The integer is equal to 6

5.3.2 无表达式 switch 形式

switch 后可以没有表达式,此时会匹配 true。例如:

package main
import "fmt"

func main() {
	var num int = 7
	switch {
	    case num < 0: fmt.Println("Number is negative")
	    case num > 0 && num < 10: fmt.Println("Number is between 0 and 10")
	    default: fmt.Println("Number is 10 or greater")
	}
}

输出:

Number is between 0 and 10

5.3.3 包含初始化语句 switch 语句

例如:

switch result := calculate() {
	case result < 0:
		...
	case result > 0:
		...
	default:
		// 0
}

5.4 for 结构

5.4.1 基于计数器的迭代

示例:

package main
import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		fmt.Printf("This is the %d iteration\n", i)
	}
}

输出:

This is the 0 iteration
This is the 1 iteration
This is the 2 iteration
This is the 3 iteration
This is the 4 iteration

由三部分组成的循环的头部,它们之间使用分号 ; 相隔,但并不需要括号 () 将它们括起来。例如:for (i = 0; i < 10; i++) { },这是无效的代码!

同样的,左花括号 { 必须和 for 语句在同一行,计数器的生命周期在遇到右花括号 } 时便终止。

5.4.2 基于条件判断的迭代

基本形式为:for 条件语句 {}。例如:

package main
import "fmt"

func main() {
	var i int = 5
	for i >= 0 {
		i = i - 1
		fmt.Printf("The variable i is now: %d\n", i)
	}
}

5.4.3 无限循环

条件语句是可以被省略的,如 i:=0; ; i++for { }for ;; { };; 会在使用 gofmt 时被移除)。这些循环的本质就是无限循环。最后一个形式也可以被改写为 for true { },但一般情况下都会直接写 for { }

5.4.4 for-range 结构

这是 Go 特有的一种的迭代结构,它可以迭代任何一个集合(包括数组和 map)。语法上很类似其它语言中 foreach 语句,但依旧可以获得每次迭代所对应的索引。一般形式为:for ix, val := range coll { }

要注意的是,val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(如果 val 为指针,则会产生指针的拷贝,依旧可以修改集合中的原值)。一个字符串是 Unicode 编码的字符(或称之为 rune)集合,因此也可以用它迭代字符串:

for pos, char := range str {
	// ...
}

每个 rune 字符和索引在 for-range 循环中是一一对应的。它能够自动根据 UTF-8 规则识别 Unicode 编码的字符。

package main
import "fmt"

func main() {
	str := "China中国"
	fmt.Printf("The length of str is: %d\n", len(str))
	for pos, char := range str {
    	fmt.Printf("str[%d] = %c\n", pos, char)
	}
	fmt.Println("index\tint(rune)\trune\t\tchar\tbytes")
	for index, r := range str {
    	fmt.Printf("%-2d\t\t%-5d\t\t%U\t\t%-4c\t%X\n", index, r, r, r, []byte(string(r)))
	}
}

输出:

The length of str is: 11
str[0] = C
str[1] = h
str[2] = i
str[3] = n
str[4] = a
str[5] = 中
str[8] = 国
index	int(rune)	rune		char	bytes
0 		67   		U+0043		C   	43
1 		104  		U+0068		h   	68
2 		105  		U+0069		i   	69
3 		110  		U+006E		n   	6E
4 		97   		U+0061		a   	61
5 		20013		U+4E2D		中   	E4B8AD
8 		22269		U+56FD		国   	E59BBD

5.5 breakcontinue

breakcontinue 的用法同 C/C++/Java。

5.6 标签与 goto

forswitchselect 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(gofmt 会将后续代码自动移至下一行)。

标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母。

package main
import "fmt"

func main() {
LABEL1:
	for i := 0; i <= 2; i++ {
		for j := 0; j <= 3; j++ {
			if j == 2 {
				continue LABEL1
			}
			fmt.Printf("i is: %d, and j is: %d\n", i, j)
		}
	}
}

输出:

i is: 0, and j is: 0
i is: 0, and j is: 1
i is: 1, and j is: 0
i is: 1, and j is: 1
i is: 2, and j is: 0
i is: 2, and j is: 1

可以看到,当 j == 2j == 3 时没有任何输出。标签的作用对象为外部循环,因此 i 会直接变成下一个循环的值,而此时 j 的值就被重设为 0,即它的初始值。如果将 continue 改为 break,则不会只退出内层循环,而是直接退出外层循环了。如果将 continue 改为 goto,则会造成死循环。

定义但未使用标签会导致编译错误:label … defined and not used

使用标签和 goto 语句是不被鼓励的。如果必须使用 goto,应当只使用正序的标签(标签位于 goto 语句之后),但注意标签和 goto 语句之间不能出现定义新变量的语句,否则会导致编译失败。

第6章:函数(function)

6.1 介绍

Go 是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。

Go 里面有三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者 lambda 函数
  • 方法(Methods)

除了 main()init() 函数外,其它所有类型的函数都可以有参数与返回值(返回值可以有多个)。函数参数、返回值以及它们的类型被统称为函数签名。

函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如:

假设 f1 需要 3 个参数 f1(a, b, c int),同时 f2 返回 3 个参数 f2(a, b int) (int, int, int),就可以这样调用 f1f1(f2(a, b))

在 Go 里面函数重载是不被允许的,这将导致一个编译错误:

funcName redeclared in this book, previous declaration at lineno

Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。

6.2 函数参数与返回值

通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 returnpanic 结尾。

6.2.1 按值传递与按引用传递

事实上,Go 所有函数参数均按值传递。如果传入的是指针(参数的地址,在变量名前面添加 & 符号),函数可以通过这个指针的值来修改这个值所指向的地址上的值。

在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。

如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。

6.2.2 命名的返回值

示例:

package main

import "fmt"

var num int = 10
var numx2, numx3 int

func main() {
    numx2, numx3 = getX2AndX3(num)
    printValues()
    numx2, numx3 = getX2AndX3_2(num)
    printValues()
}

func printValues() {
    fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}

func getX2AndX3(input int) (int, int) {
    return 2 * input, 3 * input
}

func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    // return x2, x3
    return
}

输出:

num = 10, 2x num = 20, 3x num = 30
num = 10, 2x num = 20, 3x num = 30

getX2AndX3 使用了非命名返回值,多个非命名返回值需要使用 () 括起来,比如 (int, int)

getX2AndX3_2 使用了命名返回值,命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,只需要一条简单的不带参数的 return 语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来。

即使函数使用了命名返回值,依旧可以无视它而返回明确的值。

注意:

  • returnreturn value 都是可以的。
  • 不过 return vari = expression(表达式) 会引发一个编译错误:syntax error: unexpected =, expecting semicolon or newline or }

尽量使用命名返回值,会使代码更清晰、更简短,同时更加容易读懂。

6.2.3 空白符

空白符 _ 用来匹配一些不需要的值,然后丢弃掉。

6.2.4 改变外部变量

传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。

6.3 传递变长参数

如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。

func myFunc(a, b, arg ...int) {}

示例函数和调用:

func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")

Greeting 函数中,变量 who 的值为 []string{"Joe", "Anna", "Eileen"}

如果参数被存储在一个 slice 类型的变量 slice 中,则可以通过 slice... 的形式来传递参数,调用变参函数。

package main

import "fmt"

func main() {
	slice := []int{7, 9, 3, 5, 1}
	x := min(slice...)
	fmt.Printf("The minimum in the slice is: %d", x)
}

func min(s ...int) int {
	if len(s)==0 {
		return 0
	}
	min := s[0]
	for _, v := range s {
		if v < min {
			min = v
		}
	}
	return min
}

输出:

The minimum in the slice is: 1

一个接受变长参数的函数可以将这个参数作为其它函数的参数进行传递:

func F1(s ...string) {
	F2(s...)
	F3(s)
}

func F2(s ...string) { }
func F3(s []string) { }

如果变长参数的类型并不是都相同的,有 2 种方案可以解决这个问题:

  1. 使用结构

    定义一个结构类型,假设它叫 Options,用以存储所有可能的参数:

    type Options struct {
    	par1 type1,
    	par2 type2,
    	...
    }
    

    函数 F1 可以使用正常的参数 ab,以及一个没有任何初始化的 Options 结构: F1(a, b, Options {})。如果需要对选项进行初始化,则可以使用 F1(a, b, Options {par1:val1, par2:val2})

  2. 使用空接口

    如果一个变长参数的类型没有被指定,则可以使用默认的空接口 interface{},这样就可以接受任何类型的参数。该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断:

    func typecheck(..,..,values … interface{}) {
    	for _, value := range values {
    		switch v := value.(type) {
    			case int:case float32:case string:case bool:default:}
    	}
    }
    

6.4 defer 和追踪

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数。这是因为执行 return 之后,函数真正返回还需要执行更多的操作,比如存放函数相关的栈帧出栈,修改 PC 的值等。

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。

package main

import "fmt"

func main() {
	function1()
}

func function1() {
	fmt.Printf("In function1 at the top\n")
	defer function2()
	fmt.Printf("In function1 at the bottom!\n")
}

func function2() {
	fmt.Printf("Function2: Deferred until the end of the calling function!\n")
}

输出:

In function1 at the top
In function1 at the bottom!
Function2: Deferred until the end of the calling function!

使用 defer 的语句同样可以接受参数,例如:

package main

import "fmt"

func main() {
	testDefer()	// 输出:0
}

func testDefer() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):

package main

import "fmt"

func main() {
	f()	// 输出:5 4 3 2 1
}

func f() {
	for i := 1; i <= 5; i++ {
		defer fmt.Printf("%d ", i)
	}
}

关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如:

  1. 关闭文件流
  2. 解锁一个加锁的资源
  3. 关闭数据库链接

6.5 内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:lencapappend,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

名称说明
close用于管道通信
len、caplen 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map
new、makenewmake 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)make(type)new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作。new() 是一个函数,不要忘记它的括号。
copy、append用于复制和连接切片
panic、recover两者均用于错误处理机制
print、println底层打印函数,在部署环境中建议使用 fmt
complex、real imag用于创建和操作复数

6.6 递归函数

在使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的程序栈内存分配耗尽。

6.7 将函数作为参数

函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

package main

import "fmt"

func main() {
	callback(1, add)	// 输出:The sum of 1 and 2 is: 3
}

func add(a, b int) {
	fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}

func callback(y int, f func(int, int)) {
	f(y, 2) // this becomes add(1, 2)
}

6.8 闭包

当不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }

这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fplus(3,4)

当然,也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)

参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。

defer 语句和匿名函数

关键字 defer 经常配合匿名函数使用,它可以用于改变函数的命名返回值。

请学习以下示例并思考:函数 f 返回时,变量 ret 的值是什么?

package main

import "fmt"

func f() (ret int) {
	defer func() {
		ret++
	}()
	return 1
}
func main() {
	fmt.Println(f())
}

变量 ret 的值为 2,因为 ret++ 是在执行 return 1 语句后发生的。

6.9 应用闭包:将函数作为返回值

示例1:

package main

import "fmt"

func main() {
    // store the return function of add2() to variable p2
	p2 := add2()
	fmt.Printf("Call add2 for 3 gives: %v\n", p2(3))
	// make a special adder function, a gets value 2:
	twoAdder := adder(2)
	fmt.Printf("The result is: %v\n", twoAdder(3))
}

func add2() func(b int) int {
	return func(b int) int {
		return b + 2
	}
}

func adder(a int) func(b int) int {
	return func(b int) int {
		return a + b
	}
}

输出:

Call add2 for 3 gives: 5
The result is: 5

程序中函数 Add2Adder 均会返回签名为 func(b int) int 的函数。p2 := add2() 表示将函数 add2() 的返回的函数存储到变量 p2 中。

示例2:

package main

import "fmt"

func main() {
	var f = adder()
	fmt.Print(f(1), " - ")
	fmt.Print(f(20), " - ")
	fmt.Print(f(300))
}

func adder() func(int) int {
	var x int
	return func(delta int) int {
		x += delta
		return x
	}
}

输出:

1 - 21 - 321

可以看到,在多次调用中,变量 x 的值是被保留的,即 0 + 1 = 1,然后 1 + 20 = 21,最后 21 + 300 = 321:闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。

一个返回值为另一个函数的函数可以被称之为工厂函数,这在需要创建一系列相似的函数的时候非常有用,书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数:

func MakeAddSuffix(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

现在,可以生成如下函数:

addBmp := MakeAddSuffix(".bmp")
addJpeg := MakeAddSuffix(".jpeg")

然后调用它们:

addBmp("file") // returns: file.bmp
addJpeg("file") // returns: file.jpeg

可以返回其它函数的函数和接受其它函数作为参数的函数均被称之为高阶函数,是函数式语言的特点。闭包在 Go 语言中非常常见,常用于 goroutine 和管道操作。

6.10 使用闭包调试

在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。可以使用 runtimelog 包中的特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候实现一个 where() 闭包函数来打印函数执行的位置:

package main

import (
	"fmt"
	"runtime"
	"log"
)

func main() {
	where := func() {
		_, file, line, _ := runtime.Caller(1)
		log.Printf("%s:%d", file, line)
	}
	where()
	a := 1
	fmt.Println("a = ", a)
	where()
	a = 2
	fmt.Println("a = ", a)
	where()
	a = 3
	fmt.Println("a = ", a)
}

输出:

2020/06/21 23:49:43 d:/GOPATH/src/hello/hello_world.go:14
2020/06/21 23:49:43 d:/GOPATH/src/hello/hello_world.go:17
2020/06/21 23:49:43 d:/GOPATH/src/hello/hello_world.go:20
a =  1
a =  2
a =  3

6.11 计算函数执行时间

代码示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	for i := 0; i < 1e7; i++ { }
	end := time.Now()
	delta := end.Sub(start)
	fmt.Printf("It costs %s.\n", delta)	// 输出:It costs 4.9864ms.
}

第7章:数组与切片

7.1 声明和初始化

7.1.1 概念

数组类型可以是任意的原始类型,例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以 [5]int[10]int 是属于不同类型的。

如果想让数组元素类型为任意类型的话可以使用空接口作为类型。

数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。

声明的格式是:

var identifier [len]type

Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针)。因此,数组之间的赋值,实际上会进行数组内存的拷贝:

package main

import "fmt"

func main() {
	var arr1 = [5]int{1, 2, 3, 4, 5}
	arr2 := arr1
	fmt.Printf("arr1 == arr2: %v\n", arr1 == arr2)
	for i := 0; i < len(arr2); i++ {
		arr2[i] = 2 * arr2[i]
	}
	fmt.Print("arr1: ")
	for _, value := range arr1 {
		fmt.Print(value, " ")
	}
	fmt.Print("\narr2: ")
	for _, value := range arr2 {
		fmt.Print(value, " ")
	}
	fmt.Printf("\narr1 == arr2: %v", arr1 == arr2)
}

输出:

arr1 == arr2: true
arr1: 1 2 3 4 5 
arr2: 2 4 6 8 10 
arr1 == arr2: false  

Go 语言中数组可以通过 new() 来创建: var arr1 = new([5]int)。这种方式和 var arr2 [5]int 的区别在于:arr1 的类型是 *[5]int,而 arr2的类型是 [5]int

package main

import "fmt"

func main() {
	var arr1 = new([5]int)
	arr2 := arr1
	for i := 0; i < len(arr2); i++ {
		arr2[i] = 2 * i
	}
	fmt.Print("arr1: ")
	for _, value := range arr1 {
		fmt.Print(value, " ")
	}
	fmt.Print("\narr2: ")
	for _, value := range arr2 {
		fmt.Print(value, " ")
	}
}

输出:

arr1: 0 2 4 6 8 
arr2: 0 2 4 6 8 

程序中 arr1arr2 均为指向 int 数组的指针,因此修改 arr2 的值后,arr1 的值(初始值全为零)也变化了。

若将程序中 arr2 := arr1 改为 arr2 := *arr1,输出结果为:

arr1: 0 0 0 0 0 
arr2: 0 2 4 6 8 

7.1.2 数组常量

数组常量赋值的几种方法:

package main

import "fmt"

func main() {
	// var arrAge = [5]int{18, 20, 15, 22, 16}
	// var arrLazy = [...]int{5, 6, 7, 8, 22}
	// var arrLazy = []int{5, 6, 7, 8, 22}	// 注:初始化得到的实际上是切片slice
	var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
	// var arrKeyValue = []string{3: "Chris", 4: "Ron"}	// 注:初始化得到的实际上是切片slice
	for i := 0; i < len(arrKeyValue); i++ {
		fmt.Printf("Person at %d is %s\n", i, arrKeyValue[i])
	}
}

输出:

Person at 0 is 
Person at 1 is 
Person at 2 is 
Person at 3 is Chris
Person at 4 is Ron

7.1.3 多维数组

多维数组例如:[3][5]int[2][2][2]float64

7.1.4 将数组传递给函数

把一个大数组传递给函数会消耗很多内存(再次强调:Go 中数组是值类型)。有两种方法可以避免这种现象:

  • 传递数组的指针
  • 使用数组的切片(更常用)

7.2 切片

7.2.1 概念

切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。

切片是可索引的,并且可以由 len() 函数获取长度。

和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组

切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)

package main

import "fmt"

func main() {
	var arr [10]int
	for i := 0; i < len(arr); i++ {
		arr[i] = i
	}
	fmt.Println("arr =", arr)
	var sl []int = arr[5:8]
	fmt.Println("sl =", sl)
	fmt.Printf("len(sl) = %d, cap(sl) = %d\n", len(sl), cap(sl))
}

输出:

arr = [0 1 2 3 4 5 6 7 8 9]
sl = [5 6 7]
len(sl) = 3, cap(sl) = 5

多个切片如果表示同一个数组的片段,它们可以共享数据。因此,更改一个切片中的元素值,实际会修改底层数组的元素值,其他切片对应元素值也会被修改。

声明切片的格式是: var identifier []type(不需要说明长度)。一个切片在未初始化之前默认为 nil,长度为 0

切片的初始化格式:

  • var slice1 []type = arr1[start:end], 不包含索引 end 的元素
  • slice2 := []int{1, 2, 3}

切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。下图给出了一个长度为 2,容量为 4 的切片 y
Slice in memory

7.2.2 将切片传递给函数

func sum(a []int) int {
	s := 0
	for i := 0; i < len(a); i++ {
		s += a[i]
	}
	return s
}

func main() {
	var arr = [5]int{0, 1, 2, 3, 4}
	sum(arr[:])
}

7.2.3 用 make() 创建一个切片

使用方法:slice := make([]type, len, cap)

其中 cap 参数是可选参数,cap >= len

package main

import "fmt"

func main() {
	sl1 := make([]int, 0, 5)
	sl2 := make([]int, 5, 5)
	sl3 := make([]int, 3, 5)
	sl4 := make([]int, 0)
	sl5 := make([]int, 5)
	fmt.Println("sl1 =", sl1)
	fmt.Println("sl2 =", sl2)
	fmt.Println("sl3 =", sl3)
	fmt.Println("sl4 =", sl4)
	fmt.Println("sl5 =", sl5)
}

输出:

sl1 = []
sl2 = [0 0 0 0 0]
sl3 = [0 0 0]
sl4 = []
sl5 = [0 0 0 0 0]

Slice in memory
字符串是纯粹不可变的字节数组,它们也可以被切分成切片。

7.2.4 new()make() 的区别

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值