学习Go语言

阅读《The way to Go》

Posted by Haiming on October 15, 2019

趁着在间隙的时间,把Go入个门。虽说语言只是工具,但是学学也是比不学要好。 阅读环境在:https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md 作者的中文译本是经过官方授权的,不必担心版权问题。 话不多说。实战。

前言

本书由这样几部分组成:

  • Go语言起源和开发环境
  • Go语言核心思想,包括简单与复杂类型(第 4、7、8 章),控制结构(第 5 章),函数(第 6 章),结构与方法(第 10 章)和接口(第 11 章)。我们会对 Go 语言的函数式和面向对象编程进行透彻的讲解,包括如何使用 Go 语言来构造大型项目(第 9 章)。
  • 使用Go处理不同格式文件和错误处理机制,以及分布式的网络技巧
  • Go语言的开发模式和编码规范。

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

1.2.3 Go 语言的发展目标

Go的目标是将静态语言的安全性,高效性;和动态语言的易开发性进行有机结合。

Go是一门类型安全和内存安全的编程语言。Go有指针,但是不允许指针运算。

Go的另一个目标是对于网络通信,并发和并行编程的极佳支持。从而更好的利用大量的分布式和多核的计算机。众所周知,谷歌内部的服务器不是大型机,而是很多台普通服务器进行服务。所以这一点要求完全符合Google内部的业务需求。

Go的构建速度很快,编译和链接的速度只要几百毫秒到几秒。

Go语言的依赖关系采用的是包模型。

1.2.5 语言的特性

Go语言从本质上,在程序和结构方面支持并发编程。

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

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

Go语言使用静态类型,所以其是类型安全的一门语言。

Go是强类型语言,隐式的类型转换是不被允许的。

Go支持交叉编译,例如在Linux的计算机上面开发运行在Windows下面的程序。

1.2.6 语言的用途

Go的主要用途是用来应用于搭载Web服务器,存储集群或者类似用途的巨型中央服务器的系统编程语言。其主要支持的是高性能分布式系统。

Go语言的一个目标是实现CEP,即复杂事件处理。其要求海量的并行支持,高度的抽象化和高性能。其在IoT时代非常需要。

Go不适合用来开发对实时性要求很高的软件,原因是其垃圾回收和自动内存分配的机制有问题。

1.2.7 关于特性缺失

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

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

第2章:安装与运行环境

2.1 平台与架构

  1. Go原生编译器gc:

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

    编译器目前支持以下基于 Intel 或 AMD 处理器架构的程序构建。

    img

    表格之中的 g 和 l 的意义如下:

     g = 编译器:将源代码编译为项目代码(程序文本)
     l = 链接器:将项目代码链接到可执行的二进制文件(机器代码)
    

    下面的标记是指可以通过命令行设置可选参数,来影响编译器或者链接器的构建过程,或者得到一个特殊的目标结果。

    可用的编译器标记如下:

     flags:
     -I 针对包的目录搜索
     -d 打印声明信息
     -e 不限制错误打印的个数
     -f 打印栈结构
     -h 发生错误时进入恐慌(panic)状态
     -o 指定输出文件名 // 详见第3.4节
     -S 打印产生的汇编代码
     -V 打印编译器版本 // 详见第2.3节
     -u 禁止使用 unsafe 包中的代码
     -w 打印归类后的语法解析树
     -x 打印 lex tokens
    

    从 Go 1.0.3 版本开始,不再使用 8g,8l 之类的指令进行程序的构建,取而代之的是统一的 go buildgo install 等命令,而这些指令会自动调用相关的编译器或链接器。即这两个命令在所有情况之下通用。

  2. gccgo 编译器:

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

    从 Go 1 版本开始,gc 和 gccgo 在编译方面都有等价的功能。

  3. 文件扩展名与包(package):

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

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

    Go 语言的标准库(第 9.1 节)包文件在被安装后就是使用这种格式的文件。

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

2.2 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.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产生的是本地可执行代码,这些代码仍然运行在Go的runtime 之中,其类似于JVM等虚拟机,负责管理包括内存分配。垃圾回收,栈处理,goroutine,channel,。slice,map和反射(reflection)等等。

Go 的垃圾回收器是使用 标记-清除 垃圾回收器。

2.8 Go 解释器

因为 Go 具有像动态语言那样快速编译的能力,自然而然地就有人会问 Go 语言能否在 REPL(read-eval-print loop)编程环境下实现。Sebastien Binet 已经使用这种环境实现了一个 Go 解释器。

REPL 的意思是 “读取-求值-输出”循环(英语:Read-Eval-Print Loop,简称REPL。 这个机制可以对用户的输入立刻作出反应。

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

3.3 调试器

Go的调试器还不是很完善,下面几种方法可以作为部分的替代:

  1. 在合适的位置使用打印语句输出相关变量的值(print/printlnfmt.Print/fmt.Println/fmt.Printf)。
  2. fmt.Printf 中使用下面的说明符来打印有关变量的相关信息:
    • %+v 打印包括字段在内的实例的完整信息
    • %#v 打印包括字段和限定类型名称在内的实例的完整信息
    • %T 打印某个类型的完整说明
  3. 使用 panic 语句(第 13.2 节)来获取栈跟踪信息(直到 panic 时所有被调用函数的列表)。
  4. 使用关键字 defer 来跟踪代码执行过程(第 6.4 节)。3.4 构建并运行 Go 程序

3.4 构建并运行 Go 程序

在构建程序之前,会自动调用格式化工具 gofmt 并且保存格式化之后的源文件。如果一切执行顺利并且成功退出之后,会在控制台输出 Program exited with code 0

从 Go 1 版本开始,使用 Go 自带的工具来构建应用程序

  • go build 编译自身包和依赖包
  • go install 编译并安装自身包和依赖包

3.5 格式化代码

Go代码的风格是官方指定好的。

在命令行输入:

gofmt –w program.go 会格式化该源文件的代码,并且将格式化之后的代码覆盖原始内容。 如果不加入参数 -w 则只会打印格式化之后的结果,而不重写文件。

3.6 生成代码文档

go doc 工具会从 Go 程序之中提取顶级声明的首行注释和每个对象的相关注释,并且生成相关文档。其也可以作为一个提供在线文档浏览的Web服务器

一般用法

  • 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 服务器。

3.7 其它工具

Go自带的工具集现在有三个工具实现了:

  • go install 是安装Go包的工具,用于安装非标准库的包文件,将源代码编译成对象文件。
  • go fix 用于将旧的发行版迁移到新的发行版。
  • go test 是一个轻量级的单元测试框架

3.8 Go 性能说明

根据 Go 开发团队和基本的算法测试,Go 语言与 C 语言的性能差距大概在 10%~20% 之间。

Go的二进制文件体积是最大的,因为每个可执行文件都包含runtime

3.9 与其它语言进行交互

如果想要在 Go 之中使用 cgo,要在单独的一行使用 import “C”

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

但是 “C” 不是标准库的一部分,其只是 cgo 之中的一个特殊名称,用来引用 C 的命名空间。在这个命名空间之中所包含的所有 C 类型都可以使用。比如 C.uint , C.long 等。

当然,当想要使用某种类型作为 C 之中的函数的参数的时候,必须将其转换成 C 之中的类型,反之亦然。

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

上面这段代码之中,其要使用 C 之中的 random 函数,所以要从 Go 之中的int 转换成 C 之中无符号的 int。

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

Go 的内存管理机制无法管理通过C 代码分配的内存。开发人员需要通过 C.free 来释放变量的内存。

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

第二部分:语言的核心结构与技术

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

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

Go 的源文件的文件名要是用下划线 _ 来进行分隔。文件名不包含空格或其他特殊字符。

Go之中,有效的标识符的范围很广,只要是UTF-8 的字符开头,然后紧跟着0个或者多个字符,或者 Unicode 数字。但是不可以用数字开头,也不可以有运算符。

_ 叫做空白标识符,可以像其他的标识符一样用于变量的声明和赋值。但是赋给这个变量的值都会被抛弃,因此这些值不可以在后续的代码之中使用,也不可以使用这个标识符作为变量对其他变量做赋值或者计算。

Go的25个关键字:

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

Go的关键字数量这么少的原因是为了简化在编译过程之中的代码解析环节。

除了上面这些关键字,Go还有36个预定义标识符,包含了:

  • 基本类型的名称
  • 基本的内置函数
append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

程序的代码通过语句来实现结构话,每个语句不需要用分号结尾,但是如果要将多个语句写在一行,那么需要用分号进行人工区分。

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

首先是一个最基本的示例:宇宙通用惯例 hello world

package main

import "fmt"

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

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

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

标准库

Go的标准库之中包含着大量的包,比如 fmt 和 os。

如果想要构建一个程序,那么包和其之中的文件都必须以正确的顺序进行编译。包的依赖关系决定其构建顺序。

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

Go 的编译顺序很有趣,为了保证每一段代码只会被编译一次,每段代码在编译之前,其需求关系都会一捅到底,下面是例子:

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

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

导入代码原则

多个包在导入的时候,查找顺序按照不同的如下:

全局文件——相对目录——绝对目录

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

可见性规则

与Java 这种显性定义各种对象的可见性不同,Go之中使用的是开头字母大小写来区分,当以 一个大写字母开头的时候,比如Group1, 则被称为导出,类似于 Public。 如果以小写字母开头,那么对于包外是不可见的,但是可以在包内部可见并可用,像private。

因此,在导入一个外部包之后,