面向-Java-程序员的-Go-教程-全-

面向 Java 程序员的 Go 教程(全)

原文:Go for Java Programmers

协议:CC BY-NC-SA 4.0

一、简单看一下 Go 和 Java

Java 和 Go 有很多明显和细微的区别。它们可以作为语言和运行时来比较。这项调查主要集中在语言比较上。它旨在提供一个粗略的比较。更深入的比较贯穿全文。

本章中的一些文字可能会被解读为贬低 Go。这不是我们的意图。Go 是一种强大的语言,它可以轻松地与 Java 抗衡。但是 Java 确实有 Go 没有的特性,稍后会对它们进行总结。

请注意,这里的描述可能需要更深入地了解到目前为止已经介绍过的 Go 知识,以便全面理解。在本文后面的内容中,当你对 Go 更加熟悉之后,你可能会想重温这一章。

Go 语言及其相关的运行时与 Java 语言及其相关的 Java 运行时环境 (JRE)既有许多相似之处,也有许多不同之处。本章将试图在较高的层次上对它们进行总结。这些相似点和不同点将在本文后面详细讨论。

Go 和 Java 都是图灵完全 1 环境,这意味着(几乎)任何可能的程序都可以在其中任何一个环境中编写。这只是花费的相对开发努力和产生的程序大小和性能的问题。

应该注意的是,Go 语言和 Go 开发经验更接近于 C 语言,而不是 Java 语言。Go 语言的风格和语义比 Java 更像 C 语言。标准的 Go 库也更类似于 c 语言自带的库。

与 C 语言相比的一个例外是 Go 程序构建经验。在 C 中,这通常由 Make 2 (或一个变体)实用程序驱动。在 Go 中,它由 Go 构建器工具驱动。在作者看来,Go 方法更优越,更容易使用(不需要 make 文件)。

注意一些 Go 开发人员使用 make file-like 方法,特别是在复杂的项目中,这些项目不仅仅将 Go 源文件作为组件,因此还需要构建其他工件。Make 文件通常用于编写超出 Go builder 能力范围的多步骤流程。这类似于在 Java 中使用 Ant 3 或者 Maven 4

Go 是一种编译型语言(而 Java 是解释型语言)

像 C 和 C++一样,Go 程序在执行开始前就已经完全构建好了。所有源代码都被编译成目标计算机体系结构的机器语言。此外,所有代码都被编译到目标操作系统。相比之下,Java 被编译成虚拟机语言(又名字节码),并由 Java 虚拟机(JVM)解释。为了提高性能,字节码通常在运行时动态编译成机器语言。JVM 本身是为特定的操作系统和硬件架构而构建的。

一旦建立起来,Go 程序只需要一个操作系统就能运行。此外,Java 程序需要在运行之前,在计算机上存在一个 JRE(所需版本的)。许多 Java 程序可能还需要额外的第三方代码。

Go 方法通常会导致更快的程序启动和更自包含的程序,这两者都使它更适合于容器化的部署。

Go 和 Java 共享相似的程序结构

两种语言都支持包含方法和字段的数据结构的概念。在 Go 中,它们被称为结构,而在 Java 中,它们被称为。这些结构被收集到称为的分组中。在这两种语言中,包可以分层排列(即,具有嵌套包)。

Java 包只包含类型声明。Go 包可以包含基本声明,如变量、常量、函数以及派生类型声明。

两种语言都通过导入不同包中的代码来访问它们。在 Java 中,可以选择使用非限定的导入类型(String vs. java.lang.String)。在 Go 中,所有导入的名字都必须是限定的。

Go 和 Java 在代码风格上有一些差异,这些差异会影响代码的结构

  • Java 声明把类型放在第一位,而在 Go 中,类型放在最后。例如:

    Java—int x, y z;

    go-var x, y, z int

  • Java 方法只能返回一个值。Go 函数可以返回许多值。

  • Java 方法和字段必须在它们所属的类型中声明。Go 方法是在所属类型之外定义的。Go 支持独立于任何类型的函数和变量。Java 没有真正的静态共享变量;静态字段只是某个类的字段(相对于实例)。Go 支持在可执行映像中分配的真正的静态(全局)变量。

  • Go 有全闭包(可以捕获可变数据),而 Java 只支持部分闭包(只能捕获不可变数据)。这可以让 Go 中的一流功能更加强大。

  • Go 缺少用户定义的泛型类型。一些内置类型(例如切片和贴图)是通用的。Java 支持任何引用类型的泛型类型。

    注意,有一个已获批准的建议,即在将来添加泛型类型。

  • Java 只允许其他类型(类、枚举和接口)的类型扩展,而 Go 可以在任何现有类型的基础上创建新类型,包括基本类型(如整数和浮点)和其他用户定义的类型。Go 可以支持这些自定义类型的方法。

  • Go 和 Java 接口的工作方式非常不同。在 Java 中,一个类(或枚举)必须显式地实现一个接口,如果它是通过那个接口被使用(方法调用)的话。在 Go 中,任何类型都可以简单地通过实现接口的方法来实现该接口;不需要声明实现接口的意图,这只是方法存在的副作用。Java 中的许多标准(继承的)行为(如toString()方法)在 Go 中由实现公共接口的类型提供(相当于Stringer接口的String()方法)。

Go 和 Java 都是过程语言

命令式程序是那些通过随时间显式改变状态并测试该状态来工作的程序。它们直接反映了无处不在的冯·诺依曼计算机架构。过程程序是命令式程序,由过程(也就是 Go 中的函数和 Java 中的方法)组成。每种语言都提供了过程语言的以下主要功能:

  • 可以执行表达式,通常带有变量赋值。

  • 可以执行一系列(0+)语句(通常称为基本块)。

  • 通常,一条语句也可以隐式地充当一个块。

  • 可以在代码流中创建单向(if)、双向(if/else)或 n 向(if/else if/elseswitch)条件分支。

  • 可以循环语句。

  • Java 有whiledofor语句;Go 将它们全部组合成仅仅for

  • 可以定义可以从多个位置调用的可重用代码。

  • Java 有方法;Go 有函数,有些是方法。

所有的 6 程序都可以只用这些结构来编写。

Java 是一种面向对象的语言,而 Go 并不完全是面向对象的

和所有面向对象语言一样,Java 是一种基于类的语言。所有代码(方法)和数据(字段)都封装在某个类实现中。Java 类支持继承,因为它们可以扩展一个超类(从Object开始)。Go 允许组合(一个结构可以嵌入到另一个结构中),这通常可以获得继承的一些代码重用好处,但不是全部。

Java 提供了对方法和字段封装的完全控制(通过可见性:public/protected/package private/private)。Go 不提供所有这些选项。Go 结构在拥有字段和关联方法方面与类相似,但是它们不支持子类化。此外,Go 只支持等同于公共和包私有的可见性。

在 Java 中,类和接口都支持多态方法调度。在 Go 中,只有接口做多态方法调度。Go 没有抽象基类的等价物。同样,合成可以提供这个特性的一个子集。

注:尽管 Java 通常被认为是面向对象的,但它并不是面向对象编程风格的完美例子。例如,它有原始数据类型。但是这篇文章并不是要批评 Java 的设计。

Java 是一种高度函数化的语言,Go 则不然

Java,从版本 8 开始,已经很好的支持函数式编程 (FP)。FP 仅使用具有本地数据的函数进行编程;不存在全局的和可变的状态。Java 支持创建一级函数文字(称为 Lambdas )并将它们传递给其他要调用的代码。Java 还允许外部(或显式)循环(whilefor等)。)将被内部循环(在方法内部)替换。例如,Java 支持提供了这一点。

Go 也有一级函数文字,但是缺少对内部循环的类似支持;循环通常是外部的。一级函数提供类似 lambda 的函数,通常是以一种优越的方式。缺少内部循环被认为是 Go 的一个优点,因为它能产生更明显的代码。

Java FP 支持强烈依赖于泛型类型。目前 Go 缺少这些。

Java 是一种高度声明性的语言,而 Go 则不是

通过注释和流等特性的组合,Java 代码可以用声明式风格编写。这意味着代码说明了要做什么,但没有明确说明如何做。运行库将声明转换为实现预期结果的行为。Go 并不提倡对等的编程风格;必须编写代码来明确说明如何实现一个行为。因此,Go 代码更明显,但有时比典型的 Java 代码更大、更重复。

许多 Java 特性是注释驱动的

很多 Java 库(尤其是那些叫框架的),比如 Spring ,都大量使用了 Java 的注释。注释提供元数据,通常在运行时使用,以修改库提供的行为。Go 没有注释,所以缺少这个功能。因此,Go 代码通常更加明确;这被普遍认为是一种美德。Go 可以使用代码生成来获得与注释类似的结果。Go 有一种简单的注释形式,叫做标签,可以用来定制一些库行为,比如 JSON 或 XML 格式。

注释的使用可以将配置决策绑定到源代码。有时,这是一个缺点,因为决策需要延迟到运行时。在这种情况下,Go 和 Java 通常使用类似的方法(比如命令行或配置文件参数)。

Go 不支持异常

Java 有异常(实际上是抛出的异常或错误),可以被引发来报告异常情况。异常的使用在 Java 中非常普遍,经常被用来报告可预测和不可预测的失败。由于来自方法的值很少,因此返回错误。

Go 对这些角色做了更强的分离。所有失败都由函数返回值报告,调用方必须显式测试这些返回值。这样做很好,因为 Go 函数可以更容易地返回多个值,比如一个结果和一个错误。

Go 有死机,其作用类似于 Java 错误。它们被饲养的频率要低得多。与 Java 不同,紧急值不是类型的层次结构,只是开发者选择的值的包装,但通常是error类型的实例。永远不要声明函数可能引发的异常值的类型(也就是说,没有与 Java 的throws子句等价的语句)。这通常意味着代码不那么冗长。许多 Java 代码遵循这种模式,只抛出不需要声明的RuntimeException实例。

Java 和 Go 都使用托管内存(垃圾收集器)

两种语言都使用堆栈和堆来保存数据。栈主要用于函数局部变量,堆用于其他动态创建的数据。在 Java 中,所有对象都是在堆上分配的。在 Go 中,只有可以在函数的生存期之外使用的数据才被分配到堆上。在 Java 和 Go 中,堆都是垃圾回收的;堆对象由代码显式分配,但总是由垃圾收集器回收。

Java 没有指向对象的指针的概念,只有指向位于堆中的对象的引用。Go 允许访问指向任何数据值的指针(或地址)。大多数情况下,Go 的指针可以像 Java 引用一样使用。

Go 的垃圾回收实现比 Java 简单。与 Java 不同,有几个选项可以对它进行调优,它就是工作。

Go 和 Java 都支持并发,但方式不同

Java 有线程的概念,线程是由库提供的执行路径。Go 有 Goroutines (GRs)的概念,是语言本身提供的执行路径。GRs 可以被视为轻量级线程。Go 运行时可以支持使用比 JRE 所能支持的线程更多的(数千个)gr。

Java 支持语言中的同步控件。Go 有类似的库函数。Go 和 Java 都支持可以跨线程/gr 安全更新的原子值的概念。两者都支持显式锁定库。

Go 提供了通信顺序进程 (CSP)的概念,作为 GRs 在没有显式同步和锁定的情况下进行交互的主要方式。相反,gr 通过通道进行通信,这些通道实际上是与select语句相结合的管道(FIFO 队列)来查询它们。

本文后面将讨论并发方法的其他不同之处。GRs 和线程通常以不同的方式管理,在它们之间传递状态也是如此。

Go 的运行时比 JRE 简单

Go 的运行时比 JRE 提供的要小得多。虽然没有 JVM 的等价物,但是两者都有类似的组件,比如垃圾收集。Go 没有字节码解释器。

Go 有一大套标准库。Go 社区提供了更多。但是 Java 标准和社区库在功能的广度和深度上都远远超过了当前的 Go 库。尽管如此,Go 库足够丰富,可以开发许多有用的应用程序,尤其是应用服务器。

所有使用过的库(仅此而已)都嵌入到 Go 可执行文件中。可执行文件是运行程序所需的一切。Java 库在第一次使用时动态加载。这使得 Go 程序二进制文件(作为文件)通常比 Java 二进制文件(单个“主”类)大,但是当加载 JVM 和所有依赖类时,Java 的总内存占用通常更大。

随着 Java 的解释,动态创建字节码,然后执行它是可能的。这可以通过在运行时编写字节码或动态加载预先编写的字节码(即类)来完成。这带来了极大的灵活性。Go 是预构建的,不能这样做。

Go 程序的构建过程是不同的

Java 程序是在运行时构建的类的组合,通常来自多个来源(供应商)。这使得 Java 程序非常灵活,尤其是通过网络下载时,这是 Java 的一个主要用例。Go 程序是在执行之前静态构建的。启动时,所有代码都在可执行映像中。这以牺牲一些灵活性为代价提供了更大的完整性和可预测性。这使得 Go 更适合容器化部署。

Go 程序通常由“go builder”构建,该工具结合了编译器、依赖性管理器、链接器和可执行构建器工具等。它包含在标准 Go 安装中。Java 类被单独编译(通过 javac 工具,由Java 开发工具包 (JDK)提供),然后通常被组装成保存相关类的档案(JAR/WAR)。程序从这些档案中的一个或多个加载。档案的创建,尤其是包括任何依赖关系,通常是由独立于标准 JRE 的程序(例如, Maven )来完成的。

Go 和 Java 有相似的发布周期

Go 对 1.xx 版本采用了两年一次的发布周期 7 。图 1-1 对此做了最好的总结(来自 Go 网站)。

img/516433_1_En_1_Fig1_HTML.png

图 1-1

两年一次的发布周期

Go 团队支持后两个版本。

Java 为 1.xx 版本采用了类似的两年周期 8 。Java 有一个额外的概念长期支持 (LTS)版本。在提供下一个版本(无论是否是 LTS 版本)之前,将支持非 LTS 版本;至少在下一个 LTS 发布之前,LTS 版本是受支持的。LTS 经常每 18-24 个月来一次。Java 也有实验性特性的概念,这些特性已经发布,但在未来的版本中会有变化(或撤销);它们提供了未来支持的预览。Go 的这种功能较少,但是,例如,类属类型特征可以用类似的方式预览。

二、Java 有而 Go 没有的东西

Java 有一些 Go 没有的特性,反之亦然。所以,在我们看一些在 Go 中有相同功能的 Java 特性之前,让我们先简单看一下 Go 没有的特性。并不是每一个 Java 拥有而 Go 没有的特性都可以列出来,但是下面总结了一些关键的特性。

请注意,许多 Go“缺失”的特性被故意省略,以保持语言的简单和高效,而不是因为它们难以提供。这被认为是一种美德。

多重任务

Java 可以在一条语句中将多个变量赋给相同的值。例如:

int x, y, z;
x = y = z = 10;

最接近的 Go 是

var x, y, z int = 10, 10, 10

在 Go 中,分配的类型和值可以不同。

语句和运算符

Go 和 Java 操作符的优先级不同。在作者看来,Go 的优先级更少,也更自然。当有疑问时,使用括号将表达式括起来以确保正确。

一个关键的区别是,在 Go 中x++(表示:x = x + 1)和x--(表示:x = x - 1)是语句,而不是运算符。而且根本没有--x或者++x的表情。

Go 不支持三元表达式。需要使用 if/else 语句。例如,获取较大值的 Java 表达式

var z = x > y ? x : y;

在 Go 中需要像下面这样的东西:

var z = y
if x > y {
   z = x
}

相似但不相同。你也可以这样做:

var z int
if x > y { z = <some expensive int expression> }
else { z = <some other expensive int expression>}

注意前面的if/else必须在一个源代码行中输入。

Go 不支持赋值表达式,只支持赋值语句。

断言语句

Go 没有assert语句。一般来说,Go 有 panics 可以用来实现类似的功能,但是它们不能像断言那样在编译时被禁用。因此,不鼓励这样使用恐慌。

While 和 Do 语句

Java while语句被 Go for语句取代(即for的行为类似于while)。Java do语句没有直接的等价物,但是for语句可以用来代替它。

注意,Java for语句也可以用作while语句。

例如:

var x = 0; for(; x < 10;) { ... ; x++; }

与...相同

var x = 0; while(x < 10) { ... ; x++; }

Throw 语句/Throws 子句

Go 没有throw语句(或throws子句)。Go panic (...)功能的作用与投掷动作类似。

Strictfp、瞬态、易变、同步、抽象、静态

Go 没有这些 Java 修饰符的等价物。大多数是不需要的,因为在 Java 中需要它们的问题在 Go 中以不同的方式解决了。例如,通过将声明的值作为顶级(也称为包)值来实现静态值的等效。

对象和类(OOP)和内部类,Lambdas,this,super,Explicit 构造函数

Go 不像 Java 那样完全支持面向对象编程 (OOP)。因此,它不支持这些 Java 结构。Go 具有本文稍后描述的特性,可以类似于这些 OOP 特性中的大部分来使用。因此,更好的描述是,Go 是一种基于对象的语言。Go 确实允许一个人实现 OOP 的关键目标,但是以不同于严格的 OOP 语言通常会做的方式。

Go 不支持真正的(即 Java class声明)。Go 确实支持结构,它们类似于类,但没有继承。Go 确实允许嵌套结构,这有点像内部类。

Go 在类型声明中没有extendsimplements子句。按照这些条款的规定,Go 没有继承。Go 的接口类型确实有一种隐含形式的implements

Go 不支持 Java Lambdas (编译成类实例的函数签名)。相反,Go 支持可以作为参数传递的一级函数(通常是文字)。Go 不支持方法引用(作为参数传递的 lambdas 的简单名称)。

Go 支持接口的方式与 Java 不同。Go 的接口允许鸭子打字。Go 的接口不要求显式实现(Go 中不需要implements子句);任何具有与接口的所有方法相匹配的方法的类型都会隐式实现接口。总的来说,Go 的做法更加灵活。

Java 8 和更高版本允许在接口中实现(具体的)方法。Go 不会。Java 允许在接口中声明常量;Go 不会。Java 允许在接口中声明子类型。Go 不会。

考虑 OOP 的这些租户:

  1. 一个对象有一个标识(它可以与所有其他对象区分开来)。

  2. 一个对象可能(通常确实)有状态(也称为实例数据、属性或字段)。

  3. 一个对象可能(通常确实)有行为(也称为成员函数或方法)。

  4. 一个对象由一个称为类的模板来描述/定义。

  5. 类可以被安排在一个(继承)层次结构中;实例是层次结构中类的组合。

  6. 对象实例被封装;状态通常仅通过方法可见。

  7. 变量可以在类层次结构中的任何级别声明;子类的实例可以分配给这些变量(多态性)。

Java 支持(但不一定强制)所有前面的租户。Go 不会。Go 对这些租户的支持如下:

  1. struct 实例有一个地址,该地址通常可以作为其标识(但可能不总是唯一的);结构实例类似于对象实例,但并不完全相同。

  2. 结构实例可能(通常确实)有状态。

  3. 结构实例可能(通常)有行为。

  4. 像类一样,结构实例由称为结构类型的模板描述/定义。

  5. 不直接支持;结构可以嵌入提供类似组合的其他结构。

  6. 支持但通常不使用(结构字段通常是公共的)。

  7. 不支持。

从历史上看,面向对象程序设计语言源于计算机模拟 1 和改善人机交互的愿望。2OOP 语言被设想用来实现模拟对象之间的消息传递以影响行为。随着 OOP 改进的行为重用可能性(即继承)变得众所周知,它作为一种编程风格越来越受欢迎。大多数现代语言都提供了这种能力。

对许多人来说,Go 缺乏完整的 OOP 可能是它最大的缺点。但是作者希望,一旦你习惯于做习惯性的 Go 编程,你就不会像最初想的那样怀念 OOP 的特性。Go 是一种设计良好、功能丰富的语言,它支持 OOP 的目标,而不包含其他语言(如 Java)复杂的 OOP 特性。

请考虑 OOP 并不是写好程序所必须的。所有现存的 C 程序,一些大而丰富的,如操作系统 3 和网络浏览器,证明并非如此。事实上,有时候 OOP 思维会在程序上强加不适当的结构。再说一遍,Go 是一种类 C 语言。

实现高水平的重用不需要 OOP。函数可以很好地扮演这个角色,尤其是当它们是第一流的时候。

泛型类型和方法

Go 目前不支持泛型类型和任意类型上的方法。这里,泛型意味着能够持有/使用多种类型。在 Java 中,Object类型的变量是泛型的,因为它可以保存任何引用类型的值。在 Go 中,interface{}类型的变量是通用的,因为它可以保存任何类型的值。

Java 5 细化了这个概念,声明的类型(比如容器类)可以被指定为只支持特定的(而不是所有的)类型(比如字符串或数字)作为容器类型的修饰符,例如,List<String>(而不是仅仅List)类型。Go 的内置集合类型(切片、地图和通道)在这方面是通用的。

最初,Java 不支持特定类型的泛型类型。它们是在 Java 5 中引入的,主要是为了缓解该语言中存在的集合的某些可用性问题。由于向后兼容性,Java 的通用设计有一些不理想的特性/妥协。

目前,有一个关于在 Go 中添加泛型的提议得到了批准,其原因与在 Java 中添加泛型的原因大致相同。看起来 Go 将会步 Java 的后尘。

Java(和 Go)定义的泛型类型主要是为了去除重复编码的语法糖。在 Java 中,它们根本不会影响运行时代码(因为运行时类型擦除)。在 Go 中,它们可能会导致可执行文件中存在更多的二进制代码,但不会比手动模拟更多。

广泛的函数式编程能力

Go 支持一级函数,但不支持典型的广义效用函数(map、reduce、select、exclude、forEach、find 等。)最具功能性(强烈支持功能性编程范例)的语言和 Java(通过其 Lambdas 和 Streams 支持)提供。这种省略是 Go 语言设计者故意做出的决定。当包含泛型时,Go 可能会添加一些这样的实用函数。

原始价值观的拳击

Java 集合(数组除外)不能包含原始值,只能包含对象。因此,Java 为每种原始类型提供了包装器类型。为了使集合更容易使用,Java 会自动将一个原语包装(装箱)到一个包装器类型中,以将其插入到一个集合中,并在从集合中取出该值时将其解包(取消装箱)。Go 支持可以保存原语的集合,所以不需要这样的装箱。注意需要使用装箱是 Java 在内存使用方面不如 Go 有效的地方。

源注释

Go 没有注释。Go Struct 字段可以有标签,它提供了一个类似的但是更加有限的角色。

注释,以及函数流和 lambdas,使 Java(至少部分地)成为一种声明性语言。 4 Go 几乎纯粹是一种命令式语言。 5 这是通过选择。这往往会使 Go 代码变得更加明显和冗长。

Note Go 与 Java 编译时注释有类似的概念,源文件可以包含特殊的注释(称为构建约束),构建器解释这些注释以改变代码的处理方式。例如,要为其生成代码的目标操作系统可以在源文件的最开头通过这样的注释来指定:

// +build linux,386

这将导致该文件仅适用于 Linux 操作系统(OS)和基于 386 的体系结构。

有一种替代的(通常是首选的)语法;前面的注释也可以写成

//go:build linux,386

注意,一些约束条件,比如目标操作系统和/或硬件架构,可以嵌入到 Go 文件名中。例如

xxx_windows.go

将只为 Windows 操作系统构建。

多重可见性

Java 支持四种可视性:

  1. private–只有包含类型中的代码可以看到它。

  2. 默认–只有同一个包中的代码可以看到它。

  3. protected–只有同一包或该类型的子类中的代码才能看到它。

  4. public–任何代码都可以看到它。

Go 只支持默认可见性(在 Go 中通常称为 private 或 package)和公共可见性。地鼠通常将公共可见性称为“导出可见性”,将私有可见性称为“未导出可见性”

过载/覆盖的函数

在 Java 中,可以在同一个作用域中定义名称相同但签名不同(参数数量和/或类型不同)的函数。这些被称为(通过一种参数多态性的形式)重载函数。Go 不允许重载。

在 Java 中,具有相同名称和签名的函数可以在继承层次结构中被重新定义。这种重新定义的功能被称为(通过继承多态性)覆盖。因为 Go 不支持继承,所以不允许这样的覆盖。

正式列举

Java 有正式的枚举类型,它们是特殊用途的类类型,具有离散的静态实例,以便于与 sameness ( ==)操作符进行比较。Go 不会。相反,它对整数类型的常量使用了iota运算符。在 Java 中,枚举值可以基于几种类型(但整数是常见的);在 Go 中,只允许整数类型。

注意,Java 枚举是类,可以像任何其他类一样拥有字段和方法。他们也支持继承。Go 枚举没有类似的特性。

内置二进制数据自序列化

Java 提供了以二进制形式序列化(转换为字节序列,在这个用例中通常称为八位字节 6 )数据和对象的能力。Data{Input|Output}Stream和(子类)Object{Input|Output}Stream类型提供 API 来做这件事。序列化数据通常被写入文件或通过网络传输,有时存储在数据库中。序列化可以为原本短暂的对象提供一种持久性形式。序列化也是大多数远程过程调用 7 (RPC)机制的基础。

Java 支持原语值、数组和任何包含原语类型或任何标有Serializable接口的类型以及这些类型的任何集合的数据结构(类实例)的序列化。Java 甚至支持带有引用循环的结构。

Go 没有提供这种完全对象序列化的直接等价物。在 Go 中,人们通常将数据序列化为某种文本格式(比如 JSON 或 XML ),然后保存/发送该格式。使用文本通常不如二进制表示有效(需要更多的字节和时间)。这些文本形式通常不支持数据结构中的引用循环。

Go 提供社区支持,比如针对二进制数据的 Google 协议缓冲区、 8 、??。有了标准的 Go 库,人们可以创建定制的二进制格式,这有点乏味。

并发收款

Java 有许多集合实现,每一个都为不同的用例提供了细微的优化。Go 采用了一种更简单的方法,像 Python 和 JavaScript 等其他语言一样,在所有用例中使用单个集合实现,比如一个列表或地图。这在运行时可能不是最理想的,但是它更容易学习和使用。

除了标准的等价物之外,Java 还有几个并发(在多线程中使用时性能良好(低争用))类型和集合。ConcurrentHashMap大概是最通俗的例子。Go 有一些标准的等价库,比如sync.Map类型。一般来说,这种并发类型在 Go 中的使用频率较低。经常使用替代方法,如通道。

三、Go 和 Java 的深入比较

这一章深入探讨了 Go 与 Java 的早期介绍。它更详细地描述了 Java 和 Go 之间的显著差异。通过比较 Go 和 Java,人们可以更容易地吸收 Go 的特性。

Go 是(在作者看来)比 Java 简单得多的语言;可以说,Go 甚至是比 c 更简单的语言。例如, Java 语言规范目前大约有 800 页长,而 Go 语言规范目前大约有 85 1 页长。显然,Java 比 Go 有更多的语言复杂性。

Go 标准库也是如此。就提供的类型和函数的数量以及纯粹的代码行而言,它们比 Java 标准库小得多。在某些方面,Go 库功能较少,但即使如此,它们的功能一般足以编写许多有用的程序。

与 Java 社区一样,标准库中未包含的功能通常由社区成员提供。在作者看来,Java 库,尤其是社区提供的库,通常比许多相应的 Go 库更成熟。

Java 库通常也更重(做得更多),比相应的 Go 库更难学习和使用。一般来说,对于典型的 Go 用例,Go 库更“大小合适” 2 ,因此,Go 并不缺乏其适用性。考虑到标准 Java 库的大代码库大小迫使 Java 9 将它们分成可选择的模块,这样可以减少 Java 运行时的内存占用。此外,为了进一步减小运行时的大小,许多旧的库已经被弃用(有些现在已经被删除)。

Go 社区大多由 Google 和许多个人或小团队组成。它拥有更少的审查机构,如阿帕奇软件基金会、为 Java 开发关键的第三方库和框架的机构,如阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会、阿帕奇软件基金会和阿帕奇软件基金会。

Go 和 Java 5 支持相似但不同的语句和数据类型。它们总结如下。它们将在本文后面更详细地描述。

Go 和 Java 都支持布尔值和字符、整数和浮点数。在 Go 中,一个字符称为一个rune,为 4 个字节;Java 里叫char,2 个字节。两者都使用 Unicode 编码。一般来说,Go 对符文的使用比 Java 的char类型更好,因为任何字符变量都可以表示任何合法的 Unicode 字符。

Java 和 Go 都支持字符串类型,它们实际上是字符数组。在 Go 中,字符串是一种原始类型。Go 在字符串中使用的 Unicode 转换格式 7 (UTF-8)允许许多字符串,特别是对于英文文本,使用比同等的 Java 字符串更少的字节。

在每种语言中,这些类型上的运算符都是相似的。Go 还支持复杂的浮点数,而 Java 不支持。Java 支持大形式的整数和十进制浮点数。Go 支持大形式的整数和二进制浮点数。Go 和 Java 都支持同质值数组。Java 聚合类中的异构值;Go 使用结构。

Java 支持对类实例的引用。Go 使用指针,可以定位任何类型的值。

Java 和 Go 有很多相似的语句。

两者都有赋值语句。两者都增加了(操作员参与的)任务。Go 有多重赋值。

都有ifswitch等条件语句。Go 添加了select。两者都支持循环。Java 有whiledofor语句。Go 只有for

两者都有变量声明语句。Go 为局部变量添加了一个方便的声明和赋值组合。Go 提供了基于任何现有类型的通用类型声明。Java 只能声明类、接口或枚举类型。

Go 和 Java 都有异常能力。Java 可以抛出和捕获Throwable实例;Go 可以从恐慌中崛起和恢复。

在哲学上,Go 与 Java 有一些不同之处:

  • Go 往往遵循“少即是多”的哲学。

  • Java 诞生的最初动机是简化 C++的复杂性。Go 可以从那个角度来看,但是为了简化 C(以及 Java)。例如,在 Go 语言中通常只有一种方法(而 Java 通常有几种方法)来做一些事情。

  • 请注意,Java 的大部分语法都是从 C++语法派生出来的,C++语法是 C 语法的超集,所以 Java 语法也是基于 C 语法的。在较小的程度上,Java 的很多语义都是基于 C++语义的。Go 更面向 C 功能及其支持库。

  • 创建 Go 是为了适应 C 语言这样的利基市场。

  • 与 C++语言相比,Go 与 C 有更多的共同之处(C++是 C 语言的一个大型超集,Java 就是从 C 语言中派生出来的)。它旨在成为一种类似 C 语言的“系统编程”语言,但具有改进的安全性和语义,以满足现代计算机系统的需求,特别是具有改进的多核处理器易用性。Java 就是这样,但它旨在支持更广泛的用例集。

  • Go 在源语法和格式(符号、操作符、标点和空格的使用)上类似于 C(因此也类似于 Java)。因为 Java 也是基于 C 的,所以 Go 和 Java 在这方面也很相似。

  • Go 的语法更简单。

  • 例如,Go 允许分号(“;”的大部分使用)语句结束符在可以隐含时被省略(不存在)。注意,使用省略的语句终止符是惯用的,也是首选的。与 Java 相比,这可以使代码读/写得更干净、更容易。此外,Java 中圆括号((...))的许多用法在 Go 中都被取消了。在关联类型之外定义方法可以使代码更具可读性。

  • Go 与 Java 有不同的优化点/目标。

  • Java 更多的是一种应用(尤其是商业)语言。Go 更面向系统。这些优化点强烈地影响了 Go 语言的设计/本质。像所有的图灵完全语言一样,Java 和 Go 有重叠的适用性领域,在这些领域中任何一个都是合适的选择。

  • Go 通常比 Java 更具命令性和明确性。

  • Java,尤其是如果使用了 Java 8(以及更高版本)的特性,可以比 Go 更具声明性和抽象性。在某些方面,Go 更像 Java 的第一个(1.0)版本,而不是 Java 的当前定义。

  • 在 Go 中,大多数行为都是显式编码的。

  • 行为并不隐藏在 Java Streams 和 Lambdas 所支持的函数式编程特性中。这可能会使 Go 代码在风格上更加重复。错误被显式处理(比如在每次函数返回时),而不是像 Java 那样远程/系统地处理异常。

  • 除了(在功能上受到限制)struct field tags 之外,Go 没有 Java 所具有的注释概念。同样,这是为了让 Go 代码更加透明和明显。注释和任何声明性/后处理(命令性)方法一样,倾向于隐藏或推迟行为。

  • Java 注释驱动方法的一个很好的例子是 Spring MVCJAX-RS 如何在 web 应用服务器中定义 REST API 端点。通常,注释不是在编译时解释,而是在运行时由第三方框架解释。

  • 另一个例子是数据库实体 8 如何被典型地定义为对象关系映射器 9 (ORM)。在这种有限的情况下,Go 通过 struct 标签提供选项,这些标签通常用于通知这些工具。社区提供的GORM10ORM 就是一个例子。内置的 JSON 和 XML 处理器也使用标签。

  • Go 支持(源)生成器的概念。

  • 生成器就是写 Go 代码的 Go 代码。生成器可以由 Go builder 有条件地运行。发电机有许多用例。例如,可以使用生成器来机械地创建集合类型(比如为列表、堆栈、队列、映射等的每个所需 T/K 生成一个类型)。)模仿 Java 泛型类型,但通过预处理器完成。Go 社区提供了这样的选择。

  • Go 支持指针,Java 支持引用

  • 对计算机来说,指针和引用是相似的,但对人类来说,它们是不同的。引用是一个比指针更抽象的概念。指针是保存其他值的机器地址的变量。引用是保存另一个值的定位器(可能是地址或其他东西)的变量。

  • 在 Java 中,引用在使用时总是自动解引用的(除了在赋值中)。指针可以是也可以不是。使用指针,可以获得一些数据的地址,并将其保存在指针变量中,还可以将指针转换为其他类型,比如整数。这对于引用是不可能的。

  • 与 C(或 C++)不同,Java 和 Go 都限制指针/引用来处理特定类型的数据。没有什么比 c 语言的“void”指针更好的了,也没有什么比 c 语言允许的“指针算法”更好的了。因此,Go 和 Java 一样,比 c 语言更安全(不太可能因为寻址错误而失败)

四、Go 的关键方面

像在 Java 中一样,在最基本的层次上,Go source 是一个字符流,通常被视为一系列行。Go 文件是用 UTF-8 编码编写的(在 Java 中通常如此)。Go 没有像 Java 一样必须处理 Unicode 转义成原始字符的预处理程序;所有 Unicode 字符都被同等对待,转义只能出现在字符串或字符文本中,而不能出现在标识符或其他地方。

像在 Java 中一样,字符被分组到称为空格的结构中(空格、制表符、换行符等的序列)。)和令牌,Go 编译器解析这些令牌来处理 Go 代码。Go 经常使用空格作为标记分隔符。

除了新行之外,空白序列被视为单个空格字符。在 Go 中,新的一行可以隐式地生成一个分号(“;”)声明安德因此显得有些特别。当遇到行尾时,Go 词法分析器自动添加一个分号,并且在前面的标记后面允许有一个分号。一般来说,在一些大括号({...})或圆括号((...))括起来的列表中,行可以在逗号(“,”)后拆分。

虽然很方便,但这也限制了某些标记相对于彼此必须出现的位置。因此,Go 比 Java 更严格地将源语句排列成行。最重要的是引入块的左大括号(" { ")必须与任何前缀语句位于同一行。你会在这篇课文中看到很多这样的例子。

人们通常认为 Go 程序是一串标记,通常排列在一系列代码行中。标记通常是标识符,其中一些是保留字、分隔符和标点或运算符。

一个简单的 Go 源文件(基于常见的 Hello World 示例)在一个名为main的目录下的一个名为main.go的文件中:

1: package main
2: import "fmt"  // a standard Go library
3: func main() {
4:   fmt.Println ("Hello World!")
5: }

这里,我们在一个源文件中有一个完整的程序。程序入口点被指定在(1) main包中(所有 Go 程序入口点都必须如此)。与 Java 一样,( 3–5)main()函数是必需的入口点。在这里,main就像是 Java 的static方法。该方法使用导入的【??(2,4)】标准库函数来显示消息。像在 Java 中一样,Go 有字符串文字(4)。Go 中的一个区别是字符串是内置的(相对于库java.lang.String)类型。

前面编号的列表形式将不会在本文中进一步使用,因为它会破坏示例。将使用源注释来指出特殊细节。

注意,在 Go 中,与 Java 不同的是,main的命令行参数是通过库函数调用来访问的,而不是作为main函数参数;在本例中不访问它们。

对于 Go 解析器,这个文件看起来像

package main;
import "fmt";
func main() {
      fmt.Println("Hello World!");
}

词法分析器在行尾缺少分号但应该有分号的地方注入分号。这种形式是一种合法的 Go 代码,但不是惯用的。在惯用的 Go 中,以分号结尾的语句通常被省略,除非在一行中输入多个语句,这种情况非常少见。

等价的 Java 程序(可能在Main.java中)是类似的:

public class Main {
  public static main(String[] args) {  // or String... args
    System.out.println("Hello World!");
  }
}

注意,在 Java 中main()是 public,但在 Go 中不是。由于 Go 不像 Java 那样要求函数是某种类型的成员(又名方法)(如系统),所以直接使用打印函数,但不是由所属类限定,而是必须指明所属包(本例中为fmt)。在 Go 中,许多函数的行为类似于 Java 中的static函数(没有关联的实例)。

Java 不要求代码在一个包中(可以使用默认的包),但是强烈推荐一个包,并且通常会提供一个包。所以,Java 的例子通常是这样的

package com.mycompany.hello;
public class Main {
  public static main(String[] args) {
    System.out.println("Hello World!");
  }
}

如果编译器没有自动导入java.lang.*包,这将是

package com.mycompany.hello;
import java.lang.*;
public class Main {
  public static main(String[] args) {
    System.out.println("Hello World!");
  }
}

除了封闭的Main类,这看起来更像 Go 版本。

注意,在 Go 代码中,不需要封闭类(Main)来创建运行程序。这可以减少 basic 程序所需的行数,但也有一个明显的缺点。main函数必须在main包中,一个程序(或源码树)中只能有一个main包。在 Java 中,如果有一个main方法,每个类都可以作为一个不同的程序。

简单的 Go 程序示例

作为一个简单的输出命令行参数的 Go 程序的例子,考虑一下 Java 和 Go 的一组变体。这些例子可以让你更深入地理解 Java 和 Go 编码风格的异同。这些示例使用 Go iffor语句。虽然很像它们的 Java 对等物,但您可能想看看它们的描述。

在 Go 中,第一个程序参数(Args[0])是程序名(总是存在),后面是在命令中输入的任何空格分隔的参数。注意,在 java 中args[0]不是程序名(可能是“Java”或者正在运行的类名),就像在 Go 中一样,但是为了简单起见,在这些例子中我们将假设它是程序名。

注意在下面的 Go 示例中,使用了表达式<variable> := <expression>,通常称为短声明。这是一个简短的形式

var <previously undeclared variable> <type of the expression>
<variable> = <expression>

注意前面的行是两个不同的源代码行,而不是一个单独的换行行。

短声明通常可以嵌入到其他语句中,比如iffor。如果至少有一个变量是新声明的,那么可以声明和赋值多个变量。

首先在 Java 中:

package com.mycompany.args;

class Main {
  public static void main(String[] args) { // or String... args
    var index = 0;
    for (var arg : args) {
      if (index++ == 0) {
        System.out.printf("Program: %s%n", arg);
      } else {
        System.out.printf("Argument %d: %s%n", index, arg);
      }
    }
  }
}

然后在 Go 中:

package main
import "fmt"
import "os"

func main() {
      for index, arg := range os.Args {
            if index == 0 {
                  fmt.Printf("Program: %s\n", arg)
            } else {
                  fmt.Printf("Argument %d: %s\n", index, arg)
            }
      }
}

(在 Microsoft Windows 上)运行方式:...\go_build_main_go.exe 1 2 3

它产生以下输出:

Program: ...\go_build_main_go.exe
Argument 1: 1
Argument 2: 2
Argument 3: 3

注意,可执行文件的名称可以(而且经常)不同;这里它是由使用的 IDE 定义的。

这里导入了多个 Go 包。os.Args顶层变量用于获取命令行参数。

考虑这个稍微不同的替代功能(从现在开始只显示主要功能):

func main() {
      for index, arg := range os.Args {
            if index == 0 {
                  fmt.Printf("Program: %s\n", arg)
                  continue
            }
            fmt.Printf("Argument %d: %s\n", index, arg)
      }
}

这段代码被格式化为更加惯用的 Go 风格,其中很少使用else子句;相反,使用短路动作(如breakcontinuereturn)。Go 风格是尽可能地左对齐代码(或者避免深度代码块嵌套)。

首先在 Java 中,另一个实现是

public static void main(String[] args) {
  System.out.printf("Program: %s%n", args[0]);
  for (var index = 1; index < args.length; index++) {
    System.out.printf("Argument %d: %s%n", index, args[index]);
  }
}

现在在 Go 中:

func main() {
      fmt.Printf("Program: %s\n", os.Args[0])
      for index := 1; index < len(os.Args); index++ {
            fmt.Printf("Argument %d: %s\n", index, os.Args[index])
      }
}

这两种方法(对于循环类型)都是常用的;在作者看来,第一种形式更可取。

另一个替代方法,首先在 Java 中,是

public static void main(String[] args) {
  for (var index = 0; index < args.length; index++) {
   switch (index) {
      case 0:
        System.out.printf("Program: %s%n", args[index]);
        break;
      default:
        System.out.printf("Argument %d: %s%n", index, args[index]);
    }
  }
}

现在在 Go 中:

func main() {
      for index, arg := range os.Args {
            switch {
            case index == 0:
                  fmt.Printf("Program: %s\n", arg)
            default:
                  fmt.Printf("Argument %d: %s\n", index, arg)
            }
      }
}

或者

func main() {
      for index, arg := range os.Args {
            switch index {
            case 0:
                  fmt.Printf("Program: %s\n", arg)
            default:
                  fmt.Printf("Argument %d: %s\n", index, arg)
            }
      }
}

这两种形式都可以被认为是最好的形式。第二种 Go 形式更类似于 Java 代码。

请注意 Java 和 Go 代码之间的高度相似性。最大的区别在于语句语法。Go 通常使用较少的分隔符。注意,Go 的 switch 语句中不需要break

另一个例子是清单 4-1 中所示的一个完整而简单的 web 应用程序。一个等价的 1 Java 例子,尤其是只使用标准的 JSE 库(比如说一个 JAX-RS 框架),会非常大,所以不包括在内。

注意,在 Go 声明中,类型出现在声明的名称之后,而不是之前。通常,变量的类型由初始值的类型暗示,因此被省略。

package main

import (
      "net/http"
      "log"
      "math/rand"
)

var messages = []string{
      "Now is the time for all good Devops to come the aid of their servers.",
      "Alas poor Altair 8800; I knew it well!",
      "In the beginning there was ARPA Net and its domain was limited.",
      // assume many more
      "A blog a day helps keep the hacker away.",
}

func sendRandomMessage(w http.ResponseWriter, req *http.Request) {
      w.Header().Add(http.CanonicalHeaderKey("content-type"),
            "text/plain")
      w.Write([]byte(messages[rand.Intn(len(messages))]))
}

var spec = ":8080"  // means localhost:8080
func main() {
      http.HandleFunc("/message", sendRandomMessage)
      if err := http.ListenAndServe(spec, nil); err != nil {
            log.Fatalf("Failed to start server on %s: %v", spec, err)
      }
}

Listing 4-1Sample Complete but Minimal HTTP Server

这里,我们启动了一个 HTTP 服务器(通过ListenAndServe),它在每个对“/message”路径的请求上返回一个随机消息(对于任何 HTTP 方法,这不是典型的)。服务器运行(ListenAndServe不返回)直到被用户终止。服务器自动返回许多错误(如 404)和成功(如 200)状态。该网站上的浏览器可能会显示您在图 4-1 和 4-2 中看到的内容。

img/516433_1_En_4_Fig1_HTML.jpg

图 4-1

HTTP 获取随机消息服务器 1

img/516433_1_En_4_Fig2_HTML.jpg

图 4-2

HTTP 获取随机消息服务器 2

注意这个例子的简短。服务器函数的核心只需要四行代码。这种简洁性对于大多数 Java 库或框架来说是不可能的。

在所有这些例子中,代码一般都是不言自明的,而对于 Go 代码,即使对 Go 语言没有什么预先了解,也希望你能遵循它。这证明了 Go 语言及其运行时库的简单性和透明性。

Go 包

Go 代码和 Java 代码一样,被组织成。在 Go 中,包不仅仅是类型(类、接口、枚举)的集合,也是变量、常量和函数的集合。Go 包可以是空的。所有 Go 代码必须在某个包中。

Go 包类似于 Java 包:

  • Go 包也代表一个物理结构,通常是一个文件系统目录。

  • 同一个包目录中的所有 Go 源文件用相同的声明包名逻辑地组合成一个包,就像源文件被连接在一起一样。所有这样的源文件都需要在同一个目录中,这个目录通常与包同名(除了一种情况)。

  • 注意软件包目录可能包含非 Go 文件。目录必须至少存在一个.go源文件,才能被视为一个包。

  • Go 包也可以包含子包。Go 使用正斜杠(“/”)来分隔导入路径中的包名,而 Java 使用句点(“.”).Go 引用名称;Java 没有。像在 Java 中一样,每个子包都独立于其父包(即子包没有查看父包内容的特殊能力,反之亦然)。这种安排完全是为了方便。

  • 要被不在包中的代码使用,包需要通过import语句导入。没有像 Java 提供的那样,使用完全限定名(例如java.util.List)而不导入它。Go 没有等同于java.lang.*的 Java 自动导入。

  • 导入通常使导入包的所有公共成员对导入包可见。包的私有成员不能导入到其他包中。

  • Go 不支持导入包的单个标识符;包中的所有公共标识符总是被导入。这不是一个冲突问题,因为要使用这些标识符,包别名必须始终用作限定符。

通常按照导入路径中的姓氏对导入进行排序,但这是可选的。Go 格式化工具(gofmt)和一些 ide 将为您完成这项工作。因此,将重做该顺序,将rand放在最后:

import (
          "net/http"
          "math/rand"
          "log"
)

通常,Go 工具会在处理源代码时对其进行编辑。对于 Java 工具,这通常是不正确的。

Go 不允许同一导入在一个源文件中存在多次。它也不允许导入未在源文件中使用的内容。这可能很烦人。许多 ide 会为您添加任何缺失的导入,并移除未使用的导入。

包声明和任何导入必须位于每个源文件的最前面;其他成员可以以任何顺序出现在同一个包的任何文件中,但是结构声明应该出现在任何关联的(方法)函数定义之前。

在 Java 中,一个包中的类型可以分布在许多源文件中,但是每个类型必须在一个源文件中完成(对于任何类型的声明)。Java 源文件的一般结构是

  • 包装声明

  • 有进口的吗

  • 顶级类型(“类”、“接口”、“枚举”)声明,包括所有成员

Java 源文件由一个或多个带有相关注释的顶级类型定义组成。Java 只允许每个源文件有一个公共顶级类型,但是可以有任意数量的默认可见性顶级类型。大多数源代码只有一个类型声明。生成的类文件将被逻辑地组合成一个名称空间,也称为包。

Go 源文件由一个或多个带有相关注释的顶级(公共或私有)变量、常量、函数/方法或类型定义组成。在 Go 中,包的内容,包括包中定义的类型,可以分布在许多源文件中。Go 源文件的一般结构是

  • 包装声明

  • 有进口的吗

  • 顶级变量(“var”)声明

  • 顶级常量(“const”)声明

  • 顶级函数(“func”)声明

  • 顶级类型(“类型”)声明

请注意,顶级项目可以按任何顺序出现,也可以混合出现。

转到评论

Go 和 Java 一样,允许在源代码中添加注释。Go 注释很像 C 语言,因此也很像 Java 注释。

像 Java 一样,Go 有两种风格的注释:

  • 行(也称为备注)-从“//”开始,直到行尾。

  • block–以“/”开头,以“/”结尾。这种风格的评论可以而且经常跨越行。不允许嵌套块注释。

Go 没有 JavaDoc ("/**...*/)注释。相反,Go 文档工具特别注意在package语句或任何顶级声明之前的注释。由于一个包可以有许多源文件,所以通常创建一个只有文档的源文件(通常称为doc.go),它只有前缀为包注释的package语句。

Go 中的最佳实践是注释任何公共声明。例如:

// PrintAllValues writes the formatted values to STDOUT.
// The default formatting rules per value type are used.
func PrintAllValues(values... interface{}) {
      :
}

or as:
/*
PrintAllValues writes the formatted values to STDOUT.
The default formatting rules per value type are used.
*/
func PrintAllValues(values... interface{}) {
      :
}

注意左边没有星号(“*”),这在 Java 中很常见。

Go“doc”服务器从 Go source 中的这些注释创建 HTML 文档,就像 JavaDoc 工具为 Java 源代码所做的那样。这个注释文本是纯文本(而不是 Java 中的 HTML)。在这个文本中,缩进的文本被原样采用(就像 HTML 中的<pre>...</pre>)。左侧对齐的文本换行。

每个注释的第一句(或唯一一句)很特殊,因为它包含在摘要文档中。这应该足以确定注释项的目的。

请参考 Go 软件包文档,查看 Go 代码文档的一般样式和详细信息的示例。

开始构建/运行流程

Go 开发体验与所有编译(相对于解释)语言非常相似,包括 Java。它通常由以下步骤组成:

  1. 编辑源文件–使用一些编辑器。

  2. 编译源文件–使用 Go builder。

  3. 修复任何编译器错误——使用一些编辑器。

  4. 构建可运行–使用 Go 构建器。

  5. 测试变更–使用 Go builder 和/或第三方测试工具。

  6. 发布代码。

许多内部循环可能以这种顺序出现。整个序列可以重复。假设没有发生编译器错误,第 2、4 和 5 步可以通过一个命令来完成。

注意就构建而言,Java 是一种编译语言。生成的字节码通常在运行时被解释,这在这里并不重要。

有许多工具可以帮助开发人员完成每一步。最基本的方法是在步骤 1-3 中使用文本编辑器和命令行编译器。然后是第四步的程序生成器。步骤 5 可以用调试器和/或测试用例运行器来完成。

通常,这些工具被组合成一个集成开发环境(IDE)。通常,步骤 1–3 由 IDE 代码编辑器假定(即,代码按照键入的方式(交互式)进行编译,并立即显示错误)。

Go 有多种选择。用于 Go 的 IDE 包括 IntelliJ IDEA (或者等效但独立的 IntelliJ Goland IDE)和一些基于 Eclipse 的产品。 S ome 编辑器,比如微软的 Visual Studio Code (VS Code),在某种程度上也可以充当 ide。ide 很方便,因为它们通常将编辑器、编译器、格式化程序、审查工具、构建器、调试器,以及通常的部署工具组合在一起。ide 经常减少使用命令行工具的需要,这通常是有帮助的。

请注意,本文中几乎所有的代码都是使用 IDEA 开发的,而不是通过编辑器和 Go 命令行工具。不使用 IDE,只使用 Go 运行时工具和编辑器,仍然可以成功开发 Go 代码。

因为开发 Go 代码的方法多种多样,所以本书不会在这方面提供太多指导。每个工具通常都为如何设置和使用提供了很好的指导。Go 本身提供了帮助完成第 2、4 和 5 步的工具。

去游乐场

如果你还没有安装 Go 或 Go IDE,作者建议你使用Go Playground(play . golang . org),这是一个交互式网站,可以让你输入并运行大多数 Go 代码。

游乐场这样描述自己(关于按钮):“Go 游乐场是一个运行在 golang.org 服务器上的网络服务。该服务接收 Go 程序,审查、编译、链接并在沙箱中运行该程序,然后返回输出。**

*操场是 Go 对 REPL(读取、评估、打印、循环)过程的逻辑替代,是许多语言的典型,包括 Java。

通常,前面的序列只需要几秒钟。考虑到所涉及的额外网络开销,这证明了 Go 构建和程序启动过程有多快。

图 4-3 显示了刚刚运行显示的代码后的操场截图。

img/516433_1_En_4_Fig3_HTML.jpg

图 4-3

去游乐场你好世界跑

人们可以输入 Go 代码并运行它。这里,我们在底部的输出窗格(白色)中看到了运行示例程序(在黄色/阴影窗格中)的结果。操场限制;不支持某些 Go 库函数。更多信息参见关于按钮文本。

游乐场提供了几个例子/背景。例如,它可以运行 Go 测试用例,如图 4-4 所示。

img/516433_1_En_4_Fig4_HTML.jpg

图 4-4

Go 操场测试用例运行

playground 将允许您运行代码(如果复杂,通常从一些编辑器中复制/粘贴),就像来自多个源文件一样。比如看图 4-5 。

img/516433_1_En_4_Fig5_HTML.jpg

图 4-5

Go Playground 多个源文件示例

注意,这个例子展示了 Go 模块的使用(它有一个go.mod文件)。

操场提供了有限的分担工作的能力。活动代码(最大 64KiB)可以保存在谷歌托管的数据库中,并通过共享按钮共享其 URL。一旦共享,片段 URL 可以被加载到另一个浏览器供其他人查看,如图 4-6 和 4-7 所示。

img/516433_1_En_4_Fig7_HTML.jpg

图 4-7

访问共享代码示例

img/516433_1_En_4_Fig6_HTML.jpg

图 4-6

分享代码示例

Go 集成开发环境

一个 IDE 可以给出更丰富的体验,比如下面的 IntelliJ IDEA 截图。例如,它允许在不同的窗口中同时打开多个源文件。注意此示例显示了顶点计划的一个来源。一般来说,错误报告在 IDE 中更好。

这个 IDEA 截图有许多视图(选项卡),包括一个控制台和导航层次结构。它有一个内置的调试器。它与 Git 2 (可能还有其他)源代码控制系统(SCCS)直接集成。它可以做相当于许多“走……”命令等等。因此,在使用 IDE 时,经常会直接使用“go ...”需要命令。

注意图 4-8 (图 4-9 中放大)右上方的按钮条。有一个绿色箭头(向右的三角形)运行按钮来构建和运行程序。在 Run 按钮旁边有一个绿色的{de} Bug 按钮,用于在调试器中构建和启动程序。两者的行为都很像使用“go run”命令。

img/516433_1_En_4_Fig9_HTML.jpg

图 4-9

主意菜单栏的放大

img/516433_1_En_4_Fig8_HTML.jpg

图 4-8

IntelliJ IDEA Goland 视图

Visual Studio Code (VS Code)是一个可供选择的类似 IDE 的工具,你可以使用。图 4-10 显示了经典 Hello World 示例的一个变体,以及它运行时产生的输出。

img/516433_1_En_4_Fig10_HTML.jpg

图 4-10

Go 示例的 Visual Studio 代码

VS 代码使用 Go 1.16 运行时,因此(默认情况下)需要一个go.mod文件。这个程序在BAFGoPlayground目录下有一个最小的:

module hellogophers
go 1.16

类似的体验,如图 4-11 所示,可供 Java 开发者使用。所以,如果你使用 IntelliJ IDEA,或者 Eclipse,或者任何其他主流 IDE,从 Java 迁移到 Go 应该很简单。

img/516433_1_En_4_Fig11_HTML.jpg

图 4-11

Eclipse IDE Java 视图

有些 ide 会在代码输入时在源代码视图中检测并显示所有编译时错误。如果没有显示错误,代码将启动。其他 ide 只能检测可能错误的子集。只有在代码启动时,才会检测到任何其他错误。

发生这种情况是因为每个 IDE 都有自己的 Go 编译器(或者只是 Go 解析器),它与go命令使用的不同。这些不同的编译器检测错误的方式可能不同。一般来说,运行的是 Go 编译器在启动时生成的代码(不是 IDE 编译器生成的代码,如果有的话)。

深入查看 IDEA 中启动的 Go 程序,如图 4-12 所示,您可以看到 IDE 控制台如何显示 Go 环境、用于构建程序的命令(由 IDE 用 # gosetup 标记,不是实际值的一部分),以及在显示任何程序输出之前构建的程序。这是通过调试按钮启动的:

img/516433_1_En_4_Fig12_HTML.jpg

图 4-12

IntelliJ IDEA 控制台视图

GOROOT=C

:\Users\Administrator\sdk\go1.14.2 #gosetup
GOPATH=C

:\Users\Administrator\go #gosetup
C:\Users\Administrator\sdk\go1.14.2\bin\go.exe build -o C:\Users\Administrator\AppData\Local\Temp\2\___go_build_main_.exe -gcflags "all=-N -l" . #gosetup
C:\Users\Administrator\.IntelliJIdea2019.3\config\plugins\intellij-go\lib\dlv\windows\dlv.exe --listen=localhost:58399 --headless=true --api-version=2 --check-go-version=false --only-same-user=false exec C:\Users\Administrator\AppData\Local\Temp\2\___go_build_main_.exe -- -n tiny1 -u file:/Users/Administrator/Downloads/tiny1.png -time #gosetup
API server listening at: 127.0.0.1:58399
Command arguments: [-n tiny1 -u file:/Users/Administrator/Downloads/tiny1.png -time]
Go version: go1.14.2
:
Starting Server :8080...

前面的粗体行来自程序本身。它们来自这个来源:

fmt.Printf("Command arguments: %v\n", os.Args[1:])
fmt.Printf("Go version: %v\n", runtime.Version())
:
fmt.Printf("Starting Server %v...\n", spec)

运行 Go 程序

Go 编译器(名义上通过“go build”、“go run”或“go test”命令运行)创建可执行二进制文件(EXE 3 )。生成的 EXE 可以从操作系统(OS)命令行运行。与 Java 不同,Java 运行时环境 (JRE)及其 Java 虚拟机 (JVM)并不是运行程序必须具备的先决条件。

Go EXE 是独立的,只需要标准的操作系统功能;所有其他必需的库都嵌入在 EXE 中。EXE 中还嵌入了 Go 运行时 (GRT)。这个 GRT 类似于 JRE,但比它小得多。GRT 没有正式定义,但它至少包括内置的库函数、一个垃圾收集器,以及对 goroutines 的支持(就像轻量级 Java 线程)。

这意味着对于源代码来说,不存在等同于包含字节码(目标代码)的 Java .class(又名目标)文件。这也意味着没有包含这些类文件集合的 Java 档案 (JAR)文件。

*假设一个 Go 程序内置在一个 EXE 文件中(在这个例子中是 Windows 的),比如说myprog.exe,要运行它,只需做一件事($>是操作系统的命令提示符):

$>myprog arg1 ... argN

然而,假设该类被编译到当前目录下的MyProg.class中,在 Java 中需要做:

$>java -cp .;<jar directory>... MyProg arg1 ... argN

这里,启动一个 JVM ( java.exe),它必须包含在 OS 路径中,并向它提供任何所需的类目录和/或 JAR 文件的位置。像 Python 和其他解释器一样,JVM 将开发的程序(a .class)作为参数并运行它,而不是操作系统。

构建 Go 程序

一般来说,任何 Go 代码都是从源代码构建的,包括所有需要的源文件(您的代码和任何其他库)。这确保了对任何源文件的所有更改(编辑)都包含在生成的 EXE 文件中。它还允许潜在的跨源文件优化发生。

在 C/C++中,通常需要使用 Make 实用程序来确保所有依赖于其他文件中的更改的代码得到重新编译。Java 编译器有条件地(基于源文件时间戳)重新编译其他类依赖的类。Go 和 Java 都依赖包结构来找到所有引用的源文件。

虽然 Go 方式可能看起来效率较低,并且可能较慢,但 Go 编译器通常非常快,除了大型程序,人们很少注意到每次编译所有源代码所花费的时间。当编译器检测到软件包的源文件自上次编译以来没有发生变化时,它可以创建类似于 Java JAR 文件的预编译软件包档案,以缩短编译时间。

一些开发环境可能包括将一些源代码(尤其是库类型)预编译成“对象”文件的方法,以缩短构建时间,但 Go 应用程序开发人员通常不使用这种方法。这通常是由社区开发者对库进行的。“go install”命令可以做到这一点。它创建存档文件(带有一个“.”。“扩展”)包含预编译代码。通常,这些存档文件放在“pkg”目录中。

就像在 Java 和大多数编译(相对于解释)语言中一样,所有 Go 源代码都由 Go 编译器在以下阶段进行处理:

  1. 词法分析——逐字符读取源代码,并识别标记。

  2. 解析和验证——逐个标记地读取源代码,并构建抽象语法树 (AST)。

  3. 可选的(但是典型的)优化 AST 被重新安排以使结构更好(通常执行起来更快,但是生成的代码可能更少)。

  4. 代码生成–机器代码被创建并保存到目标文件中;在 Java 中,字节码(JVM 的机器码)被写入类文件。

请注意,在第一阶段中,会添加任何缺少的以分号结尾的语句。

Go builder 的行为很像第三方 Java MavenGradle 构建工具,因为它解析依赖库(针对 Go 编译器和代码链接器)并创建完整的可运行代码集(在 Java 中,通常以一个或多个 JAR/WAR 文件的形式;在 Go as EXEs 中)。Go 构建器添加了以下阶段:

  1. 外部引用链接——代码中使用的所有编译过的源代码和外部库都被解析并集成在一起。

  2. 可执行文件构建–构建特定于操作系统的可执行文件。

  3. 可选执行–可执行文件在生产或测试环境中启动。

构建的源代码可以是应用程序代码和/或任何依赖项(或库)。依赖项通常作为手动前置步骤(即使用go getgo install命令)被提取(作为源文件,或者更常见的是,作为档案)到前面的序列。Go 模块可以使依赖版本的选择更加可预测。

Go builder 可以说比 Java 编译器(javac)更完整。Java 编译器假设程序是由 JVM 在运行时实时(??)组装的,所以没有静态链接和程序构建阶段。由于这种运行时链接,Java 可能在编译时和运行时使用不同的库,这可能会有问题。这在 Go 中不可能发生。

Go 允许为许多操作系统(OS)类型构建代码。确切的集合会随着 Go 版本的变化而变化,但这里有一个样本集合:

  • [计]高级交互执行程序(Advanced Interactive Executive)

  • 机器人

  • 达尔文

  • 蜻蜓

  • 操作系统

  • 赫德

  • 插图

  • 射流研究…

  • Linux 操作系统

  • 氯化钠

  • netbsd

  • 从源代码构建

  • 计划 9

  • 操作系统

  • 窗子

  • 现场备用电系统

Go 允许为不同的硬件(HW)架构构建代码。确切的集合会随着 Go 版本的变化而变化,但这里有一个样本集合:

  • Three hundred and eighty-six

  • amd64

  • amd64p32

  • 手臂

  • 安布尔

  • arm64

  • arm64be

  • ppc64

  • ppc64le

  • 分子印迹聚合物

  • 简单的

  • mips64

  • mips64le

  • mips64p32

  • mips64p32le

  • 告别...

  • riscv

  • riscv64

  • s390

  • s390x

  • 平流层过程及其在气候中的作用

  • sparc64

  • 世界睡眠医学协会

标准 Go 安装包中可能不包含某些操作系统和/或体系结构列表成员。

字节码与真实代码

Go 方法与 Java 形成鲜明对比,在 Java 中,编译器生成与操作系统和硬件无关的字节码目标文件。JVM 负责解释字节码或将字节码转换成依赖于 OS 和/或 HW 的代码。这种转换通常由作为 JVM 一部分的 JIT (实时)或 Hotspot (使用优化)字节码编译器在运行时(而不是编译/构建时)完成。

这种差异是 Go 优于 Java 的一个原因。构建 Go 程序时,所有代码都以可运行的形式解析到它的映像中。操作系统只需要将文件读入内存,然后就可以立即开始执行。在 Java 中,代码是在内存中逐步构建的(许多较小的文件读取),而且代码需要在运行时进行 JIT 和链接。这种增量读取和 JIT 行为会显著降低程序的启动速度。但是一旦启动完成,Java 代码就可以像 Go 代码一样快速运行。此外,在 Java 中,一些需要的类文件可能不可用,导致程序突然失败。Go 不能这样。

所以,有人会问:哪个更快?Java 还是 Go?

答案是,生活中的许多事情都是如此:视情况而定!

由于上述原因,Go 程序往往会启动得更快。一旦加载,图片就不那么清晰了。

Go 是静态编译的,也就是说它也是静态优化的。所有的优化都是由 Go 编译器根据源代码本身的信息来完成的。当使用 Java JIT 编译器时,这是类似的,但是优化是在运行时完成的。但是 Java 也可以有一个 Hotspot 编译器,它使用运行时信息来进行改进的优化。随着运行时条件的变化,它甚至可以重新优化代码。因此,从长远来看,人们可以期待 Java 代码得到更好的优化,从而有可能运行得更快。

然而,程序运行时并不总是依赖于它自己的代码。很多时候,第三方服务(比如数据库系统和远程服务器)可以支配程序的执行时间。再多的优化也无法弥补这一点。但是更好地使用并发编程模式可能会。

与 C/C++等以前的语言相比,Java 最初的优势之一是它相对易于使用并且内置了对操作系统线程的支持。Go 及其 goroutines 本质上比 Java 做得更好。因此,在高度并发编程成为可能的情况下,人们应该期望 Go 在一般情况下胜过 Java。

Java 提供了对 Java 编译器(javac)的运行时访问。这允许 Java 代码创建 Java 源代码,然后编译它。因为 Java 可以在运行时加载类,这允许一种自我扩展的 Java 程序。

Go 有一些类似的支持,通过各种go包标准库子包来处理 Go 代码,但是 Go 不能在运行时可靠地扩展程序。

Go 对动态插件的支持是有限的(依赖于操作系统)和不完整的,其中动态代码是可能的。这是否会最终成为完全支持的功能有待确定。Go 代码可以动态编译和构建,然后可以启动生成的可执行文件(作为一个单独的操作系统进程)。这与 Java 方法有些相似,但是插件必须在不同的进程中运行。

Java 的javac编译器还允许在编译期间运行一些外部代码,允许修改抽象语法树 (AST),它是编译器对解析的 Java 类的内部表示。这允许编译时的注释处理。例如, Lombok 4 工具,可以自动化一些常见的 Java 编码动作,就使用了这种能力。

Go 也有类似的支持。例如,它被用于内置的 Go 格式和林挺工具,但是任何开发人员都可以利用它来构建强大的 Go 语言处理工具。

虽然 Go 通常是操作系统(OS)不可知的,但它不一定是基于 OS 类型的无偏见的。像 Java 一样,Go 被设计成在基于 Unix 的系统上运行。Go 支持 Microsoft Windows(和其他操作系统),但不是主要的操作系统类型。这种偏见表现在几个方面,比如命令行处理和文件系统访问。Go 提供对运行时操作系统类型和硬件架构的访问,因此您的代码可以根据需要进行调整。

Java 和 Go 都可以在代码运行时检测(测量/分析)代码。Java 管理扩展 (JMX)通常允许添加静态和动态测量。Go 的选项更加静态(但是可以在运行时启用/禁用)。两者都允许远程访问这些测量。有关此功能的更多详细信息,请参见 Go 文档。第三方提供这种支持。例如,普罗米修斯5(这是用 Go 写的)可以用来仪器化 Go 代码。

转到命令行工具

有点像 Java,Go 可以内置在模块中。Java 模块允许包显式地声明供其他包使用的类型,并显式地控制要导入的可见包。今天开发的大多数 Java 代码并没有显式地使用 Java 模块,但是它隐式地使用了,因为所有的 JRE 库都是模块化的。Java 模块使得像 Go 程序一样以自包含的 EXE 形式生成 Java 程序成为可能,但这种情况并不常见。

注意,从 Go 1.16 开始,使用模块(即go.mod和相关的库解析)已经成为默认(一个小的突破性改变)。为了获得先前的行为,正如本文中通常采用的(因为任何示例都是小型的和自包含的),需要通过将 GO111MODULE 环境值设置为auto来显式禁用模块。未来的 Go 版本可能会完全取消auto模式。

Go 也有一个类似的选项,开发者可以控制导入哪些包以及包的版本。像在 Java 中一样,如果不使用模块,生活会简单一点,但是当包含来自第三方的库时,它们就变得很重要。除非您正在创建这样的库供他人使用,否则您通常可以忽略自己代码中的模块。Still 模块允许您更好地控制您使用的库,并且它们使得以后将您的代码公开为库变得更加容易,所以推荐使用它们。

Go without using modules 假设所有要一起构建的源代码(即生成一个 EXE)都包含在一组源代码树中(由 GOROOT 和 GOPATH 环境值设置)。通过模块,下载的依赖项也可以在 Go builder 维护的本地缓存中找到。

Go 只允许每个 EXE 有一个入口点(main函数),所以每个程序都需要自己的源代码树(或者主包分支,如果你正在构建多个 EXE 的话)。相比之下,Java 允许每个类型(类)都有自己的入口点(main方法),所以每个类型都可以是自己的程序,独立于包结构。在 Go 中,通常会将多个可执行文件放在一个包含多个子目录的cmd(按照惯例,是src的替代)目录中,每个独立的程序通常包含一个main目录和该可执行文件的一个main包。

许多 Go 工具都采用这种结构。Go with modules 允许每个模块有选择地拥有自己独立的源代码树。这可能更容易管理(比如因为源代码在不同的源代码库中)。我们将在本书的后面讨论模块。

Go 命令中捆绑的工具

除了各种“构建”操作之外,“go”命令还有许多选项。这个命令取代了一大堆不同的 Java 构建工具。下面总结了通过“go”命令可以执行的关键操作:

  • bug–在新的 bug 报告中打开浏览器。

  • 构建–编译代码(按包)和任何依赖项,并生成可执行文件。

  • clean–删除所有生成的对象文件(通常自动完成)。

  • doc——像“Javadoc”命令一样,它创建 HTML 形式的包 API 文档。

  • env–显示 Go builder 和其他工具使用的操作系统环境值。

  • 修复–重写 Go source,用任何替换特征替换删除的特征。由于 Go now 承诺完全向后兼容,所以很少需要这个工具。

  • fmt——重写 Go 代码以符合标准(惯用的)Go 源代码格式规则。通常,IDE 会在输入/保存代码时这样做。

  • 生成–通常用于根据 Go source 中的指令(特殊注释)生成新的 Go source。可以用来替换 Java 通过泛型和注释提供的功能。它被用作“开始构建”之前的预备步骤

  • get——从(通常)公共存储库(如 GitHub)获取(下载并安装)一个依赖项(导入的东西),并构建它。

  • 帮助–显示可用操作的帮助。

  • install–与 Get 相关,Install 安装并编译通过导入引用的代码。

  • list–列出已安装的软件包。

  • mod 下载、安装和管理模块。它有几个子动作。

  • 运行–构建并运行 Go 程序(EXE)。

  • 测试–构建并运行 Go 测试。测试就像程序一样,但是在一个源文件中可以定义多个测试;Go 测试很像 Java JUnit 测试。

  • 工具–列出可以运行的工具(操作)。

  • 版本–显示生成的 EXE 的 Go 版本或其自己的版本。

  • vet——在 Go 代码中寻找可能的问题。很像 UnixLint6工具和各种 Java 代码质量检查器,这可以避免运行时的错误。Java 审查工具的例子包括check style7FindBugs8/spot bugs。通常,IDE 会在输入/保存代码时这样做。

对于最新的列表,使用“go”(无参数)命令。

经常使用“构建”、“运行”和“测试”。有关更多详细信息,请参见 Go 文档。

其他工具

还有一些之前没有列出的独立工具。下面列出了几个很有用的例子。此外,前面的许多操作可以作为独立的工具运行。通常,独立的工具具有更广泛的范围,比如一个完整的源代码树与一个单独的 Go 源代码或包。

“cgo”命令在 go 代码和外语代码(通常是 C 语言)之间创建了一个链接。它的用法很像 Java 原生接口 10 (JNI)用于从 Java 代码调用外语(一般是 C)代码。

今天,JNI 式的代码已经很少见了;大多数 Java 功能都是由纯 Java 产品实现的。CGo 代码在 Go 界更为常见,它是通向现有非 Go 产品的桥梁。在笔者看来,随着时间的推移,Go 在这方面会步 Java 后尘,CGo 代码会逐渐淡出。

“cover”命令用于通过分析在“go test -coverprofile”运行期间生成的统计数据来获得代码覆盖率报告。在 Java 中,必须使用第三方(比如 IDE)工具来获得代码覆盖率。

还有其他 Go 工具。有关更多详细信息,请参见 Go 文档。

Go 运行程序而不是类

Go 没有直接等同于 Java 虚拟机 (JVM)或 Java 运行时环境(JRE——一个 JVM 加上标准 Java 类库)。Go 有一个运行时,它提供了支持 Go 语义所需的功能。这包括用于其集合类型和 goroutines 的库。它还包括一个垃圾收集器来管理堆驻留内存分配。这个运行时比 JRE 小得多(通常只有几 MB,而不是几百 MB)。

您的代码、任何库和运行时都构建(链接)到操作系统(OS)运行的单个可执行文件中。这与 Java 程序组装和链接的即时 (JIT)方式形成了对比。Go 在构建时使用早期(静态)链接。Java 在运行时进行后期(动态)链接。

Go 方法类似于 C/C++(以及其他更古老的)语言中使用的方法。它更传统,但不太灵活(特别是,在运行时不容易向 EXE 添加新代码,而 Java 服务器(以及以前的小程序)经常这样做(即,在运行时通过网络下载代码))。

Go 方法会产生一个自包含的可执行文件(不需要安装任何其他的先决条件,比如 JRE)。这可以使部署比典型的 Java 更容易。这是 Go 在容器化(例如 Docker、Kubernetes)环境中如此流行的一个原因。其他用例也可以从这种更加独立的特性中受益。

Go 正朝着更加独立的方向发展。例如,Go 1.16 增加了将文字内容(比如 HTML、CSS 或 JavaScript 文件等文本的目录)嵌入 EXE 主体的能力,而在过去,这需要交付独立的文件。如果充分利用,一个完整的解决方案,如 web 服务器,可以作为一个单独的二进制文件交付。这种嵌入是通过在声明前添加前缀来完成的,如下所示:

//go:embed <path to file>
var text string  // string data

或者

//go:embed <path to file>
var bytes []byte  // binary data

或者

//go:embed <path to directory>
var fs embed.FS  // file system
dirEntries, err := fs.ReadDir("<path to directory>")
:

或者

//go:embed <path to file>
var fs embed.FS  // file system
bytes, err := fs.ReadFile("<path to file>")
:

<path to ...>值可以包含通配符。详见embed包装说明。

这种自包含有额外的好处,因为丢失的库或数据不可能只在运行时才被发现。这确实意味着可执行文件可以比仅作为归档文件(JAR)交付的 Java 程序大得多,并且假设已经存在可用的 JRE。即使是最小的 Go 可执行映像的大小也只有几兆字节。这可以通过在没有调试信息的情况下构建代码来减少,但不建议这样做。

因为 Go 程序是预先组装的,所以它的加载和启动速度通常比典型的 Java 程序要快(通常只需要几秒钟)。这在容器化环境和无服务器云环境中也有帮助。

Go 方法要求为每个目标操作系统构建可执行文件。在 Java 中,类文件可以跨操作系统移植(它们在运行时被 JIT(编译)成本地代码)。这就导致了著名的 Java 写一次,到处跑(wear)Go 所没有的特性。在 Java 中,依赖于操作系统和硬件架构的是 JVM,而不是程序本身;为每个支持的组合构建一个 JVM 版本。

幸运的是,Go 语言本身通常与操作系统和硬件架构无关,它的大多数库也是如此。很少有库是架构相关的。少数依赖于操作系统的标准库是为一组流行的操作系统提供的,如 Linux、iOS 和 Windows。通常依赖于操作系统的第三方库(一小部分)也是如此。因此,大多数 Go 程序可以跨许多操作系统移植,代价是要构建多次,每个操作系统一次。

转到内存管理

Go 可以在几个位置为值分配空间:

  • 代码图像11–用于顶级值

  • 调用堆栈12–用于多个函数或块局部变量

  • 13——用于动态值或可通过闭包访问的值或动态大小/长度的值

在使用动态内存分配的计算机程序中,最大的错误来源之一是不恰当的内存管理。许多故障,如内存泄漏、内存块的不正确重用、过早释放内存等。,经常会导致灾难性的程序失败。像 Java 一样,Go 通过提供自动内存管理来避免这些问题。

与 Java 一样,Go 提供了一个自动(也称为托管或垃圾收集)堆内存管理功能,该功能提供了以下关键功能:

  1. 为对象(Go 中任何数据类型的实例)分配空间

  2. 自动回收任何未引用(通常称为死的或不可访问的)对象的空间

对象被动态地分配在函数调用栈中,或者像 Java 一样,被分配在堆中。与 Java 一样,Go 提供了垃圾收集 14 (GC)堆内存分配/释放。

所有基于堆的对象都被垃圾收集。当所属函数返回或所属块退出时,所有基于堆栈的对象都被释放。对于这两种情况,都没有程序员可以使用的方法来释放它们。像在 Java 中一样,对堆对象的唯一控制是将指向不需要的对象的指针设置为nil

Java GC 实现将对每个即将被回收的对象调用finalize()方法。对于许多类型,这个函数什么也不做,但是它可以进行清理活动。Go 提供了一个类似的功能,但是它并不是通用于所有的分配。任何需要在 GC 时清理的分配对象都必须向 Go 运行时显式注册,这样它才会被清理。为此,我们使用

runtime.SetFinalizer(x *object, fx(x *object))

其中object是任意类型,它接受一个指向x的指针,并在一个 goroutine 中对其运行fxx值被自动取消注册,可以在下一次 GC 时释放。

像在 Java 中一样,堆对象通常使用new函数来分配。通过获取对象文字或变量的地址,也可以将对象放在堆上。

像 Java 一样,当 GC 运行时,让 GC 机制使代码暂停。GC 可能在任何堆分配上发生,并且发生的时间通常是不可预测的。这是使用垃圾收集的主要缺点。

Java 有几个 GC 实现的原因之一是试图将这些暂停调整到自然状态(批处理/命令行、交互式、服务器等)。)的程序。注意 Go 和 Java 一样,有一个 API,runtime.GC (),允许用户强制执行 GC,通常是在可以更好地容忍暂停的时候;这可以创造更多的可预测性。

最简单的 GC 方法被称为标记-清除,Go 实现可能(并且通常会)使用这种方法。它有两个阶段:

  1. 标记–所有对象都标记为不可访问,然后所有可从每个引用根访问的对象都标记为可访问。

    根是具有指针字段的任何顶级指针或对象(结构)以及任何活动调用堆栈中的任何类似指针和结构。完成从每个根开始的参考树遍历。

  2. sweep–释放(或回收)所有仍标记为不可访问的对象。

有关标记清除收集器的更多详细信息,请参见附录 D。

为了防止在 GC 期间这些根发生变化,所有活动的 goroutines 可能都需要暂停。这通常被称为停止世界 (STW)。所以实际上,Go 程序在这段时间内不做任何工作。Go 团队一直在努力减少 STW 暂停持续时间;现在,在现代机器上,大多数都不到一毫秒,因此是普遍可以接受的。

GC 算法的评级依据是

  • 最长停止世界时间–应该尽可能短。

  • GC 消耗的总运行时间的百分比——应该尽可能小。

  • 通常,很难同时优化这两个值。

应该注意的是,Go GC 使用的机制与几个 Java GCs 不同(随时间和运行时上下文而变化)。因为 Go 支持指针(而 Java 支持引用),所以它不容易在堆中移动对象。因此,Go 不使用 Java 中常见的清理(即压缩)收集器。Go 的方法会导致更高的堆碎片,从而降低内存的使用效率。

如前所述,Java 允许在几个 GC 实现中进行选择。Go 不会。随着 JVM 用例的发展,Java GC 选项也随着时间而发展(删除和添加收集器)(表明 JVM 似乎不能提供“一刀切”的选项)。

Go 堆上的对象通常有两个部分:

  • 标题–至少包含标记-扫描命中指示符,通常还包含数据的大小。也可以存在其他值,例如类型和/或调试/分析信息。

  • 数据——实际的数据。

因为头的存在,大多数系统对所有堆对象都有一个最小的大小,通常是 8 或 16 字节,即使数据更小,比如一个布尔值。内存通常以至少这个最小大小的块来分配。因此,为了更好地使用堆,应该避免将许多小值(比如标量值)单独放在堆上(比如作为大数组的一部分)。

在 Java 中,数据的堆栈和堆位置是显而易见的。由new操作符创建的任何东西都在堆上。其他的都在堆栈上。一般来说,这意味着所有原始标量变量都在堆栈中,所有对象都在堆中。

注意,由于装箱的原因,对于集合中的原始类型,Java 的内存效率可能比 Go 低。

在 Go 中,数据的位置并不总是显而易见的。数据可以存在于堆栈或堆中,这取决于它们是如何被引用的以及 Go 运行时是如何工作的。栈对于只在创建它们的函数的生存期内存在的函数局部变量来说是最佳的(即,没有指向它们的外部指针或者不被闭包使用)。其他数据通常需要堆。大数据值也需要堆分配。

Note Go 从堆中分配 goroutine 调用栈。每个 goroutine 都有自己的调用堆栈。这些堆栈开始时很小,然后根据需要增长。在 Java 中,线程调用栈也来自堆,但它们开始时要大得多(通常有几兆字节);相对于 goroutine 调用堆栈的数量,这严重限制了线程调用堆栈的数量。

堆栈和堆分配的混合会影响 Go 程序的性能。Go 提供了分析工具 16 来帮助确定这个比率并指导任何调整。

Go 和 Java 管理内存使用的方式,尤其是在堆中,是完全不同的。由于细节通常依赖于实现并会发生变化,所以它们没有被很好地记录。这些差异意味着类似的 Go 和具有类似数据结构的 Java 程序可以消耗显著不同的运行时内存量。这也意味着内存不足的情况可能会以不同的方式出现。JVM 有比 Go 更多的管理内存使用的选项。Go 更高的内存块碎片也会对此产生影响。

许多对象由 Go new函数分配,该函数分配空间来保存值(通常,但不总是,在堆上,就像在 Java 中一样)并将其初始化为二进制零(根据类型解释为“零”值)。new函数总是返回一个指向分配值的指针(或者在可用内存不足的情况下出现混乱)。

许多标量值(例如,数字和布尔值)和仅标量的结构被分配在堆栈上。大多数集合(比如切片和映射)都是在堆上分配的。

通常,任何地址被占用的值也必须在堆上分配。发生这种情况是因为在声明该值的块返回很久之后,该地址仍可以保存和使用。例如:

func makeInt() *int {
      var i int = 10  // a local, can be on the stack
      return &i       // now can live beyond this function; now on heap
}

或者实际上相当于:

func makeInt() *int {
      var pi = new(int) // on heap
      *pi = 10
      return pi
}

考虑这个结构示例:

type S struct {
      x, y, z int
}

然后:

func makeS() *S {
      return new(S)  // x, y, z = 0
}

或者相当于:

func makeS() *S {
      return &S{} // or &S{1,2,3} if fields initialized
}

Go 还使用make函数创建内置的结构、映射和通道类型。内置的make函数与new的不同之处在于,它们基于参数初始化(有点像 Java 中的构造函数调用)值,并且它返回值本身,而不是指向它的指针。例如,考虑一个类似切片的结构,它可能被定义为(从概念上讲,不是一个真正的 Go 切片;不合法)

type Slice[T any] struct {
      Values *[Cap]T  // actual data; can be shared
      Len, Cap int    // current and maximum lengths
}

where (say) make(new(Slice[int]), 10, 100)创建并返回这个结构和支持数组,并设置所有字段。

Go 标识符

像 Java 一样,Go 使用标识符来标记编程结构。像在 Java 中一样,Go 标识符有一套语法规则。Go 的规则就像 Java 的一样,所以在这里使用你的 Java 经验(任何问题都会被编译器报告)。具体规则见 Go 语言规范

在 Go 中,可以识别的命名结构有

  • 包——所有顶级类型、函数和值都包含在某个包中。

  • 类型——所有变量都有某种类型;所有函数都有某种类型的参数和返回值。

  • 变量–变量是有存储位置的命名值,可以随着时间的推移而改变。

  • 字段–字段是包含在结构(struct)中的变量。

  • 函数(声明的或内置的)–函数是独立的或者是结构或接口(仅原型)成员的代码块;函数可以被其他函数调用。

  • 常量–常量是不能更改的命名值;它们为编译器所知,但通常没有运行时存在。 17

  • 语句关键字——语句或者是声明,或者是嵌套的语句组,或者代表可以用 Go 语言表达的操作;大多数语句(除了赋值语句)都是用一个关键字引入的。

注意在 Go 中,像在 Java 中一样,每个变量都必须有一个声明的静态类型。这意味着该类型在编译时是已知的,不能在代码运行时改变。Go 和 Java 一样,允许一个接口类型的变量的动态(运行时)类型改变为符合(实现)该接口类型的任何类型。Go 没有可以设置为子类实例的类类型变量的等价物。

Java 有一个 Go 仅部分支持的特性,在这个特性中,变量可以被赋予实现该变量类型的任何类型(或子类型)。这通常被称为继承多态性(一个关键的面向对象编程特性)。这适用于类和接口类型。

在 Go 中,这种多态性只适用于接口类型。Go 中没有结构类型继承的概念。因此,如果一个 Go 变量有一个接口类型,那么它只能被赋予一个实现该接口所有方法的任何类型的实例。通常,这比 Java 的多态性更灵活,但却没有那么严格。

Go 范围

Java 和 Go 都是块范围的语言。标识符在声明它们的块中可见,在任何词汇嵌套的块中可见,并且基于它们的可见性,可能在其他块中可见。通常,特别是在 Go 中,封闭块是隐含的,而不是显式编码的。嵌套块可以从包含块中重新声明(从而隐藏)声明。

注意,作用域是一个编译时的概念;生存期(稍后讨论)是一个运行时概念。

块可以充当名称空间,它有时被命名为标识符的集合(通常在一个名称空间中是唯一的,但在不同的名称空间中不一定是唯一的)。虽然块是嵌套的,但是命名空间经常是重叠的。在 Go 中,像在 Java 中一样,名称空间是隐含的。在其他一些语言中(比如 C++),可以显式声明它们。

Java 支持几种标识符范围。通常,标识符在以下范围内声明:

  • 包——类型的名称空间

  • 类型(类、接口、枚举)-嵌套类型、字段或方法的名称空间

  • 方法或块——嵌套(也称为局部)变量的名称空间(方法创建一个块)

Go 支持多种作用域。通常,标识符是在某个范围内声明的:

  • package——全局变量、常量、函数或类型声明的名称空间。又名顶级

  • struct–嵌套字段或方法(与结构相关联的函数)的名称空间

  • 接口——方法原型(也称为签名)的名称空间

  • 函数或块–嵌套变量的名称空间(函数创建块)

一个关键的区别是 Go 允许全局(不像 Java 要求的那样包含在某些类型中)变量声明。Java static字段是全局变量的近似值。同样,在 Go 中,而不是在 Java 中,函数、类型或常量可以被全局声明。

更完整地说,Go 具有以下概念块范围:

  • 一个通用块,包含所有一起编译的 Go 源文件。

  • 每个包创建一个块,包含该包的所有 Go 源文件;这是顶级声明存在的地方。

  • 每个 Go 源文件充当一个包含该文件中所有 Go 源文件文本的文件块。

  • 每个 Go 结构或接口都创建自己的块。

  • 每个 if、else、for、switch 或 select 语句都是它自己的隐式块。

  • 每个 switch 或 select case 或 default 子句都是它自己的隐式块。

内置(或预声明)标识符位于由多个文件块组成的通用块中。包规范(不是声明)是在文件块级别(每个文件块都有自己的一组导入)。包块不会跨越不同的目录。顶层声明在 package 块中。任何局部变量(包括函数接收方、参数和返回名称)都在它的包含块(可以是函数体)中。局部声明从声明点开始,而不是从包含块开始。同一个标识符在同一个块中只能声明一次。

通用块中预先声明的(因此被认为是保留的,尤其是 IDEs 有些可以重新声明,但这是不明智的)标识符是

  • 类型—bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr

  • 常量或零值-false iota nil true

  • 功能-append cap close complex copy delete imag len make new panic print println real recover

Go 支持语句标签在块中自己的命名空间中。它们由breakcontinuegoto语句使用。标签在函数块(而不是嵌套函数)范围内。它们必须是唯一的,并且在该块中使用。不允许跨职能控制流转移。

像 Java 一样,Go 包是声明的名称空间。像 Java 一样,Go 包映射到某个文件系统中的目录。与 Java 不同,包名必须总是用来限定导入的声明。没有什么能等同于

import java.util.*;

在 Go 中,必须做与下面等价的事情(概念上的,Java 中不允许):

import java.util;
:
util.List l = new util.ArrayList();
or:
import java.util as u;
:
u.List l = new u.ArrayList();

Go 有一个类似 Java static字段导入的特性,其中导入的名称被合并到导入的名称空间中。例如:

import . "math"

会将math包中的所有公共名称包含在当前包中,因此它们可以被无限制地使用。不鼓励使用这个特性(这是一种不推荐使用的语言特性)。例如,导入的名称可能会发生冲突。更多细节见 Go 语言规范

在 Java 中,包目录保存类型(类、接口、枚举)源代码,通常(但不要求)每个源文件有一个顶级类型(.java)。包中可以有任意数量的类型。任何这样的类型对同一个包中的其他类型都有特殊的特权可见性(称为默认可见性)。在 Java 中,一个类型的所有方法必须在该类型的定义中(因此在同一个源文件中)。

Go 的私有可见性与 Java 的默认可见性几乎相同。Go 无法使一个类型的成员(比如一个结构)只对该类型私有。

Java 支持嵌套类型声明(例如,定义为一个类的成员并因此由该类限定(区分)的枚举)。这些嵌套类型可以是命名的(具有开发人员分配的名称),或者,如果是类或接口,可以是匿名的(具有编译器生成的名称)。

其他类型可以使用公共嵌套命名类型。这些嵌套类型被编译成单独的类文件(使用编译器构造的名称),并且对于 JVM 是不同的(就像来自不同的源文件一样)。Go 不允许这种嵌套,但是一个 Go 源文件可以定义任意数量的类型。

Go 范围与 Go 源文件

在 Go 中,包目录保存一个或多个 Go 源文件(.go)。每个文件的文本在逻辑上连接在一起(按照源文件名词汇顺序)形成包的内容。包中的声明在源文件中的排列方式有一些限制。这意味着 Go source 比 Java source 组织得更少,但是更灵活。

此外,与 Java 一样,生成的二进制代码(Java 中的.class)通常放在与源代码不同的目录中。在 Java 中,二进制文件是持久化的,并且可以被管理(比如放在一个 JAR 中)。在 Go 中,生成的二进制文件通常是临时的(可能只在内存中),一旦构建了目标 EXE,通常就会被删除。

一个要求是 Go 包中的每个源文件都包含一个package语句作为第一个语句。在 Go 中,没有默认包。该语句声明了包的名称。同一包中的文件应该在同一目录中。通常,目录名与包名相匹配;例如,main包通常位于名为“main”的目录中 Go 允许主包位于一个不同名称的目录中,如果程序是在包含主包的目录中启动的,则可能包含来自不同包的代码,但不建议这样做。

一般来说,每个 Go 程序的源代码都植根于一个目录(称为 GOPATH ),这个目录构成了任何包路径的起点。库包也可以通过 GOROOT 路径找到。GOROOT 可以是一个目录列表(很像 Java 类路径)。包可以驻留在这个根目录的某个路径中。导入本地包时,将使用该根目录的路径。

如果我们看左上角的图 4-13 ,我们会看到这组目录。 LifeServer 项目使用 GOROOT(下面是 Go SDK 目录)来访问 Go 编译器和运行时以及标准库。在这个目录下是所有的 Go 标准包(以源代码的形式,这在调试代码时有帮助)。它使用 GOPATH 来访问任何附加的库(通过“go get ...”来访问))由项目使用。

我们还可以看到如何轻松访问几个 Go 命令选项。有些,如 Go 编译,是自动启动的,因此没有列出。

img/516433_1_En_4_Fig13_HTML.jpg

图 4-13

显示了选择构建工具的 Goland

可以直接将一个包导入到一个远程存储库中(比如 GitHub )(通过先执行等价的“go get ”;IDEA 有助于实现自动化)。在这种情况下,使用存储库的 URL(减去协议前缀)。例如:

import "github.com/google/uuid"

将带有本地包引用(或别名)uuiduuid 包导入当前文件名称空间。

默认情况下,路径中的最后一个名称用作本地包引用。它可以被覆盖(例如,如果两个不同的导入以相同的名称结束,或者只是通过首选项),如下所示:

import guid "github.com/google/uuid"

通常,我们使用一个本地的,比如 Git,存储库加上一个或多个远程存储库来提供一套完整的可导入代码。

初始化 Go 变量

在 Java 中,变量在声明时被初始化。除了块局部变量之外,所有变量都有一个默认值,如果没有显式提供初始值,则使用该默认值。在第一次读取局部变量之前,必须对其进行显式初始化或赋值(否则会产生编译器错误)。以下字符串(比如一个类字段)的默认值为null:

String name;

大多数变量由作为声明的一部分提供的表达式值初始化。例如:

String name = "John Smith";
String name2 = name;

这里,变量name被设置为引用一个文字字符串值(存储在常量池中)。然后name2被设置为引用相同的字符串。记住,在 Java 中,Object类型(或子类型)的所有变量都持有引用,而不是值。

在 Java 中,类型的字段可以在单独的“初始化器”块中初始化。在一个类型中可以有任意数量的初始化块。这些块对于静态变量可以是static(当类加载时设置)或者对于实例变量可以是非静态的(当通过new创建实例时设置)。实例初始值设定项块是定义构造函数的一种替代方法。例如:

String name;
{
  name = "John Smith";
}

当不能通过简单的表达式初始化变量时,通常使用这些块。在 Java 中,一个变量或字段只能初始化一次。

Go 有类似的行为,除了所有变量(也包括局部变量)总是被初始化。如果省略了某个值,则使用“零”值,很像 Java 的默认值。例如:

var name string

这里,name字符串具有空的(不是nil)字符串值,这是一个字符串的零值。在某些方面,这种类型的初始化使 Go 比 Java 更安全。

顶级值也可以由函数初始化(与 Java 中的块不同)。特殊的无参数空函数init()用于此目的。包内部和包之间可以有任意数量的这些初始化函数。

这些函数仅用于顶级变量。这些函数应该放在它们初始化的变量声明的后面和附近。当不能通过简单的表达式初始化变量时,通常使用这些函数。这些函数在程序启动时由 Go 运行时调用(在调用main函数之前),而不是由开发人员代码调用。每个init()函数只被调用一次。例如:

var name string

func init() {
      name = "John Smith"
}

在 Go 中,init函数可以重置在声明时或在其他init函数中初始化的变量。最后一个叫的赢。这可能会引起一些意外。Go 有一种机制,可以根据代码的依赖性对程序中调用init函数的顺序进行排序。

源文件之间的init()处理排序是基于导入的包。首先初始化没有导入的源文件,然后是直接导入这些源文件中的包的文件,依此类推,直到到达主包。文件(以及init()函数)按照这些依赖关系进行排序。包中的这种排序可以部分地由包中 Go 源文件名的字母排序顺序来控制,以便对源文件的处理进行排序。

这是包导入中没有循环的原因之一(即,A 导入 B and B(直接或间接)导入 A)。Java 没有这样的限制。防止(或消除)导入循环有时是一项挑战。可能需要在包之间移动(即重新打包)代码段或定义的数据类型,以改变所需的导入列表来解决任何循环。通常,Go 编译器会提供信息来帮助定位循环导入模式。

在 Go 中,如果零值不足,实例初始化需要创建一个构造函数(即NewXxx)。

一些地鼠不喜欢使用init()函数,因为它们不能带参数,并且函数运行的时间(或者是否运行)不能被显式控制。为此,您可以选择创建自己的初始化函数,并在需要时显式调用它们。

注意一个包必须由一些代码导入,它的init()函数才能运行。因此,空白的标识符在导入中是允许的。例如,下面的import语句不导入任何符号;它只运行任何可能在包中的init()函数(以及包中包含的过渡包):

import _ "github.com/google/uuid"

像任何函数一样,init()函数中的代码会导致混乱。这就像在 Java 的初始化块中抛出一个异常。由于 point init函数在程序流中运行,这些混乱可能需要以不同于其他地方的混乱的方式来解决。有两种主要的方法来处理它们:

  1. 忽略它们,让程序在main()启动前失败。

  2. init()函数内的defered 函数中捕获它们,并恢复以允许程序继续执行main函数。

Go 标识符的寿命

一个生命周期是一个变量的值保持有效的运行时间。如果变量在范围内,它本身就存在。

Java 变量有以下基本生存期:

静态–如果与这些值相关联的类型(类、接口或枚举)被加载到 JVM 中,则这些值存在。这些值存在于堆中(在一些类型中;记住 Java 中的运行时类型是对象)。

大多数开发人员认为它们在 JVM 的生命周期中是持久的,但这并不总是正确的。类型被延迟加载(在第一次引用时),并且可以在任何时候卸载,因为它们没有剩余的实例,并且堆变得受限。Java 程序员也倾向于认为static值是唯一的,但事实并非总是如此。由不同的类装入器装入的同一个类会有不同的静态值集合。

实例–当与这些值相关联的对象(即实例字段)存在时,这些值会一直存在。这些值存在于堆中(实例内部)。由于 Java 中的对象是垃圾回收的,所以至少在存在一个对实例的引用时是这样。

method/block–只要声明局部值的块在调用堆栈上,局部值就会一直存在。

Go 变量具有相似的生存期:

顶层或包——这些值作为 Go 可执行文件的一部分分配,因此在可执行文件的生命周期内存在。

实例–当与这些值相关联的对象(即实例字段)存在时,这些值会一直存在。这些值存在于(结构内部)堆或调用堆栈中。由于 Go 中的堆对象是垃圾收集的,所以至少在存在一个对实例的引用时是这样。

method/block–只要声明局部值的块在调用堆栈上,局部值就会一直存在。

closure——当在某个闭包(函数字面量)中至少存在一个对局部值的引用时,即使分配块已经结束,块局部值也会持续。这些值通常存在于堆中。Java 没有类似的功能,但是它对只读(final)局部变量有类似的行为;它通过创建变量的副本来实现这一点。

Go 模块摘要

Go 包可以分组到模块中。当提供代码给其他开发人员使用时,模块是重要的结构。它们对于单个应用来说通常不太重要。

来自 Go 网站: 18 “一个模块是一个一起发布、版本化和分发的包的集合。”Go 模块是源代码树中的包的集合。这里没什么新鲜的。但是要成为一个模块,在源码树的根中有一个额外的文件叫做go.mod。这个文件设置了模块路径,它标识了源文件的导入根位置,以及可选的模块版本。

请注意,未来的 Go 版本可能会启用模块,即使go.mod不存在。

通常,模块路径是一个指向托管发布模块的服务器(比如 GitHub)的 URL(减去协议头、地址和端口);比如xyz.com/libraries/library/v2。这是其他代码导入模块的路径。go.mod文件还指出了任何依赖包及其模块路径和所需版本。它还指示模块构建时使用的 Go 版本(或最低要求;Go 构建器并不总是强制这样做)。

请注意,对于标准库,导入 URL 的主机名部分缺失。仅使用库包路径。包通常从 GOROOT 或 GOPATH 中解析。

注意,在 Go 版本 1.16(及更高版本)中,默认使用模块。不再需要一个go.mod文件来启用模块行为。如果需要,可以激活旧版本行为。

一般来说,Go 模块是以源代码的形式导入的(代码从使用的宿主位置复制/下载到您的本地机器上,通常是在第一次引用时自动进行)。然后用你自己的代码编译它们,就像是你自己写的一样。因此,通常没有正式的库代码构建。模块定义也可以驻留在本地(或远程)文件系统中。这在模块发布之前的开发和测试阶段是很典型的。

在 Java 中,这通常是不同的。Java 依赖项很少以源代码的形式提供。相反,提供了编译的 Java 类的 jar。这些 jar 通常由库作者预先构建,并托管在某个存储库中(比如说Maven Central19)。这些 jar 有时是按需下载的。因此,Java 代码只能以二进制形式发布,因而更具保密性。Go 代码一般比较开放。

有了模块,这就扩展了,允许开发者源代码在另一个目录中,称为模块 路径。因此,每个模块可以并且经常被放在不同的源代码树中,其中一些可能是远程的。

Go 模块可以有个语义版本,这些字段有如下含义:

<major>.<minor>.<fix>

其中字段表示

  • major–任何增加都表明与过去的突破性(不向后兼容)改变。

  • 轻微——任何增加都表明与过去相比没有重大变化(通常是增加)。

  • 修复(又名补丁)——任何变更都表示一些小的变更(比如 bug 修复)。

Go builder 可以在找到更新版本时升级相关模块。这有助于保持您的依赖关系是最新的,但它可能会导致意想不到的/不希望的变化。从 Go 1.16 开始,默认情况下这不再是自动化的;依赖模块版本的更改需要通过更新go.mod文件和显式go get(或等效)命令来显式完成。这提供了对所使用的依赖项版本以及何时/是否更新它们的更明确的控制。

随着更多地使用go.mod文件,在依赖项导入路径中显式指定版本的需求(如下所示)已经减少,但是一些包可能仍然使用这种方法。使用模块时,只导入模块路径(没有版本信息);使用的版本来自于go.mod文件。这使得 Go 行为更接近于使用 Maven 或 Gradle 进行 Java 构建的行为。go.mod文件的行为有点像 Maven POM 文件的依赖项部分。

要在使用 Go 1.16+时获得之前的行为,您需要使用以下环境选项构建代码:

GO111MODULE=auto

在 Go 1.11 中引入,在 Go 1.16 中该选项的默认设置从auto更改为on。本文中的一些示例需要将该值设置为auto才能正确构建。这代表了 Go 1.16 中一个小的(但合理的)突破性变化。

这些版本用于导入,以控制所使用的版本,如下所示:

import {<alias>} "<path>{.v<version>}"

在哪里

是导入的可选别名。

是包的本地或远程名称。

是要使用的包版本。默认情况下,它表示第一个。

<major>{.<minor>{.<fix>}}

一般形式是

例如:

import xxx "gitworld.com/xxx/somecoolpackage/v2"

导致使用第二个版本。版本上的“v”前缀是识别其为版本规范的惯例。通常,当提供一个新版本(比如 v2)时,任何(或者至少几个修订)旧版本也被保留,以允许人们选择使用哪个版本。这允许跨主要版本的增量升级,尤其是在进行了一些重大更改的情况下。如果没有提供版本指示符,通常会选择 v1 或第一个版本。

也可以通过go get获取包的任何版本,然后在本地使用它,而不需要版本限定符。通过这种方式,开发人员可以完全控制使用哪个版本以及何时进行升级。同样,如果存在一个go.mod文件来显式地声明期望的依赖版本,那么这就不那么频繁了。

每个go.mod文件以如下模块语句开始:

module <module path>

其中路径是模块代码的名称,它不一定是,但通常是,以包含go.mod文件的目录为根的目录树。

该文件通常使用如下go mod命令创建:

go mod init <module path>

该命令在每个模块中使用一次,不管该模块中有多少个包。模块路径通常包括

<source>/<name>

其中<source>通常是一个存储库(或目录)定位器。而<name>是模块名。例如:

mycompany.com/example

go.mod文件中还包含了构建该文件所用的或需要的 Go 版本。

当您从外部(比如远程)存储库导入包(比如mycompany.com/example)时,Go builder 可以解析导入并将其作为依赖项添加到go.mod文件中。在 Go 1.16 及更高版本中,默认情况下这不再是自动化的;需要通过go get进行明确的更新。可以选择导入库的任何可用版本。Go builder 可以(通常会)在本地缓存这个远程模块的内容,以提高构建性能。如果需要,可以添加可传递的依赖关系。

添加依赖项也可以手动完成,这允许用户选择依赖项的不同版本。例如,一旦使用添加的依赖项重新构建了代码,go.mod文件可能看起来像这样

module mycompany.com/example
go 1.16
require xyz.com/utils v1.1.3

通常,会列出多个依赖项。require关键字可以被分解出来,如下所示:

require (
  xyz.com/utils v1.1.3
  abc.com/common v2.2.3
  :
)

“go”命令上的版本向 Go 编译器指示代码的目标语言版本。这可能会导致编译器拒绝使用该版本之后定义的功能的代码。它还可能导致代码编译方式的细微差异。如果任何依赖库需要不同的版本,这可能不会导致错误。请访问 Go 网站了解更多详细信息。

go mod命令提供了管理(通常是升级)下载的依赖项的选项。

您可能会注意到在您的模块根目录中有一个名为go.sum的文件。该文件包含依赖校验和,由 go 工具管理。不要更改或删除它。

本书不会深入探讨模块的使用。有关更多详细信息,请参见 Go 文档。Go 还提供了一种称为“vendoring”的依赖解决方法(制作项目的第三方包的副本的行为依赖于将每个包放在项目内的一个vendor目录中)。有关更多详细信息,请参见 Go 文档。

在 Java 中,文件module-info.java中类似的模块描述如下

module com.mycompany.example {
  requires com.xyz.utils;
  requires com.abc.common;
}

Java 模块还允许开发者通过exports语句来限制 Java 模块公开的包。Go 模块没有类似的特性,但是 Go builder 有一个约定,模块根目录下的任何包都不能被使用库的代码导入。这使得代码实际上是模块的私有代码。当使用模块时,通常按照惯例,公共程序源被放在/pkg目录中(类似于/internal),而不是/src。在这种情况下,pkg的含义与其在模块之前的用法略有不同,如下所述。

Go 不需要前面的结构。例如,本文中列出的一些程序定义在如图 4-14 所示的目录结构中。

img/516433_1_En_4_Fig14_HTML.jpg

图 4-14

目录结构中定义的程序

每个.go文件都包括一个package main语句、任何需要的导入、任何特定于程序的代码和一个main()函数。因此,每个.go文件都是一个可执行程序。

在模块出现之前,大多数开发人员编写的代码都在 GOPATH 中列出的目录中,通常在/src目录下。第三方二进制代码(通常带有.a扩展名)通常安装在/pkg目录下。一些本地构建的包也将放在这里。Go 或其他第三方交付的代码通常放在 GOROOT 集中的一个目录中。典型结构是这样的:

<GOPATH>
      /src
            /main – your application and associated packages
            /xxx – some third-party packages (in source form)
            /yyy – some third-party packages (in source form)
            /zzz – some third-party packages (in source form)
      /pkg
            /ggg – some third-party packages (perhaps binary only)
      /bin – executable results

对于一个 Go 程序来说,下的目录可以看作是一个工作区。您可以更改< GOPATH >(比如通过 CHDIR 和/或 EXPORT 命令)来访问不同的工作区。

这种前模块结构在本书中使用频率最高。通常,显示的代码没有引用它所在的目录。

Go 作业和表达式

计算机最基本的功能是计算(即计算机是一种编程计算器)。在 Java 和 Go 中,计算是通过使用表达式完成的。在许多情况下,表达式的结果通过对变量的赋值存储在一些变量中(以便以后可以访问)。赋值(Go 和 Java 中的“=”操作符)记住变量中的某个值。这是命令式编程的精髓。

表达式可以很简单,比如单个文字的 20 个 值:

x = 1

或者单个变量值(一个术语):

x = y

或者价值交换:

x, y = y, x

或者通过混合文字、术语、运算符和函数调用使它们变得复杂:

c = 1 / (math.Sqrt(a * a + b * b) + base )

注意,只有当abc,base都是float64类型时,前面的表达式在 Go 中才是合法的,比如声明为

var a, b, c, base float64

在 Go 中,像在 Java 中一样,表达式有类型,只能存储在兼容类型的变量中。Go 在这方面比 Java 更严格;类型必须完全匹配(接口类型除外,在接口类型中,值可以是符合接口的任何类型)。

这包括使用数值时。在 Go 中,比如说一个int16不会自动转换成一个int32,一个int32也不会变成一个int64或者float64。任何此类转换都必须由“cast”函数显式完成。这可能不太方便,但是因为类型可以从内置类型派生,所以这是必要的。例如:

var x int16
var y int32
var z float64
z = float64(x) + float64(y)

为了方便起见,Go 会自动调整文字数值的类型(比如 1)以匹配目标(比如 a float64 (1.0)或complex128 (1.0+0.0i))。Go 可以做到这一点,因为文字值是“无类型的”任何类型都是由使用该文本的上下文指定的。数字文字基本上没有大小限制,至少可以和最大的正式数字类型一样大。

注意,Go 编译器通常使用math包中的IntFloat类型来实现数值。

像标识符一样,Go 的数字、字符串和字符文字严格遵循 Java 语法。因此,在这里使用您的 Java 经验(任何问题都会被编译器报告)。具体规则见 Go 语言规范

Go 有一个字符串文字的扩展语法。如果使用反斜杠(`)而不是引号(")字符作为分隔符,则字符串可以跨行。这些字符串被称为“原始字符串”。“字符串中的任何字符都被视为文字值,因此不需要(或识别)转义。任何回车符都从原始文本中删除。带引号的字符串被称为“解释字符串”。两个字符串都被编码成 UTF 8 字符。

注意 Java 15 提供了多行字符串,当用三重引号("""...""")分隔时允许转义。

像在 Java 中一样,Go 解释的字符串支持转义值:

八进制(\###)–转换为一个字节(#是一个八进制数字–01234567)

十六进制(\x##)–转换为字节(#是十六进制数字–0123456789 ABCDEF | ABCDEF)

Unicode ( \u####\U########)–转换为 16 位或 32 位值(#是十六进制数字)

ASCII (\a,\b,\f,\n,\r,\v,\,\ ',\ ")-就像 Java 转义一样

使用数字转义时必须小心,因为它们必须表示 UTF-8 编码的字符。

Go 有一个名为rune的字符类型,长度为 32 位(而 Java 是 16 位的char)。符文文字类似于字符串文字,但只允许一个字符。像在 Java 中一样,rune 文字用撇号(')括起来。符文文字用 32 位 Unicode 编码。

Go 中的文本格式

能够计算值并不有趣,除非结果可以呈现给用户。通常,这意味着向用户显示或格式化以打印或写入某个永久存储器。大多数操作系统有两种途径向用户显示纯文本(通过控制台):

  • 标准输出(STDOUT)–正常输出

  • 标准错误(STDERR)-错误输出

在 Java 中,这些被提供为 PrintStreams :

  • System.out

  • System.err

Java 允许使用默认格式(使用print()println()方法)或开发人员指定的格式(使用printf()方法)将值写入这些流。你也可以通过使用String.format()方法(?? 在幕后使用)格式化成一个字符串。

Go 也做类似的事情。缺省情况下,Go 支持打印到标准输出,打印到任何写入器(包括标准输出、标准错误、文件等)。),并通过 format(“fmt”)包中提供的函数转换为字符串。有关更多详细信息,请参见“去库调查”部分。

以下是一些示例:

fmt.Print(1, 2, 3)      // like System.out.print(1 + " "  + 2 + "  " + 3)
fmt.Fprintf(os.Stdout, 1, 2, 3)   // like above explicitly to standard out
fmt.Print(1)            // like above, but just 1 value
fmt.Fprintf(os.Stderr, 1)   // like above but to standard error
fmt.Println(1)          // same as fmt.Print(1); fmt.Print("\n")
fmt.Printf("%v\n", 1)   // similar to above

哪个输出(假设标准输出和错误都是到控制台的)

1 2 31 2 3111
1

格式化的表单(名称以“f”结尾)接受一个格式字符串和零个或多个要格式化的值(格式字符串中每个“%”一个值)。文件格式(名称以“F”开头)以一个io.Writer作为第一个参数。结果被写入编写器,编写器可以是一个打开的文件。字符串形式(名称以“S”开头)返回带有格式化文本的字符串。fmt.Sprintf()函数经常用于格式化值。

由于 Go 允许开发人员定制类型,所以最好为这些类型提供一个定制的字符串格式化程序(比如 Java toString()方法)。这是由fmt.Stringer接口完成的。许多 Go 库类型都这样做。

给定一个自定义类型,可以这样做:

type MyIntType int

func (mt MyIntType) String() string {  // conforms to Stringer interface
      return fmt.Sprintf("MyType %d", mt)
}

可以按如下方式使用:

var mt MyIntType = 1
formatted := fmt.Sprintf("%s", mt)  // could use "%v" too
fmt.Println(formatted)

哪些输出:MyType 1

注意使用"%s" (vs. "%v")来确保使用 Stringer 接口。

泛型(%v)说明符提供的格式根据被格式化的实际数据类型而变化。其他说明符应该与值的实际类型相匹配。表 4-1 中列出了标量数据类型的有效格式。

表 4-1

基本类型的格式选项

|

类型

|

有效格式

|
| --- | --- |
| Bool | %t |
| int types | %d |
| uint types | %d, %#x when formatted via %#v |
| float and complex types | %g |
| String | %s |
| Chan | %p |
| &above (pointer) | %p |

对于复合数据类型,使用表 4-2 中列出的规则对元素进行格式化,可能是递归的。

表 4-2

复杂类型的默认格式选项

|

类型

|

有效展示

|
| --- | --- |
| struct types | {field0 field1 ...} |
| array, slice types | [elem0 elem1 ...] |
| map types | map[key0:value0 key1:value1 ...] |
| &above (pointer) | &{}, &[], &map[] |

注在 Java 中,逗号(",")而不是空格(" ")通常用于分隔元素。此外,Java 格式经常用数据的类型名作为非基本数据的前缀。

如前所示,Go fmt (format)包有很大的效用。它是将 Go 值格式化成字符串的主要方法,通常是将它们打印出来,并将来自用户、文件或字符串的文本输入转换成值。

基本的格式化是通过Print系列函数完成的。可以对用户、文件或字符串进行打印。一般形式是

func Printf(format string, args ...interface{}) (n int, err error)

这将导致参数与格式字符串中嵌入的格式说明符(%x)一一匹配,并返回格式化计数或某些错误。通常,调用方不会检查返回的计数和错误值。这是经常违反“总是检查错误”规则的一个地方。因此,这样的输出可能会丢失。这可能只有在输出指向文件或通过网络时才有意义。参见顶点工程utility.go文件,了解可用于克服这一问题的函数。

可以输出多个值,每个值具有不同的格式规格:

fmt.Printf("Value 1: %d, value 2: %s, value 3: %q\n", 1, "2", "3")

要接受输入,使用Scan系列函数之一。一般形式是

func Scanf(format string, args ...interface{}) (n int, err error)

与 Printf 类似,它使来自输入源的文本与格式字符串匹配,并与扫描的计数一一对应地放入args值中,否则会返回一些错误。args值必须是指向要设置的正确类型变量的指针。

可以输入多个值,每个值都有不同的格式规范:

var one int
var two, three float64
fmt.Scanf("%d %e %v\n", &one, &two, &three)

格式字符串是嵌入了格式规范的任何字符串。扫描时,规范中除格式控制以外的文本必须完全匹配。在打印时,这样的文本按原样输出。与 Java 一样,任何这样的字符串都在运行时被解释,而不是被编译。这意味着故障可能在运行时发生。与 Java 相比,Go 通常对这些类型的错误更宽容(没有引起恐慌)。这些规范非常丰富,这里进行了总结。与 Java 一样,规范以百分号(" % ")开始,以区分大小写的格式代码字母结束。修饰符和宽度可以进行格式编码。一般格式是

%{[<index>]}{<modifier>}{<width>}{.<precession>}<code>

代码列于表 4-3 中。

表 4-3

fmt 包格式代码

|

密码

|

使用

|

适用类型

|
| --- | --- | --- |
| % | %字符 |   |
| v | 一般价值 | 任何(如 Java 中的%s) |
| b, t | 布尔代数学体系的 | 布尔 or(如果整数基数为 2) |
| s | 线 | 线 |
| d | 小数 | 以 10 为基数的整数 |
| f | 浮点十进制 | 数字 |
| g, G | 浮动通用 | 数字 |
| e, E | 浮动科学 | 数字 |
| o, O | 八进制的 | 基数为 8 的整数 |
| x, X | 十六进制的 | 基数为 16 的整数 |
| u, U | Unicode 转义 | 符文或字符串 |
| q | 引用和转义字符串 | 线 |
| c | 性格;角色;字母 | 古代北欧文字 |
| p | 指针 | 任何指针类型 |
| T | 价值类型 | 任何的 |

允许使用表 4-4 中列出的修饰符(因代码而异)。

表 4-4

fmt 包格式代码修饰符

|

修饰语

|

使用

|

注意

|
| --- | --- | --- |
| + | 始终添加一个标志 |   |
| - | 右侧(相对于左侧)的焊盘宽度 |   |
| # | 使用更详细的格式 | 在整数上添加基本指示符;结构上的字段名 |
| | 在正值上添加前导空格 |   |
| Zero | 用零填充左边的宽度 |   |

<width>值设置最小值宽度。<precision>值设置显示在任何小数点右侧的位数或显示的最小字符数。

如果存在,<index>是从 1 开始的自变量位置。这允许格式重用参数或重新排序参数。

戈里普斯

Java 最重要的特性之一是支持内置于语言和 via 类型中的相对简单的多线程(相对于 C 和 C++),例如标准库中提供的线程,以及语言特性,例如同步方法/块。Go 基于对 Goroutines 的使用提供了类似的特性,Goroutines 是一种轻量级的类似线程的方式来运行与通道相结合的代码(将在本文后面讨论)。

并发性问题

在我们讨论在 Go 中进行并发编程的机制之前,让我们看一下并发编程可能导致的一个问题。Java 和 Go 都使用共享内存模型(所有线程都可以访问相同的内存位置),因此临界区 (CS),即访问变量时会受到并行访问影响的代码区域,是很常见的。Java 语言有synchronized块来帮助控制对 CS 的访问。Java 允许任何对象成为这样一个 CS 上的门(又名条件)。

考虑一个例子:

public class Main {
  public static void main(String[] args) {
    int N = 10;

    var sum = new int[1];
    var rand = new Random();
    var threads = new ArrayList<Thread>();
    for (var i = 0; i < N; i++) {
      var t = new Thread(() -> {
        try {
          Thread.sleep(rand.nextInt(10));
        } catch (InterruptedException e) {
          // ...
        }
        sum[0] += 100;
      });
      threads.add(t);
      t.start();
    }
    try {
      for (var t : threads) {
        t.join();
      }
      System.out.printf("Sum result: %d%n", sum[0]);
    } catch (InterruptedException e) {
      // ...
    }
  }
}

注意sum是一个由一个int组成的数组,所以它在线程体中是可写的。这是必要的,因为 Java 没有闭包。

这里,期望的结果是sum等于N * 100。有时(可能大多数时候)会是这个值,但也可以更小。例如,当 N = 10 时:

Sum result: 900

这是因为声明

sum[0] += 100; // same as sum[0] = sum[0] + 100

是一个(隐藏的)临界区,因为+=操作不是线程间的原子操作,因此在提取sum并向其添加 100 以及设置新的sum值之间可能会发生线程切换。任何这样的读-修改-写序列如果不是自动完成的,就会创建一个 CS。

这可以通过如下更改分配来解决:

synchronized (threads) {
   sum[0] += 100;
}

这确保一次只有一个线程执行该语句。在这种情况下,可以使用除threads之外的其他值,也可能使用this。更简单的方法是使用原子值:

var sum = new AtomicInteger(0);
:
sum.addAndGet(100); // replaces synchronized block

像 Java 一样,Go 也有内存访问顺序的特性。Java 用(有点复杂)发生-之前 (HB)关系来解释这一点。 21 Go 也有 HB 关系 22 用于内存访问。必须小心,尤其是当涉及多个 goroutines 时,以确保代码尊重所有 HB 关系。可以使用 Go 通道、原子访问和锁定功能来实现这一点。

走向并行

Go 通过一个名为 Goroutines 的特性支持并发编程,这使得异步或并行处理变得相对(相对于 Java)容易。Java 中最相似的概念是前面所示的线程。Goroutines 可以引入相同的临界区问题。我们将在后面讨论如何处理 Go 中的关键部分。

注意并行并发不是一回事。并发意味着能够并行运行。这并不意味着代码总是并行运行的。并发通常意味着代码的行为是可预测的,与它是否并行运行无关。通常,这是代码设计的一个功能。

在多处理器(或内核)系统上,代码可以真正并行(同时)运行,但前提是其设计支持并发性。有时,可以通过在单个处理器上多路复用不同的代码执行线程来模拟并行行为(通常称为多任务或分时)。

注大多数现代计算机至少包含两个内核,因此并行处理是可能的。服务器级机器通常包含几十个(可能有 100 个)内核。

goroutine 只是一个普通的 Go 函数。创建一个 goroutine,并以go语句开始。go 语句立即返回,goroutine 函数与调用者异步运行,并且可能与调用者并行运行。

Goroutines 与通道(稍后讨论)相结合,提供了通信顺序流程 23 (CSP)的实现。CSP 的基本概念是独立的执行路径(在 Java 中称之为线程,在 Go 中称之为 goroutines)可以通过在它们之间以受控的方式传递数据来进行交互(通常像通道一样先进先出)。这通常比管理 CS 更容易和安全,并且是 Java 同步方法的一种替代方案。

使用 CSP,每个线程不同时共享数据(注意 Go 中没有任何东西阻止这一点,但通常不需要),而是使用一种消息传递的形式;数据由源 goroutine“发送”(传输),由目标/处理器 goroutine“接收”。这防止了临界区的可能性。通过缓冲这样的消息,发送方和接收方可以异步工作。

CSP 就像演员 24 系统。参与者系统也在参与者之间发送消息。参与者通常是具有特殊方法的对象,该方法在指定的线程上运行并接收任何消息;actor 本身通常不是线程,而是由某个 actor 运行时管理的共享线程。这为 actor 系统提供了更好的实例规模。参与者运行时负责将消息路由/传递给参与者。在 Go 中,通道承担了这个角色。

Java 社区提供了几个很好的 actor 库/框架,例如, Akka25 再来一次,Go 默认提供了这个能力;在 Java 中,它是一个附加组件。

CSP 和 Actors 都通过使消息的处理顺序化(不间断)来简化编程。处理器在准备好接收新消息之前不会收到新消息。它们还一次只允许一个线程访问任何数据。

与 Java 线程相比,goroutine 是轻量级的(使用较少的资源)。Goroutines 就像 Java 中通常所说的绿色 26 线程(这是由运行时而不是操作系统创建的类似线程的函数,通常比原生操作系统线程更轻,并且提供更快的上下文切换)。

通常,每个操作系统线程可能有许多绿色线程。goroutines 也是如此。实际上可以使用的 Java 线程的最大数量一般在几千个左右,而通常可以使用数万个(在大型系统中,达到数百万个)goroutines。

goroutines 如何实现的细节在不同的 Go 版本中会有所不同,因此在本文中不做深入的解释。值得注意的一点是,每个 goroutine 都有自己的调用堆栈(这占了 goroutine 消耗的大部分资源)。

与 Java 线程不同,Java 线程的堆栈通常只会增长,而且通常有几兆字节,而 goroutine 堆栈可以根据需要随时间增长和收缩。因此,一个 goroutine 消耗的堆栈正好是它所需要的,仅此而已。这是 goroutines 轻量级的原因之一,尤其是相对于 Java 线程而言。

另一个方面是,在 Go 操作系统中,线程是按需创建和结束的,并保存在池中以供重用,因此通常只需要支持活动的 goroutines。

考虑一下,如果所有的 goroutines 都是 CPU 受限的(也就是说,它们不做太多的 I/O),那么线程的数量只需要和处理器内核的数量一样多(其他没有采用多任务处理的线程必然是空闲的)。由于完全受 CPU 限制的代码(至少在很长一段时间内)很少,因此需要额外的线程来支持并发的 CPU 和 I/O 密集型 goroutines。

在 Go 中,goroutine 调度程序通常维护一个线程池,如图 4-15 所示。它将 goroutines 分配给池中的非活动线程。这种关联不是静态的,而是会随着时间的推移而变化。它会根据需要添加新的 I/O 线程,但通常会根据机器中处理器(核心)的实际数量来限制 CPU 线程。一般来说,goroutines 与 threads 的比率可以很大(比如> > 100)。

img/516433_1_En_4_Fig15_HTML.png

图 4-15

Goroutine 处理概述

如果 goroutine 做了一些事情来阻止它继续执行(或者主动放弃它的线程),那么它的线程就会被分离并交给另一个 goroutine。如果 goroutine 发出阻塞操作系统调用(比如执行文件或套接字 I/O 操作),Go 调度程序也可以分离线程。因此,调度器可能有两个线程池:一个用于 CPU 绑定的 goroutines,另一个用于 I/O 绑定的 goroutines。

Go 提供了有限的方法来控制 Go 用来执行 goroutines 的操作系统线程。可以使用 GOMAXPROCS 环境值和等效的runtime.GOMAXPROCS(n int)函数来设置运行 goroutines 的最大 CPU(或内核)数量。

Go 提供了通过使用上下文以编程方式取消或超时长时间运行的异步进程的能力,比如 goroutines 和网络或数据库请求中的循环。 28 上下文还提供了一个通道来通知侦听器这样一个长时间运行的进程已经完成(正常情况下或通过超时)。上下文将在本文后面详细讨论。

runtime.Goexit()函数在运行完所有延迟的函数后杀死调用的 goroutine。注意,main函数运行在一个 goroutine 上。当main的 goroutine 退出(返回)时,EXE 结束。这就像当所有非守护线程结束时,JVM 也结束了。

runtime.Gosched()函数使当前的 goroutine 自愿放弃(让出)它的线程,但保持可运行。这就像在 Java 中使用Thread.yield()一样。在长时间运行的代码段(如循环)中,让步是很好的做法。

由于 goroutines 比 Java 线程更轻量级,所以对池化和重用它们作为 Java 支持的支持更少;新的 goroutines 通常是根据需要创建的。Goroutines 不像线程那样提供身份和类似的管理身份的方法。通道通常取代了 Java 中对线程本地的需求。

Goroutines 示例

与 Java 一样,没有语言手段来测试 goroutine 的完成情况,但是标准库函数确实存在。Java 使用Thread.join()方法来实现这一点。在 Go 中,一种常见的方式是通过 WaitGroups (WG)。WG 实际上是一个递增/递减计数器,客户可以在这里等待它倒数到零。完成此操作的常用方法如下:

var wg sync.WaitGroup
:
wg.Add(1)
go func() {
      defer wg.Done() // idiomatic. Done() is equivalent to Add(-1)
      :
}()
:
wg.Add(1)
go func() {
      defer wg.Done()
      :
}()
:
wg.Wait()

在每个 goroutine 启动之前,WG 都会递增。此增量必须在 goroutine 主体之外才能正常工作。在每个 goroutine 中,当 goroutine 结束时,WG 递减Done。然后,启动的 goroutine 等待(暂停)所有(可以有任意数量的 go routine)启动的 go routine 结束。

下面是一个类似的 Java 解决方案:

var threads = new ArrayList<Thread>();
var t1 = new Thread(() -> {
  :
});
t1.start();
threads.add(t1);
:
var t2 = new Thread(() -> {
  :
});
t2.start();
threads.add(t2);
:
for (var t : threads)
  try {
    t.join();
  } catch (InterruptedException e) {
    // ...
  }
}

Go 频道也可以做类似的事情:

var count int
// support up to 100 completed before any blocked
var done = make(chan bool, 100)
:
count++
go func() {
      defer sayDone(done)  // must be a function call
      :
}()
:
count++
go func() {
      defer sayDone(done)  // must be a function call
      :
}()
:
waitUntilAllDone(done, count)

func sayDone(done chan bool) {
      done <- true
}
func waitUntilAllDone(done chan bool, count int) {
      for count > 0 {
            if <- done {
                  count--
            }
      }
}

清单 4-2 显示了作为完全可运行示例的前述方法的一个稍微不同的表达式。

package main

import (
      "fmt"
)

var count int
var done = make(chan bool, 100)

func sayDone(index int) {
      done <- true
      fmt.Printf("go %d done\n", index)
}

func waitUntilAllDone(done chan bool, count int) {
      for count > 0 {
            if <-done {
                  count--
            }
      }
}

func main() {
      fmt.Println("Started")
      for i := 0; i < 5; i++ {
            count++
            go func(index int) {
                  defer sayDone(index)
                  fmt.Printf("go %d running\n", index)
            }(i)
      }

      waitUntilAllDone(done, count)
      fmt.Println("Done")
}

Listing 4-2Complete Example of the Use of Channels

它产生以下输出:

Started
go 4 running
go 1 running
go 1 done
go 0 running
go 0 done
go 4 done
go 3 running
go 3 done
go 2 running
go 2 done
Done

如果您将done通道的大小减少到 1,您会得到如下输出:

Started
go 4 running
go 4 done
go 3 running
go 3 done
go 1 running
go 1 done
go 0 running
go 0 done
go 2 running
go 2 done
Done

请注意,交叉工作较少。通道的容量会强烈影响使用它的 goroutines 的并行性。还要注意,如果在sayDone(...)中将"fmt.Printf(...)"放在"done <- true"之前,输出模式可能会不同。

Go 与前面在包sync/atomic中讨论的 Java 中的原子值等价:

var sum int32
:
atomic.AddInt32(&sum, 100)

Go 例程的行为可能不可预测,尤其是当多个 Go routines 同时运行时。考虑清单 4-3 中所示的简单例子。

package main

import (
      "fmt"
      "time"
)

func printNum(id string, count int) {
      for i := 0; i < count; i++ {
            fmt.Printf("%s: %d\n", id, i)
            delay := time.Duration(rand.Intn(10)) * time.Millisecond
            time.Sleep(delay)  // delay a bit
      }
}

func main() {
      printNum("one", 5)
      printNum("two", 5)
      printNum("main", 5)
}

Listing 4-3Complete Serial Printing Example

能出来什么?具体如下:

one: 0
:
one: 4
two: 0
:
two: 4
main: 0
:
main: 4

但是有了这个小小的改变:

func main() {
      go printNum("one", 5) // now a goroutine
      go printNum("two", 5) // now a goroutine
      printNum("main", 5)
}

能出来什么?可能是与之前相同的行,但是以任何可能的顺序(其中前面的顺序是不可能的)。但是可能只有一些one和/或two线出来。这是因为 Go 调度程序只能运行准备好的 goroutines(而Sleep让它们不准备好),但是可以按任何顺序运行,并且可以随时在它们之间切换。同样,main函数也运行在一个 goroutine 中,它可能会在其他 go routine 完成之前结束,导致程序结束。因此,goroutines 通常表现得像 Java 中的守护进程线程一样。

下面是一个输出示例:

main: 0
one: 0
two: 0
main: 1
two: 1
one: 1
two: 2
main: 2
main: 3
two: 3
two: 4
one: 2
main: 4
one: 3

作为使用 goroutines 的最后一个例子,让我们提供一个程序,它可以并行压缩所有命名为命令行参数的文件。它使用“去库调查”部分中定义的CompressFileToNewGZIPFile函数。它有这样的签名:

func CompressFileToNewGZIPFile(path string) (err error)

我们的main是这样定义的(用一个虚拟版本的CompressFileToNewGZIPFile只是为了演示并发性)。

package main

import (
      "fmt"
      "log"
      "math/rand"
      "os"
      "sync"
      "time"
)

func CompressFileToNewGZIPFile(path string) (err error) {
      // dummy compression code
      fmt.Printf("Starting compression of %s...\n", path)
      start := time.Now()
      time.Sleep(time.Duration(rand.Intn(5) + 1) * time.Second)
      end := time.Now()
      fmt.Printf("Compression of %s complete in %d seconds\n", path,
      end.Sub(start) / time.Second)
      return
}

func main() {
      var wg sync.WaitGroup
      for _, arg := range os.Args[1:] { // Args[0] is program name
            wg.Add(1)
            go func(path string) {
                  defer wg.Done()
                  err := CompressFileToNewGZIPFile(path)
                  if err != nil {
                        log.Printf("File %s received error: %v\n", path, err)
                        os.Exit(1)
                  }
            }(arg)  // prevents duplication of arg in all goroutines
      }
      wg.Wait()
}

Listing 4-4Parallel File Compression Example

这会产生以下命令行输出,包括

file1.txt file2.txt file3.txt file4.txt file5.txt

Starting compression of file5.txt...
Starting compression of file1.txt...
Starting compression of file3.txt...
Starting compression of file2.txt...
Starting compression of file4.txt...
Compression of file4.txt complete in 2 seconds
Compression of file5.txt complete in 2 seconds
Compression of file1.txt complete in 3 seconds
Compression of file3.txt complete in 3 seconds
Compression of file2.txt complete in 5 seconds

请注意,goroutines 以不可预测的顺序开始。

值得注意的是,goroutine 不能向其调用者返回结果,因此 goroutine 中出现的结果(或错误)必须以其他方式报告。在这个例子中,它们被记录(并且程序被终止),但是通常有一个通道,在这个通道上这样的错误(或结果)被报告给通道监听器。

回到关键部分。与 Java 不同,Go 中没有同步的语句或块。经常使用的是锁柜接口。本质上是

type Locker interface {
      Lock()    // better named: WaitUntilAvailableAndLock()
      Unlock()  // better named: UnlockAndThusMakeAvailable()
}

sync.Mutex类型实现了这个接口。它可用于控制对关键部分的访问。它的基本用法如下:

var mx sync.Mutex
:
func SomeAction() {
      mx.Lock()
      defer mx.Unlock()
      : do something that is a critical section

}

注意锁不允许同一个 goroutine 重入,就像synchronized使用 Java 线程一样。所以,小心使用它们以防止自我死锁。

通道可以做类似的事情:

var ch = make(chan bool, 1)  // allow only one received message at a time
:
func SomeAction() {
      ch <- true
      defer func() {
            <- ch   // discards the value
      }()
      : do something that is a critical section
}

如果没有空间接受该值,则顶部的 send 会阻塞。因为通道只能容纳一个值,所以只能允许一个用户。底部的 receive 删除该值。通过增加通道的大小,我们可以允许有限数量的 goroutines 并发地进入操作。

Go 有一个额外的选项来避免锁定关键部分。通过以其他方式使用通道,正如本文后面所讨论的,锁通常可以被消除。相反,数据是通过通道在消费者之间传输的,所以关键部分根本不存在。这通常是首选。

**

五、Go 基本功能

在这一章中,我们将深入探究一些让 Go 成为现在这个样子的重要特征。当我们读完这一章时,你应该能够描述出 Go 语言与 Java 语言的相似之处和不同之处。

语言关键字

Go 保留字(不能用于任何其他目的)是

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

表 10 Go 保留字

Java 和 Go 都有关键字,有些是保留字。它们在表 5-1 中列出。如果它们具有相同/相似的目的,则在同一行中列出。某些关键字是保留字(只能按照语言中的定义使用,不能作为变量名)。

表 5-1

保留字和关键字比较

|

爪哇

|

|

目的

|
| --- | --- | --- |
|   | _ | 可废弃的价值;Java 里没有 |
| abstract |   | Go 没有对等物 |
| assert |   | Go 没有直接的对等词;使用 panics 是类似的 |
| boolean | bool | 相同的 |
| break | break | 相同的 |
| byte | byte | Go 中未签名;用 Java 签名 |
| case | case | 一样;Go 有一些扩展 |
| catch |   | Go 在延迟函数中有一个内置的recover()函数,而不是try/catch |
|   | chan | Java 没有对等物 |
| char |   | Go 有一个rune (32 位)类型 |
| class |   | Go 没有 OOP 一个struct类型是最接近的近似值 |
| const | const | 在 Java 中未使用 |
| continue | continue | 相同的 |
| default | default | switch中相同;Go 中没有类似的能见度。在 Go 中,函数没有用处 |
|   | defer | 喜欢 Java try/finally |
| do |   | Go 没有直接的对等词 |
| double |   | Go 有一个float64型 |
| enum |   | Go 使用int (iota)常量代替(如 C) |
| else | else | 相同的 |
| extends |   | Go 没有传承 |
|   | fallthrough | 创建 Java switch失败行为 |
| final |   | Go 有一个const声明 |
| finally |   | Go 使用延迟函数代替try/finally |
| float |   | Go 有一个float32型 |
| for | for | 类似的 |
|   | func | Java 有 Lambdas |
|   | go | Java 有线程 |
| goto | goto | Java 中未使用;在 Go 中,目的与break相似,但可以在循环之外使用 |
| if | if | 相同的 |
| implements |   | Go 隐式地这样做 |
| import | import | 类似的 |
| int | int | Go 也有一个int32类型 |
| instanceof``(type)x | x.(type) | Go 有一个类型断言测试;断言的强制转换部分 |
| interface | interface | 类似角色 |
| long |   | Go 有一个int64类型 |
|   | map | Java 有一个 HashMap (以及其他)库类型 |
| native | 没有主体的函数定义 | Go 没有直接的对等词;但是 CGo 做了类似的事情 |
| new | new | 创建一个对象;Go 内置了一个类似的新功能;Go 没有与类型相关联的构造函数的概念 |
| package | package | 类似角色 |
| private |   | Go 使用小写名称(更多的包保护) |
| protected |   | 不需要;Go 没有传承 |
| public |   | Go 使用大写名称 |
|   | range | Java 有一个for语句 |
| return | return | 一样;Go 可以返回多个值 |
|   | select | Java 没有对等物 |
| short |   | Go 有一个int16类型 |
| static |   | Go 有全局(相对于类)变量 |
| strictfp |   | Go 没有对等物 |
|   | struct | Java class可以以类似的方式使用 |
| super |   | Go 没有对等物 |
| switch | switch | 类似;Go 有扩展名 |
| synchronized |   | Go 没有直接的对等词;库可以提供类似的行为 |
| this |   | Go 可以为此角色使用任何名称 |
| throw |   | Go 有一个内置的应急功能 |
| throws |   | 不需要;Go 没有要声明的例外 |
| transient |   | Go 中没有对等词 |
| try |   | Go 没有直接的等价物,但是支持类似于try/catchtry/finally的行为 |
|   | type | Java 没有对等物 |
| var | var | Java 使用类型名(块局部变量除外) |
| void |   | 省略的返回类型在 Go 中起到了同样的作用 |
| volatile |   | Go 没有对等物 |
| while |   | Go 有for |

运算符和标点符号

Java 和 Go 都有运算符和标点符号。许多语言有相同或相似的目的,但是每种语言都有一些独特的目的。由于 Go 支持有符号和无符号整数类型,一些操作符的工作方式略有不同(Java 不支持无符号整数)。还有,Go 不会自动将较小的数字转换成较大的数字(如byte->-short->->-double);这种转换在 Go 中必须是显式的。表 5-2 总结了 Go 和 Java 操作符以及它们之间的比较。

表 5-2

Java 和 Go 运算符比较

|

爪哇

|

|

目的

|
| --- | --- | --- |
| + | + | 一样;(二元)加法和(一元)正数或字符串连接 |
| - | - | 一样;(二进制)减法和(一进制)负数 |
| * | * | 同(二进制)乘法;在 Go 中,还有指针声明和(一元)解引用 |
| / | / | 一样;分开 |
| % | % | 一样;以…为模 |
| & | & | 一样;逐位 and 在 Go 中,也(一元)用作地址 |
| &#124; | &#124; | 一样;按位或 |
| ^ | ^ | 一样;逐位异或;(一元)不在 Go 中的布尔值上 |
| << | << | 一样;按位左移 |
| >> | >> | 一样;按位右移 |
| >>> |   | 无符号逐位右移;在 Go 中,对无符号 int 类型使用>> |
|   | &^ | 位清零(和不清零);Java 里没有 |
| = | = | 分配;在 Go 中,一种语句,而不是一种运算符 |
| += | += | 加赋值;在 Go 中,一种语句,而不是一种运算符 |
| -= | -= | 减去赋值;在 Go 中,一种语句,而不是一种运算符 |
| *= | *= | 乘法赋值;在 Go 中,一种语句,而不是一种运算符 |
| /= | /= | 划分作业;在 Go 中,一种语句,而不是一种运算符 |
| %= | %= | 模数分配;在 Go 中,一种语句,而不是一种运算符 |
| &= | &= | 和赋值;在 Go 中,一种语句,而不是一种运算符 |
| &#124;= | &#124;= | 或者赋值;在 Go 中,一种语句,而不是一种运算符 |
| ^= | ^= | 异或赋值;在 Go 中,一种语句,而不是一种运算符 |
| <<= | <<= | 左移赋值;在 Go 中,一种语句,而不是一种运算符 |
| >>= | >>= | 右移位赋值;在 Go 中,一种语句,而不是一种运算符 |
| >>>= |   | 无符号右移位赋值;不在 Go 中 |
|   | &^= | 位清零分配;Java 里没有 |
| && | && | 一样;逻辑与;短路 |
| &#124;&#124; | &#124;&#124; | 一样;逻辑或;短路 |
| ++ | ++ | 自动递增;在 Go 中,只有后缀;在 Go 中,一种语句,而不是一种运算符 |
| -- | -- | 自动减量;在 Go 中,只有后缀;在 Go 中,一种语句,而不是一种运算符 |
| == | == | 一样;等于测试 |
| != | != | 一样;不等于测试 |
| < | < | 一样;小于测试 |
| <= | <= | 一样;小于或等于测试 |
| > | > | 一样;大于测试 |
| >= | >= | 一样;大于或等于测试 |
|   | := | 简单声明;Java 里没有 |
| ... | ... | 类似;varargs 声明;在 Go 中,列出函数参数的展开 |
| ( | ( | 一样;打开参数列表或打开子表达式 |
| ) | ) | 一样;关闭参数列表或关闭子表达式 |
| [ | [ | 一样;开放索引 |
| ] | ] | 一样;收盘指数 |
| { | { | 一样;打开块或初始化列表 |
| } | } | 一样;关闭块或初始化列表 |
| ; | ; | 一样;在 Go 中,如果放在行尾,通常可以省略 |
| : | : | 一样;分离器 |
| @ |   | 批注指示器;不在 Go 中 |
| :: |   | 方法参考;不在 Go 中 |
| . | . | 一样;字段参考 |
| , | , | 一样;列表或参数分隔符;不是 Go 中的运算符 |
| ~ |   | 没有比特智慧;不在 Go 中 |
| ?: |   | 三元选择;不在 Go 中 |
| ! | ! | 一样;逻辑非 |
| -> |   | Lambda 表达式声明;不在 Go 中 |
|   | <- | 向通道发送或从通道接收(基于位置);Java 里没有 |
| instanceof``(type)value | x. (y) | 测试类型;不在 Go;Go 有一个断言表达式,用于转换类型并返回一个布尔值if,这种转换是可能的;can if 测试布尔型 |
| new | New``make | 分配和构造一个对象;Go 有新功能,make 有。此外,可以声明 Go 结构并获取地址,这将导致相同的操作。在 Go 中,new不运行任何构造函数;那就是make的作用 |

Java 和 Go 都有关系运算符(==!=<<=>>=),但它们的工作方式并不总是相同。例如,要在 Java 中比较两个字符串s1s2(或任何其他引用类型)是否相等,必须使用

if(s1.equals(s2)) { ... }

在 Go 中,人们会使用

if s1 == s2 { ... }

只有当类型可比较时,这种比较才有效。大多数内置类型都是。有关详细信息,请参见 Go 语言规范。切片、贴图和函数值除了与nil值比较之外是不可比的。指针、通道和接口值也可以与nil进行比较。

在比较引用的 Java 中,以下内容具有不同的含义(即,相同性测试):

if(s1 == s2) { ... }

这是一个测试,看看s1s2引用是否指向同一个对象(即是彼此的别名)。为了在 Go 中得到等价的测试,需要这样的测试来比较字符串的地址(不是值):

if &s1 == &s2 { ... }

Java 不隐式支持引用类型的关系测试;类型本身必须提供一些方法来做到这一点。对于字符串,测试可以是

if(s1.compareTo(s2) < 0) { ... }

在 Go 中,这将是

if s1 < s2 { ... }

只有当类型通过某种方式排序时,这种比较才有效。在 Go 中,很多类型都是。有关详细信息,请参见 Go 语言规范。例如,字符串被排序为字节数组,较短的字符串意味着附加的零单元。

像在 Java 中一样,&&||操作符是短路的,可能只计算左边的参数。

Go take address ( &,又名的地址)一元运算符返回其操作数的地址,该地址必须是可寻址的(有存储位置;例如,常数,而许多表达式不是)。Java 没有与这个动作等价的动作。对于任何类型为T的值,&操作符返回类型为*T的值。我们不能接受一个nil值的地址。

可寻址的值有

  • 已声明的变量

  • 指针解引用(*p)–返回 p

  • 数组或切片索引表达式

  • 结构字段选择器表达式

  • 复合(数组、切片、映射、结构)文字

注意,对于任何导致恐慌的表达式exp,表达式&exp也将。

Go 运算符优先级

Java 操作符优先级很复杂,这里不再赘述(更多细节参见 Java 语言规范)。Go 优先一般比较简单。一元运算符优先于二元运算符。

一元从高到低:

  • Wrapping ( ... )

  • Prefix + - * &

  • Suffix [ ... ] ( ... )

注意 Go 中的++--是语句,不是运算符。

二进制从高到低:

  • / % << >> & &^

  • + - | ^

  • == != < <= > >=

  • &&

  • ||

最佳做法是在有疑问时使用括号(...)来阐明优先顺序,尤其是对于一元运算符。

注意 Go 有 Java 没有的位清除(&^)运算符。

表达式x &^ y实际上是x AND (NOT y)

注意 Go 没有 Java 的二进制 not ( ~)运算符。请改用异或运算符。例如:

func not32(x uint32) uint32 {
       return x ^ 0XFF_FF_FF_FF
}
func not64(x uint64) uint64 {
       return x ^ 0XFF_FF_FF_FF_FF_FF_FF_FF
}

or:
func not32(x uint32) uint32 {
       y := int32(-1)
       return x ^ uint32(y)
}
func not64(x uint64) uint64 {
       y := int64(-1)
       return x ^ uint64(y)
}

当运行时

fmt.Printf("%X\n", not32(10))
fmt.Printf("%x\n", not64(10))

产生(注意大小写差异)

FFFFFFF5
fffffffffffffff5

Go 内置函数

Go 有几个内置函数,总结在表 5-3 中,用于访问常见行为。这些函数通常是通用的(或重载的),因为它们处理不同的数据类型。Java 通常有执行类似行为的特定于类型的方法。

注意,Go 内置函数名不是保留关键字;这些名称可能用于其他目的(可能隐藏了内置函数)。

表 5-3

Java 和 Go 常用函数比较

|

爪哇

|

|

目的

|
| --- | --- | --- |
| .length, .length(), .size(), ... | len(...) | 获取字符串、数组、切片、映射、通道的长度 |
| 因收藏类型而异 | cap(...) | 获取切片、地图、频道的容量;对于某些集合,caplen是相同的 |
| new。。。或工厂方法 | make(...)new(...)&<structureType>{} | 创建(并为make初始化)一个集合或结构;返回一个指向分配内存的指针;对于new,在 Go 中没有构造函数调用 |
| System.arraycopy | copy(...) | 在相同/不同数组之间复制/移动数组元素 |
| 因收藏类型而异 | delete(...) | 从地图中删除元素;通常用作陈述 |
| Java 里没有;有些类型有这样做的方法 | close(...) | 关闭一个频道;通常用作陈述 |
| (<type>)... | <type>(...) | 将参数转换为指定的类型(即强制转换) |
| throw <throwable> | panic(...) | 引起恐慌;可以发送任何类型作为紧急值,但是最好发送一个error实例;一般用作陈述;避免使用panic(nil) |
| try/catch | v := recover() | 惊慌失措。通常用在延迟函数中 |
| 因收藏类型而异;经常add() | append(...) | 向切片追加值;如果需要,重新分配切片;应该将结果分配给输入切片 |
| Java 里没有 | complex(...) | 生成一个复数值 |
| Java 里没有 | real(...) | 获取复数值的实部 |
| Java 里没有 | imag(...) | 获取一个复数值的虚部 |

六、Go 类型

在这一章中,我们将深入探讨 Go 的类型系统,以及它与 Java 的类型系统有何不同。读完这一章后,你应该能够清楚地识别 Go 和 Java 类型系统之间的相似和不同之处。

原始/内置类型

Java 和 Go 有相似的原语类型。在 Java 中,基本类型不能放在集合中(数组除外)。这与引用类型有很大的不同。此外,基本类型不能有方法。基本类型的概念在 Go 中意义不大,因为任何类型都可以是集合的元素,任何派生类型都可以有方法。

像 Java 中一样,Go 布尔类型是最简单的类型。它只有truefalse作为值。

两种语言都将字符、有符号整数和浮点类型作为数字类型。Go 将无符号整数和complex数字相加。

Go 还有一个指针(有点像 Java 引用)类型。两种语言都有一个null(在 Go 中称为nil)值。在 Go 中,指针可以转换成无符号整数,反之亦然。这种用例并不常见,因此不在本文中讨论。大多数情况下,这样做是为了与用 C 语言编写的代码(也称为 CGo)接口。

民数记

内置数值类型:

  • 有符号整数类型—int8int16int32(又名rune)、int64int

  • 无符号整数类型—uint8(又名byte)、uint16uint32uint64uint

  • 浮动类型—float32float64

  • 复杂类型(如realimag对)–complex64complex128

后缀数字(如果有)是该值的位数。

非内置数值类型:

  • 大整数—Int

  • 大浮动—Float

  • 有理-Rat(两个int64值的)

注意,Java 有一组库函数使用它的有符号整数来模拟无符号整数;Go 方法更好。Go 没有大十进制类型。

像在 Java 中一样,数字文字值可能有一个基本前缀:

  • 0b,0B–二进制,数字:0,1

  • 0o,0O–八进制,数字:0,1,2,3,4,5,6,7

  • 0x,0X–十六进制,数字:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F(或 A,B,C,D,E,F)

像在 Java 中一样,数值可以在数字之间包含下划线(“_”)。像 Java 一样,如果一个数以“0”开头,并且没有基数指示符,那么它就是八进制的。

浮点数添加一个可选分数(" .“引入者”)和/或可选的带符号十进制指数(十进制数用“E”或“E”引入者,十六进制数用“P”或“P”引入者,很少使用 P 指数)。

与 Java 不同,数字文字没有大小指示符或值限制。

像在 C 中一样,Go 增加了对架构敏感的类型(基于架构的 32 位或 64 位字长的整数和指针)。Java 显式地隐藏了依赖于架构的数字特征。

Go 添加了带有虚文字量的复杂浮点类型,虚文字量是以“I”为后缀的任何数字文字量。Go 不支持复数的极坐标表示法。

Go 使用不确定精度的无类型数值。这些文字被自动转换为表达式或初始化所需的类型。Java 数字文字有特定的类型(shortint等)。)后缀。

字符和字符串

Go 的内置字符称为 Rune,是有符号的 32 位(int32 ) Unicode 整数。Java 使用名为char的无符号 16 位 Unicode 整数,其中较大的字符需要char对(充其量是笨拙的)。两者都被视为整数数值。

Go 有一个内置的string类型,它本质上是一个由byte值组成的数组(byteuint8的同义词),预期用 UTF-8 格式表示一个 0+字符的字符串。Java 有一个字符串 (JRE 库)类型,本质上是一个 Java char数组。Java 和 Go 字符串都是不可变的。

Go 字符串的长度是字符串中的字节数(不是字符数),而在 Java 中,字符串的长度是char值的个数。注意如果使用 ASCII 编码,字符和字节是同义的。字符串可以被索引到它们的长度来提取字符。将它们作为字节进行索引时必须小心,因为它们可能包含 UTF-8(多字节)字符。处理符文的帮助见scanner包。

例如:

s := "Hello World"
firstByte, hello, world, copy, lenS :=
      s[0], s[:5], s[6:], s[:], len(s)

很像 Java 的charAt()方法,单索引表达式是[index]

字符串是不可变的,因此如下表达式是非法的:

s[0] = 'a'

很像 Java 的substring方法,范围索引表达式是[{start}:{end+1}];缺少开始= = 0;缺少的 end == length。

Go 也有一个类似于字符串的 byte slice 类型,但不一定会出现在 UTF-8 中。如果所有字节都是 ASCII 码,则可以将其视为一个字符串。

所有的数字和字符串类型都可以比较(==,!=>>=<<=)。字符串就像字节数组一样进行比较。

注意字符串不能是nil(但是指向字符串的指针可以是)。将nil值赋给任何字符串变量都是不合法的。使用len(s)功能(vs. s == "")测试空字符串。

注意大多数在 Java 中被视为静态或String(或StringBuilder/Buffer)类的实例方法的函数都被定义为 Go fmtstringsstrconv包中的函数。

引用与指针类型

Java 基本上有两类可变数据:

  1. 原始值–数字和布尔值

  2. 引用(或对象)–一个值,它是对象实例或null的定位器

    对象总是在堆中分配。

Go 也有类似的分类:

  1. 任何(非指针)类型的值

  2. 指针–是另一个值的地址的值,包括另一个指针,或nil

    通常,由指针定位的数据值通常在堆中分配。

注:接口类型的变量可以像值或指针一样工作。

一个 Java 引用选择了某个对象(或者如果null没有选择任何对象)。引用是如何实现的取决于 JVM,但是可以如图 6-1 所示实现,其中每个引用是一个指向对象的指针数组的索引。变量(A、B 和 C)在左边。参考索引表在中间。被引用的对象在右边。这种方法使垃圾收集变得容易,因为当删除未使用的对象并将剩余的对象压缩到内存中时,只需要更新索引表。请注意,同一索引可以有多个引用;这就产生了别名。

img/516433_1_En_6_Fig1_HTML.png

图 6-1

可能的 Java 参考实现

在 Go 中,如图 6-2 所示,指针是对数据的直接引用(对象或任何类型的值)。变量(A、B 和 C)在左边。被引用的对象在右边。注意,同一个地址可以有多个变量;这就产生了别名。这种方法使得垃圾收集期间的压缩变得困难,因此不经常进行。

img/516433_1_En_6_Fig2_HTML.png

图 6-2

可能的 Go 指针实现

Java 引用就像一个指针,但是它通常是隐式解引用的(如前所示)。例如:

public class Xxx {
  public int x, y, z;
}
:
Xxx xxx = new Xxx();
int v = xxx.z;

在这里,引用xxx指的是堆上的一个 Xxx 实例,它被“.”解除引用操作员。

类似地,在 Go 中,使用结构而不是类:

type Xxx struct{
      X, Y, Z int
}
:
var xxx *Xxx  // creates a "zero" valued (or nil) pointer variable
xxx = &Xxx{}  // assign instance address (a new is implied)
v := xxx.Z

或者

var xxx = &Xxx{}  // alternate declaration

这里,指针xxx指向一个 Xxx 实例,并且它被。“接线员。这在逻辑上等同于

v := (*xxx).Z

其中解引用是显式的。

注意这样做在 Go 中是合法的(但在 Java 中是无意义的,因为 xxx 会有一个null值):

var xxx Xxx // creates a "zero" valued variable
v := xxx.Z

在 Go 中,可以获取该实例的地址:

pToXxx := &xxx

Java 中没有类似的东西。在 Go 中,可以这样更新xxx实例:

pToXxx.Z += 10

也可以获取字段的地址:

pToXxxZ := &(xxx.Z)

这样就可以更新xxx实例了:

*pToXxxZ += 10

这种获取对象地址的能力意味着 Go 必须提供真正的闭包。闭包是捕获函数调用历史中的一些值,这样函数就可以在函数的生命周期内获取或设置它们。Java 有一种有限形式的闭包,闭包中的所有变量都必须是final,所以它们不能被更改。Go 也允许它们被改变。Go 程序经常利用这种能力。

向下钻取基本数据类型

像在 Java 中一样,布尔、整数和浮点类型以及字符和字符串类型是常用的类型。

布尔型

Java 和 Go 支持几种布尔类型。见表 6-1 。

表 6-1

布尔类型

|

Java 类型

|

大小(位)

|

Go 类型

|

Go 示例

|
| --- | --- | --- | --- |
| boolean | 未定义 1 | bool | 真实的 |

整数类型

Java 和 Go 支持几种整数类型。见表 6-2 。

表 6-2

整数类型

|

Java 类型

|

大小(位)

|

Go 类型

|

Go 示例

|
| --- | --- | --- | --- |
| byte | eight | int8 | 10, –1 |
| short | Sixteen | int16 | 10, –1 |
| int | Thirty-two | int32 or int 2 | 10, –1 |
| long | Sixty-four | int64 or int 3 | 10, –1 |
| char | Sixteen | uint16 | “A”或 10 (ASCII 值) |
|   | eight | uint8 or byte | “A”或 10 (ASCII 值) |
|   | Sixteen | uint16 | “A”或 10 (ASCII 值) |
|   | Thirty-two | uint32 | “A”或 10(符文值) |
|   | Sixty-four | uint64 | Ten |
|   | Thirty-two | rune``(an int32 alias) | “A”和 Unicode 转义 |

Note

Java 有一个库,提供对有符号值的无符号操作。

Java 有所有整数类型的对象包装器。Go 没有这样的等价物,因为不需要它们。

浮点类型

Java 和 Go 支持几种浮点类型。见表 6-3 。

表 6-3

浮动类型

|

Java 类型

|

大小(位)

|

Go 类型

|

Go 示例

|
| --- | --- | --- | --- |
| float | Thirty-two | float32 | 10.0,1e10 |
| double | Sixty-four | Float64 | 10.0,1e10 |
|   | Sixty-four | complex64 | 10.1+3.2 升 |
|   | One hundred and twenty-eight | complex128 | –4.0i |

Java 有所有浮点类型的对象包装器。Go 没有这样的等价物,因为不需要它们。

Note Go 允许使用非十进制尾数的浮点文字(指数仍然是十进制)。

当零可能出错时

Java 程序员经常会遇到可怕的NullPointerException(NPE——由于 Java 没有指针,它应该被命名为 NullReferenceException )。Go 不能解决这个问题;大多数对nil指针的解引用都会导致类似的运行时混乱。但是 Go 的函数调用风格倾向于减少 NPE 的出现率。还有,在 Go 中,a nil有时可以像值一样使用;例如,一个nil切片可以被追加,而在 Java 中一个null集合不能被追加/添加。同样,与 Java 中的 npe 相比,这减少了 Go 中死机的发生。

通常,Java 中的函数可以返回null。通常,程序员不会先测试结果看它是否是null,从而得到一个 NPE。在 Go 中,测试函数的结果是否失败是惯用的方法,因此通常都是这样做的。例如,考虑这个可以得到 NPE 的典型 Java 模式:

:
var xs = getXxx().toString();
:

人们应该,但经常不这样编码:

var x = getXxx();
if(x != null) {
  var xs = x.toString()
  :
}

也就是说

if x, err := GetXxx(); err == nil {
      xs := x.String()
      :
}

errnilxnil的几率很低(写得好的 Go 里是 0)(反过来也是可能的——如果err不是nilx就会是nil)。Go 没有 Java Optional类型的等价物,可以帮助减少 Java 编码中的 npe。此外,在 Go 中不能将指针类型声明为非nil(也就是说,总是指向某个东西)。

注:在 Go 中,如果在创建时没有特别初始化,所有变量都有一个“零”值。对于指针,这是一个nil值。对于非指针类型,它通常是一个有用的值。

程序员定义的类型

Go 和 Java 都允许程序员创建新的数据类型。Java 使用classinterfaceenum构造来完成这项工作。Go 使用type语句,该语句基于预先存在的或直接(文字)基类型创建一个命名类型。Java 没有类似的功能。

基类型可以是基元类型、数组类型、结构类型、映射类型、接口类型、通道类型或指向任何类型的指针(包括其他指针)。它也可以是声明的类型。

每个这样声明的类型都是不同的,即使基类型是相同的。例如:

type weight      float64
type temperature float64
type age         float64

都是不同的类型(不能直接相互赋值或比较),即使它们共享相同的基类型(float64)。前面的例子也可以写成

type (
      weight      float64
      temperature float64
      age         float64
)

给定:

var w weight = 10
var a age = 10

由于类型不同(即使都基于float64),下面的代码不会编译;这是增加更多类型安全性的一种方法:

var sum = w + a

但是这将会编译(即使是无意义的),因为类型现在是相同的:

var sum = age(w) + age(a)   // second age(...) not required

当然,一个人不应该做这样的事情,除非它在当时的情况下有意义。

Go 支持创建现有类型的别名,如下所示:

type weight = float64
type temperature = float64
type age = float64

这里,这些新类型都是相同的类型(如果缺少“=”,则是不同的)。这就像byteuint8的内置别名一样。应谨慎使用该功能。

考虑这个例子(改编自 Go 语言规范例子):

type TimeZone int
const (
      EST TimeZone = -(iota + 5)
      CST
      MST
      PST
      AKST
      _  // blank value
      _  // blank value
      HST
)
func (tz TimeZone) String() string {
      return fmt.Sprintf("GMT%+dh", tz)
}
func (tz1 TimeZone) Difference(tz2 TimeZone) int {
      return int(tz1) - int(tz2)
}
func Gap(tz1, tz2 TimeZone) int {
      return tz1.Difference(tz2)
}

在这里,一个新的类型时区,基于int类型(实际上应该是一个浮点类型,因为一些时区与小时有 30 分钟(或 0.5 小时)的偏差),具有相关的常量(两个作为占位符)、方法和一个独立的实用函数。它还实现了fmt.Stringer接口。

从预先存在的类型中创建不同类型的概念有助于使 Go 代码更加安全,并且可以说更加清晰(因为类型名称有助于使代码更加不言自明)。不可能意外地(没有显式转换)将一种类型的值赋给另一种类型,例如,将权重值赋给温度变量。

这可以防止 Java 更容易出现的规模和精度损失错误。在 Java 中,变量命名方案可以部分地提供更多的安全性,但是 Go 在这方面做得更好。

要使 Go 聚合类型近似于 Java 类类型,可以这样做:

type Person struct {
      Age          float64
      Name         string
      PlaceOfBirth string
      privateValue int32
}

与 Java 相比:

public class Person {
  public double age;
  public String name;
  public String placeOfBirth;
  private int   privateValue;
}

Note Go 使用字段(和类型)名称的大小写来确定私有和公共可见性。

像在 Java 中一样,聚集成员和局部变量的声明是不同的。

在 Go 中,局部和全局变量是用“var”、“const”或“func”语句声明的。结构字段不使用“var”导入器。界面功能不使用“func”导管鞘。

在结构中,结构的字段是这样声明的:

type S struct {
      s string
      x, y, z int
      f func(int, int) int
}

其中没有允许的初始(超过零值)字段值。初始值必须来自结构文本:

var s = S{"abc", 0, 1, 2, nil}

或构造函数:

var s = NewS("abc", 0, 1, 2, func(x, y int) int {
      return x * y
})

其中第二个s是指针(*S)类型。NewS函数必须由一个人编码;Go 编译器不会生成一个。所有具体(如 struct)类型的构造函数(又名工厂)都应该返回指针,如new函数。在 Go 中,按照惯例,工厂方法(以创建实例为主要功能的方法)以New开始,比如New<typename>。如果这个包只定义了一种类型,那么按照惯例,这个名字就是New

例如:

func NewS(name string, a, b, c int, f func(int, int) int) (s *S) {
      s = &S{}  // or s = new(S)
      s.s = name
      s.x = a
      s.y = b
      s.z = c
      s.f = f
      return
}

或者更简单地说:

func NewS(name string, a, b, c int, fx func(int, int) int) (s *S) {
      s = &S{name, a, b, c, fx}
      return
}

注意变量名的不同(a 与 x,...).

注意在S结构定义中没有为函数fx提供代码体。在这里,fx是一个保存函数的变量(就像一个引用),而不是函数本身。任何函数定义都是在结构之外完成的。

在接口中,方法是这样声明的:

type Openable interface {  // or Opener (especially if 1 function)
      Open([]byte) (int, error)
      Close() error
}

与 Java 不同,在 Go 接口中只能定义函数签名。

注意没有为任何函数提供代码体。像在 Java 中一样,接口方法总是纯抽象的。Go 在接口中没有等价的(static final)字段,在接口中也没有具体的(default)方法。

像在 Java 中一样,如果工厂(和类似的)函数返回接口,而不是具体类型,会更好。这允许工厂返回符合接口的任何类型。在 Java 中,这意味着

public <I extends Integer> List<I> makeIntegerList() { ... }

是比这个定义更好的选择:

public <I extends Integer> ArrayList<I> makeIntegerList() { ... }

同样,在 Go 中最好做这样的事情:

type IntegerList interface {
      Len() int
      GetAt(index int) int
      SetAt(index int, v int)
}
func makeIntegerList() IntegerList { ... }

new函数这样的工厂方法应该返回指针。允许替换工厂名,如Make<typename>(在各种make函数之后),如果最好的话,它们可以返回一个实例(相对于指向实例的指针)。考虑一下:

func NewIntegerList() *IntegerList { ... }

它可以返回任何实现IntegerList的类型。或者:

func MakeIntegerList() IntegerList {
      return *NewIntegerList()
}

记住,在不传递和返回指针时要小心,因为值会被复制到函数中或从函数中复制出来。这可能会增加开销,在某些情况下会导致行为问题。

工厂方法很少有接收者参数。一个例外可以是基于复制或原型的方法,可能命名为CopyClone:

func (il *IntegerList) Clone() *IntegerList { ... }

数组

Go 和 Java 都支持任何单一(同构)类型的密集数组(一维定长打包列表)。两者都使用从零开始的索引。两者在创建时都被设置为固定长度,不能更改。两者都以相似的方式支持多维数组。像在 Java 中一样,数组提供了 O(N) 搜索和 O(1) 访问。

所有的 Java 数组都存在于堆上;Go 可能允许数组也存在于调用堆栈中。

使用[<length>]type语法声明数组。例如:

var x [10]int               // array of 10 ints, each set to 0
var x [10]string            // array of 10 strings, each set to ""
var x [10][10]int           // array of 10x10 ints, each set to 0
var x = [3]int{1,2,3}       // array of 3 ints, each set to provided values

Java 的对等物是

var x = new int[10];
var x = new String[10]; // need a loop to create the 10 empty strings
var = new int[10][];    // need a loop to create the 10 nested arrays
var x = new int[]{1,2,3};

在 Go 中,数组长度是其类型的一部分,所以[10]int[20]int是不同的类型。这在 Java 中是不成立的。

同类型的数组可以比较(==,!=>>=<<=)。

零起点索引表达式访问数组元素,如下所示:

a := [...]int{1,2,3}  // has implied length of 3
a[0] = a[1]

这里,根据数组的定义,len(a)cap(a)都是 3。像在 Java 中一样,数组边界之外的索引访问会导致混乱。

像在 Java 中一样,数组可以有文字值。通常,文字意味着数组的长度。一些例子:

var x [...]int{1,2,3}    // array of 3 ints; each set to provided values
var x [10]int{1:1,5:5,6} // array of 10 ints; select values set, others 0

请注意,数组文字可以有显式索引(int 类型的常量表达式)。所有或部分条目可以有索引。如果所有索引都存在,索引顺序可以是任意的。任何缺失的索引都是序列中的下一个值。任何提供的索引必须是唯一的。因此,前面的示例相当于

var x = [10]int{0,1,0,0,0,5,6,0,0,0}

也可以写成

var x = [10]int{0,1,0,0,0,5,6}

其中隐含了最后的值。

与 Java 的一个重要区别是,给数组赋值会产生一个副本。例如:

var a1 = [...]int{1,2,3}
var a2 = a1
a2[0] = 10

Go 里,a1[0]还是一。在 Java 里,a1[0]现在也是十。这是因为在 Java 中a2a1的别名。这是因为在 Java 中所有的数组都是堆对象。

copy函数(像 Java 的System.arraycopy方法)使得将一个数组的元素复制到另一个数组变得容易。源数组和目标数组可以相同,这允许将元素移动到数组中的不同索引:

array1 := [...]int{1,2,3,4}
copy(array1[N:], array1[N+1:])    // shift (overwrite) elements down
array1[len(array1)–1] = 0        // set zero value (optional)

通过对所有元素取一个范围,可以将数组转换为数组上的一个切片(下一部分):

var a1slice = a1[:]

注意当你将数组作为函数参数或返回值传递时,数组被复制,就像赋值一样。传递一个指向数组的指针(或者最好是数组上的一个片)以允许访问原始数组。

注意,在 Go 中直接使用数组不如在 Java 中常见;切片的使用频率更高。

部分

像 Java VectorsArrayList一样,Go 允许一个可变大小的类似数组的构造,称为 slice 。切片是一种内置(相对于库)类型。切片类似于数组,但没有预先声明的大小。切片是通用的(因为不同的实例可以保存不同的类型,但是每个切片实例只能保存一种类型)。一个特例是可以保存任何类型的[]interface{}(类似于 Java 中的List<Object>)。与 Java 不同,Go 中没有可选的列表接口实现;只有一个实现可用。像在 Java 中一样,片给出了 O(N) 搜索和 O(1) 访问。

使用[]type语法声明切片。例如:

var x []int           // slice of ints
var x []string        // slice of strings
var x [][]int         // slice of slice of ints
var x []int{1,2,3}    // slice of 3 ints, each set to provided values

注意,片声明看起来像数组声明,但是缺少长度值。

在大多数情况下,人们在 Go 中使用切片,而不是数组。切片隐式包装一个数组。

空切片通常用make函数显式创建。例如:

var x = make([]int, 0, 10)        // slice of up to 10 ints
var x = make([]string, 0, 10)     // slice of up to 10 strings
var x = make([][]int, 0, 10)      // a loop to create the 10 nested slices

每个示例切片的初始长度为 0,容量为 10。如果只提供长度(第一个数字),则容量被设置为长度。请注意,该片完全分配有零值:

var x = make([]int, 10)           // slice of 10 ints, each 0

作为使用make的替代方法,可以像这样创建一个切片,但这种方法不太常用:

var x = []int{} // or []int{1, 2, 3 } to initialize

注意makenew是相互关联的。以下示例是等效的:

var x = make([]int, 5, 10)
var x = new([10]int)[0:5]

这显示了切片是如何在数组上包装的。

前面的示例还展示了如何使用索引从数组中选择子元素(这里是前五个)。Java 语言不能做到这一点,但是大多数集合类型都有可以做到这一点的方法。这在 Go 中有效,因为切片是内置类型。

在幕后,就像(实际上没有)一个切片被定义如下:

type Slice[T any] struct { // slices behave as if generic
  data *[cap]T  // data "held" by the slice (an array)
  len int       // current length; always <= cap
  cap int       // max capacity; can append until exceeded
}

如果您需要切片容纳超过其当前容量的项目,则需要使用append(...)内置函数对其进行扩展。Java List 实现不需要显式扩展。例如:

x = append(x, 100)      // add 100 to x increasing its length by 1

如果x的容量耗尽,会用更多的容量重新创建;因此,有必要将任务分配回x。如果切片的容量(不是长度)需要扩展,Append 可以返回输入切片或新切片。因此,片既有长度(元素的实际计数)又有容量(元素的最大计数),所以每次添加一个项目时不一定需要扩展。这是一种优化。

从前面的结构中,很容易看出不同的片实例如何访问(共享)相同的数据。data指针只是指向某个共享数组。此外,每个片可能指向共享数组中的不同索引。例如:

slice1 := make([]int, 5, 10)  // len: 5, cap: 10
slice2 := slice1[2:5]         // len: 3 (5-2), cap: 8 (10-2)

这里,slice1slice2共享一个公共的数据数组(长度为 10),但是slice1指向数组的第一个元素,slice2指向第三个元素。每个切片都有不同的长度和容量值。slice2长度和容量值必须在slice1的范围内。

切片是数组或另一个切片(切片的切片)上的包装...).如前所述,切片是后备数组或切片的子范围的视图。通过任何切片更改元素都可以被其他切片看到。扩展切片可以替换该切片的后备阵列。

Java 的对等物是

List<Integer> x = new ArrayList<>(10);
List<String> x = new ArrayList<>(10); // need a loop to add the 10 empty strings
List<List<Integer>> x = new ArrayList<>(10); // need a loop to add the 10 nested lists
List<Integer> x = new ArrayList<>(10); // need a loop to add the values

注意,在 Java 中,如果列表是不可变的,那么更直接的形式(带有值)是可能的:

List<Integer> x = List.of(1,2,3);

零起点索引表达式可以访问切片元素,如下所示:

a := []int{1,2,3}  // a slice literal
a[0] = a[1]

这里,len(a)cap(a)都是 3。像在 Java 中一样,超出片长度的索引访问会导致死机。

考虑这个定义:

var x = []int{1:1,5:5,6} // slice of ints

请注意,切片文字可以有显式索引(int 类型的常量表达式)。所有或部分条目可以有索引。如果所有索引都存在,索引顺序可以是任意的。任何缺失的索引都是序列中的下一个值。任何提供的索引必须是唯一的。因此,前面的示例相当于

var x = []int{0,1,0,0,0,5,6}

当片被稀疏填充时,初始化的索引形式可能是有用的。

copy函数使得将一个片的元素复制到另一个片中变得容易。源切片和目标切片可以是相同的,这允许在切片中移动元素。例如,要从切片中删除第 n 个元素,可以这样做:

slice1 := []int{1,2,3,4}           // slice to remove from
copy(slice1[N:], slice1[N+1:])     // shift (overwrite) elements down
slice1 = slice1[: len(slice1) - 1] // remove (now dup) last element

在 Go 中,与 Java 数组一样,多维切片被实现为包含切片的切片。这可能会降低存储利用率。要使这样的切片既规则又尽可能密集,请使用一个一维切片,并通过代码将其划分成行。给定:

type PackedIntSlice struct {
      width, height int
      data []int
}
func NewPackedIntSlice(width, height int) (pas *PackedIntSlice) {
      if width <= 0 || height <= 0 {
            panic(errors.New("size must be positive"))
      }
      pas = &PackedIntSlice{width, height, make([]int, width * height)}
      return
}
func(pas *PackedIntSlice) Get(x, y int) int {
      pas.check(x, y)
      return pas.data[x * pas.width + y]
}
func(pas *PackedIntSlice) Set(x, y int, v int) {
      pas.check(x, y)
      pas.data[x * pas.width + y] = v
}
func(pas *PackedIntSlice) check(x, y int) {
      if x < 0 || x >= pas.width || y < 0 || y >= pas.height {
            panic(ErrIndexOutOfRange)
      }
}
var ErrIndexOutOfRange = errors.New("index out of range")

要制作密集的 100x200 整数切片,可以这样做:

var packed = NewPackedIntSlice(100, 200)
topLeft, topRight, bottomLeft, bottomRight :=
      packed.Get(0, 0), packed.Get(0, 199),
      packed.Get(99, 0), packed.Get(99, 199)

这个概念可以扩展到更高维度,数学家和科学家经常用到。也可以使用其他类型,如布尔型、浮点型、复杂型甚至字符串型。一些 Go 库就是这么做的。

地图

像 Java 一样,Go 支持类似关联数组的哈希表哈希表。Go 为此有一个内置的(vs . library)map类型。映射是通用的(因为它们可以有键并保存任何类型的值,但是每个映射实例只有一种类型)。像在 Java 中一样,map 给出了 O(1) lookup。

使用map[keyType]valueType语法声明映射。例如:

var x map[string]int            // map of ints with string keys
var x map[int]string            // map of strings with int keys
var x map[string]interface{}    // map of any type with string keys

一个映射对于每个不同的键只有一个条目。像 Java 一样,键必须是可散列的和可比较的(因此通常是基本的)类型。与 Java 不同,Go 中没有可选的 Map 接口实现;只有一个实现可用。Go 映射的枚举顺序是未定义的,可能是随机的。这防止了程序员依赖于任何预先确定的顺序。在 Java 中,对于某种类型和键集的映射,顺序是固定的(有时还有键插入顺序);这可以是故意的。

注意 Go 在sync.Map类型中有一个map类型的近似。它用于安全的并发访问,但它并不完全等同于内置的map类型。

地图通常用make函数显式创建。例如:

var x = make(map[string]int, 10)            // empty map with capacity 10

地图的容量自动扩展;初始值是一个优化值,通常可以忽略,但应该设置为> =最大预期项目数的值。

Java 的对等物是

Map<String,Integer> x = new HashMap<>(10);

与 Java 不同,Go 可以通过以任意顺序提供键/值对来创建文字映射,每个键/值对用冒号(":")分隔。这样的地图是可变的。例如:

var m = map[string]int{"key1":1, "key2":2, "key3":3}

或者

var m = map[int]string{1:"key1", 2:"key2", 3:"key3"}

这种文本中的所有值都必须有一个键。键和值都是表达式。除非是单个术语,否则应该用括号括起来。如果是常量表达式,则键必须是唯一的:

var m = map[int]string{(1 + 5):"key6", 2:"key2", (3*6):("key3"+"key6")}

地图元素通过一个键值来访问,该键值必须支持==和!=运算符,如下所示:

a := map[int]int{1:3,2:2,3:1} // a map literal
a[0] = a[1]

这里,len(a)是 3(地图上不允许有cap())。与 Java 不同,未定义的键返回值类型的零值(相对于nil)。要确定映射中是否包含键,需要修改 get 表达式:

valueAt99, ok := a[99]

这里,如果没有定义键,ok 值将是false,否则将是true,并设置 value 变量。这就像 Java 的Map.contains()方法。与这个 Java 示例相比:

var a = new HashMap<Integer,Integer>();
:
if(a.contains(99)) {
  var valueAt99 = a.get(99);
  :
}

和...相对

var valueAt99 = a.get(99);
if(valueAt99 != null) {
  :
}

在 Go 中,可以使用内置的delete(<map>,<key>)功能移除地图键。

与 Java 不同,Go 不支持集合类型。但是可以模拟一个,例如一组整数,映射类型如下:

s := map[int]bool{1:true, 20:true, 50:true}

其中键可以是任何可比较的类型,但是值总是布尔值(为了便于测试,按照惯例)。可以通过以下方式测试成员资格:

if s[5] { // is 5 in set }

虽然在 Go 中没有明确指定,但在概念上(不是合法的 Go ),映射类型由以下类型描述:

type map[K builtin, V any] struct {  // maps are generic
  data      *[hashLimit][]mapEntry[K builtin, V any]
  cap       int
  hashLimit int   // often a prime number
}

func (m *map) hash(k *K) (h int) { ... } // 0 <= h < hashLimit

type mapEntry[K builtin, V any] struct {
  key   K
  value *T  (or just T for a primitive type (say int, string))
}

cap字段是关于第一个维度data有多大的提示。随着键的数量增加或减少,可以替换(重新计算)?? 数组+片。data的数组由键值的hash索引。它的大小基于可能的哈希值的数量。键类型仅限于 Go 编译器理解的数据类型(可以哈希),并且是不可变的(比如数字或字符串)。MapEntry片保存键/值对,其中键散列为相同的值。

复制地图时,内部内容被复制,但不复制data内容。因此,映射的行为很像指针类型。

功能

在 Java 中,函数(称为方法)是一个只在源代码中使用的构造。它们不是运行时值。Java 有一个 Lambda 的概念,它看起来像一个函数文字,但在它的背后是语法糖,实际上是编译器编写的类的一个实例,有一个符合 Lambda 签名的方法(由@FunctionalInterface注释定义)。在 Go 中,函数是一个值,就像任何整数或字符串一样(例如,它的第一个类)。

Go 函数可以有 0+个参数,也可以有 0+个返回值。没有返回值的函数就像 Java 中的void方法。使用以下模式定义函数:

func <name>({<arg>{,<arg>}...}) {(<return>{,<return>}...)} {
      :
}

其中的形式为 {, }

如果只有一个未命名的返回值,这可以简化为

func <name>({<arg>{,<arg>}...}) <type> {
      :
}

如果没有返回值,这可以简化为

func <name>({<arg>{,<arg>}...}) {
      :
}

作者建议按照习惯命名所有返回值。这就创建了可以在函数内部赋值的局部变量,并允许所有的返回都不用任何值来表示。

例如:

func xxx(x, y, z int) (result int, err error) {
      :
      if err != nil {
            return
      }
      :
      result = x * y * z
      :
      return
}

其中return在任何地方都是相同的,使用返回变量的最后一次赋值。通常,这是创建返回变量时使用的零值。

一些 Gophers 建议不要使用这种模式,特别是对于那些显式返回值提供更多本地上下文的长函数。如果函数保持简短,这就不是问题了。即使命名的变量没有被使用(即return有值),它们也为代码创建了更好的自文档

前面的示例等效于

func xxx(x, y, z int) (int, error) {
      :
      err := ...
      if err != nil {
            return nil, err
      }
      :
      var result = x * y * z
      :
      return result, nil
}

函数的最后一个(或唯一一个)<arg>可能是这样的:

name... type

这意味着该参数可以重复零次或更多次。这被称为可变参数(或可变参数)。Java 也有类似的特性,例如:

void xxx(String... strings) { ... }

像在 Java 中一样,任何 vararg 参数都必须是最后一个参数。

在幕后,实现略有不同。

在 Java 中,vararg 作为声明类型的可能为空(如果没有提供参数)的数组传递。这意味着从方法的角度来看,下面的形式实际上是相同的:

void xxx(String[] strings) { ... }

在 Go 中,vararg 作为声明类型的非空片传递,如果没有提供参数,则作为nil传递。

有时,希望将切片的内容作为单个值发送给 vararg 参数。为此,请使用以下表格:

aStringSlice := []string{"123", "456", "789"}
xxx(aStringSlice...)

函数(而非方法)声明的另一种形式是

var <name> := func({<arg>{,<arg>}...}) {(<return>{,<return>}...)} {
      :
}

这创建了一个函数文字(通常也是一个闭包;我们将在本文后面更多地讨论闭包)。这里,函数没有名字;<name>只是一个设置为函数文本实例的变量。通过这种方式,这些函数文字(但不是声明/命名的函数)可以在函数体中使用的调用上下文中的所有局部变量上创建闭包。这在传递给defergo语句的函数中很常见。

在另一个函数中声明一个命名函数是不合法的,但允许使用函数文本,如下所示:

func xxx() {
      func yyy() {            // illegal
      }
      yyy := func() {            // legal
      }
}

这种通过赋值来定义函数的方式部分解释了5Go 相对于 Java 的一个限制。Go 和 C 一样,不允许重载函数。重载函数是一组不同函数中的一个,在相同的范围内,与另一个函数具有相同的名称但不同的参数。

对于 Java(或 C++)程序员来说,这可能是一个巨大的损失。不使用重载,必须使用(作者的术语)多重加载(有时也称为名字混淆),在这里函数名被调整(通常通过后缀)以使其唯一。例如,在 Java 中,可能会产生这些函数(忽略基于 Java 泛型的方法):

int max(int... values) { ... }
long max(long... values) { ... }
double max(double... values) { ... }
while in Go it would be (say):
func maxInt(values ...int32) int32 { ... }        // or maxInt32
func maxLong(values ...int64) int64 { ... }       // or maxInt64
func maxDouble(values ...float64) float64 { ... } // or maxFloat64

像在 Java 中一样,Go 有函数参数和返回值,它们通过值传递,这意味着它们被复制到函数中或从函数中复制出来。调用方看不到对函数中参数副本的更改。

对于较大的(比如数组)或复杂的(比如结构)数据类型,这种复制可能会很昂贵。通常,改为传递指向这些类型的指针。注意传递切片时,复制的是切片本身(很小),而不是支持数据数组(可以在切片之间共享),因此切片通常是按值传递的,除非它们是可选值。类似于 map 类型,除了很少通过指针传递它们,除非它们是作为函数输出创建的。

要在 Java 中传递可变数据,必须传入函数可以修改的对象(比如数组、集合或包装对象)。在 Go 中,除了传入一个指向某个可变对象的指针(相对于引用)之外,它是类似的。

请注意,Go map 类型总是以这种方式有效地传递;对传递到函数中的映射所做的任何更改都可以被函数的调用者看到。

在某些情况下,甚至有可能将命名变量设置为具有相同签名的不同函数。这种变量通常被称为函数适配器。这在一些语言中很常见,比如 JavaScript 。在 Java 中,最常见的是使用一个 @FunctionalInterface 类型的变量,并将 lambdas 赋给它们。

在 Go 中,通常函数签名被定义为支持这一点的类型。例如:

type MultFunction func(x... int) int

像这样使用:

func DoFunction(f MultFunction) int {
      return f(1)
}

其中函数(注意不是指向函数的指针)作为参数传递。

最重要的是,在 Go 中,函数实例可以传递给不同的函数和/或由不同的函数返回。这意味着函数在 Go 中是“第一类”(而在 Java 中不是)。这给了 Go 一个类似 Java 的特性。它还支持丰富的、类似函数式编程的功能来组合函数。

将函数作为参数或返回函数的函数称为“二阶函数”这可以(很少)扩展到“三阶函数”及以上。如果参数和返回都不是函数,那么函数就是“一阶的”

Go 功能可以通过以下方式退出:

  1. 返回一个结果——当设计不可能失败时。

  2. 返回成功或失败值-特殊值表示失败。

  3. 返回一个值和一个成功/失败布尔值——布尔值通常被命名为ok

  4. 返回值和可能的nil错误——通常是首选的错误报告机制;误差值通常被命名为err

  5. 引起恐慌——仅当问题罕见或由于编程不当(例如,被零除)时;恐慌被视为异常退出,如 Java Error 异常。

注意前面的结果本身可能是多个值。习惯上,任何特殊的布尔值或错误返回值,如果声明了,应该是最后一个。

作为函数的方法

在 Go 中,与类型相关联的函数被称为方法。使用以下模式将方法声明为带有特殊附加参数的函数:

func (<receiver>) <name>({<arg>{,<arg>}...}) {(<return>{,<return>}...)}  {
      :
}

其中<receiver>的形式为<name> <type>。只能有一个接收者。

请注意,这与

func <name>(<receiver>, {<arg>{,<arg>}...}) {(<return>{,<return>}...)}  {
      :
}

虽然合法,但不被认为是接收器类型的方法。这种形式更像是 Java 中的静态方法。

接收器类型是该函数作为其方法的关联类型。该方法可以看到该类型的任何字段,甚至是私有字段(假设该类型是 struct 类型)。请注意,这些方法不是在包含类型中声明的,而是独立的(甚至可能在同一包的不同源文件中;与 Java 截然不同)。例如:

type SomeStruct struct {
      x,y,z int
}

func (ss *SomeStruct) X() int {
      return ss.x
}
func (ss *SomeStruct) SetX(v int) {
      ss.x = v
}
func (ss *SomeStruct) Z() int {
      return ss.z
}
func (ss *SomeStruct) SetZ(v int) {
      ss.z = v
}

可以让公共访问私有值,就像 Java 访问方法一样。Go 较少使用访问方法;通常所有字段都是公开的。与 Java 不同,Go 对于这样的访问方法没有一致的命名约定。Java 遵循(isXxx|getXxx) / setXxx命名模式。一些地鼠使用Xxx/SetXxx模式。

<receiver>可以是直接接收者命名类型,也可以是指向接收者命名类型的指针(如前所述)。它不能直接是指针或接口类型。如果在方法中需要改变接收者,必须传递一个指针;否则可以传递一个值。

如果一个类型有多个方法,建议所有方法要么接受一个接收方,要么接受一个指向接收方的指针,而不是混合使用。作者建议总是为结构类型传递指针。

注意,在 Go 中,对接收者的 Java thissuper引用是不等价的。没有隐含使用this。必须始终使用显式名称(如在 Python 中)。在 Go 中,接收者的名字可以是任何东西。不像 Java 中那样限制为this。一些地鼠为此使用了名字self(或者me)(就像 Python 中的一样),但是更常见的是使用接收器类型名的缩写。

任何声明的类型都可以有自定义函数

基于基元类型的类型也可以有方法。这是与 Java 的一大区别,是一个强大的特性。例如,考虑一些众所周知的温度范围(可以相互转换):

type Celsius float64
func (c Celsius) ToFahrenheit() Fahrenheit {
      return Fahrenheit(c * RatioFahrenheitCelsius + 32)
}
func (c Celsius) String() string {
      return fmt.Sprintf("%GC", float64(c))
}

type Fahrenheit float64
func (f Fahrenheit) ToCelsius() Celsius {
      return Celsius((f - 32) / RatioFahrenheitCelsius )
}
func (f Fahrenheit) String() string {
      return fmt.Sprintf("%GF", float64(f))
}

通常对于新的类型,人们会提供一些常量,例如

const (
      FreezingPointFahrenheit Fahrenheit = 32.0
      BoilingPointFahrenheit Fahrenheit = 212.0
      FreezingPointCelsius Celsius = 0.0
      BoilingPointCelsius Celsius = 100.0
      RatioFahrenheitCelsius = 9.0/5.0
)

fmt包中有一个名为 Stringer 的标准接口,定义如下:

type Stringer interface {
      String() string
}

这个接口是 Go 对所有实现toString()的 Java 对象子类型的替代。在 Java 中,总是提供一个toString()方法(默认从对象继承而来);这在 Go 中是不成立的。fmt包提供的各种格式化器(比如Sprintf)可以格式化大多数类型,即使没有提供String()方法。“%#v”格式化程序将显示带标签的结构字段。

摄氏和华氏类型都隐式地实现了这个接口,因此是 Stringer 类型,可以在任何允许 Stringer 类型的地方使用。一个常见的例子是

fmt.Printf("Celsius: %s\n", Celsius(100.1))
which would print:Celsius: 100.1C

作为值的功能

在 Go 中,函数是值,就像数字和字符串一样,因此可以以类似的方式赋值和使用。每个函数都有一个调用该函数的内置操作。当函数值后跟一个调用操作符(括号)时,就会发生这种情况,例如

var identity = func(x int) int {
      return x
}
:
var fx = identity
var result = fx(1)  // fx followed by the call operator

这里,函数 literal 被间接调用。和调用identity(1)是一样的。

声明的函数类型可以有额外的动作。例如:

type MyFunction func(int) int
func (f MyFunction) Twice(x int) int {
      return f(x) + f(x)
}
:
var xf = MyFunction(identity)
var aCharm = xf.Twice(1) + xf(1)

有一个函数指针是可能的,但是很少,比如

var fp *MyFunction

var result = (*fp)(1)

请注意,调用带有nil值的函数变量会导致死机。

与 Java 不同,Go 具有可以返回多个值的函数,比如

type Returns3 func(int) (int, int, int)
type Takes3 func(x, y, z int) int
:
var f Takes3 = ...
var g Returns3 = ...

这些可以这样使用:

a, b, c := g(1)
result := f(a, b, c)

对于这种具有匹配的返回和参数计数和类型的函数组合,Go 有一个方便的快捷方式:

result := f(g(1))

Go 函数提供了一个 Java 中没有的特性。Go 函数文字可以充当闭包(一个代码块,它捕获在它之外定义的变量,只要这个代码块存在,这些变量就存在)。在 Java 中,只有只读变量可以用这种方式捕获,而在 Go 中可以捕获读写变量。

这是一个强大的特性,但是它有一个经常让开发人员出错的问题。考虑这个例子:

for _, v := range []int{1,2, ..., N} {
      go func() {
            fmt.Printf("v=%v\n", v)
      }()
}

这里,func()捕获了v变量。将打印哪些 N 值?

许多人会期望以某种明显随机的顺序打印值 1 到 N。虽然有可能,但这不太可能。

goroutine 内部的v变量与 for 循环中的v相同(在 Java 中,如果支持,但不支持,它将是外部v的副本),它被 goroutine 捕获并最终打印出来。

最有可能的情况(取决于 goroutine 实例相对于执行for循环的速度)是 N 次打印值 N。这可能是因为一些 goroutines 直到for循环结束后才开始,这是可能的。

对此问题有两种常见的修复方法:

  1. Send the v value as a goroutine parameter:

    for v := range []int{1,2,..., N} {
          go func(v int) {
                fmt.Printf("v=%v\n", v)
          }(v)
    }
    
    

    这里,v值在调用 goroutine 时(而不是运行时)被复制到参数中,因此它是当前迭代的值。请注意,goroutine 参数的名称可以不同(为了清楚起见,应该不同)。

  2. Send the v value as a distinct variable:

    for v := range []int{1,2,..., N} {
          vx := v
          go func() {
                fmt.Printf("v=%v\n", xv)
          }()
    }
    
    

    这里,v值在调用 goroutine 之前被复制到vx局部变量,因此xv是 goroutine 捕获的变量。因为xv是在 for 循环的每次迭代中创建的,所以它在每个 goroutine 实例的捕获中是一个不同的存储位置。

    Note that this subtle variant also works:

    for v := range []int{1,2,..., N} {
          v := v  // only change from original loop
          go func() {
                fmt.Printf("v=%v\n", v)
          }()
    }
    
    

这里,块内的新局部变量恰好与块外的变量同名。它隐藏了 for 循环的值。这是 Go 中常用的成语。

函数声明可能会省略主体。这表明该函数被定义为一个外来函数(在不同的语言中(通常是 C))。例如,用 C 编写的二分搜索法可以用下面的声明来访问:

// Search for float64 value v in float64 slice values.
// Values must be sorted.
// Returns the index or < 0 if not found.
func BinarySearchDouble(values []float64, v float64) int

编写这种外来函数的技术超出了本文的范围。参见 Go online 文档网站上的 CGo 工具。外来函数经常被用来访问一些用 C 编写的现有函数,比如数据库系统。CGo 函数必须专门编码来消费或返回 Go 数据类型,并在 Go 运行时环境中运行。

注意,单行函数可以用更简洁的方式输入。例如:

func square(x float64) float64 { return x * x }

这通常不是推荐的形式,除了传递给其他函数的匿名函数。例如:

type Floater func(x, y float64) float64
func DoFloater(f Floater, x, y float64) float64 {
      return f(x, y)
}

用作

var v = DoFloater(func(x, y float64) float64 { return x * y }, 2.5, 3)

这很像 Java 实现:

@FunctionalInterface public interface Floater {
  double op(double x, double y);
}

public double doFloater(Floater f, double x, double y) {
  return f.op(x, y);
}

用作

var v = doFloater((x, y) -> x * y , 2.5, 3);

或者更详细但等价的完整表示,更像 Go 版本:

var v = doFloater((double x, double y) -> { return x * y; }, 2.5, 3);

为了鼓励它们的使用,Java 为编写 lambda 调用提供了非常简洁的选项,包括方法引用。Go 往往不那么简洁。

结构

Go 和 Java 都支持多种(异构)类型的聚合。在 Java 中,这些集合是(在较小程度上是枚举)。在 Go 中,这些集合是结构。从概念上讲,类是支持完全面向对象编程(OOP)租户的结构的扩展。Go 没有 OOP 类的概念。

像类一样,结构由 0+个数据/状态字段和 0+个行为方法组成。结构不支持继承,但是一个结构可以包含另一个结构,所以支持直接(相对于 Java 支持的引用)组合。结构字段可以是任何类型,包括其他结构类型。

缺少 OOP 可能是 Go 和 Java 作为语言的最大区别。它对如何编写 Java 和 Go 程序有着深远的影响。Go 不是一种面向对象的语言,尽管许多专家声称它是。

这并不是说 Go 相对于 Java 有所欠缺。和 Java 不一样。基于你的观点,Go 对 Java 的 OOP 的替代方案要么是积极的,要么是消极的。例如,Go 的接口概念差异(例如,Duck Typing)可以提供比 Java 的接口更大的灵活性。

Go 具有允许程序员模拟许多 OOP 语言特性的特性,但是 OOP 不是语言本身的内置特性。Go 设计者认为一些 OOP 特性过于复杂(可能运行时效率低下),因此故意没有包括在内。

结构定义示例(定义为类型,这是典型的–在允许自定义类型名称的情况下,也允许文本结构):

type Person struct {
      Name string
      Address string
      Age float32
      Sex int
}
const(
      UndeclaredSex int = iota
      MaleSex
      FemaleSex
      TransgenderSex
)

前面定义的常量为Sex字段提供了特定的值。像这样使用常量是很常见的。前面的枚举(又名 iota 常量)集不是特定于类型的。可以创建特定类型的枚举,如清单 6-1 所示。

package main

import "fmt"

type FileSize uint64
const (
      B FileSize = 1 << (10 * iota)
      KiB
      MiB
      GiB
      TiB
      PiB
      EiB
)

var fsNames = [...]string{"EiB","PiB","TiB","GiB","MiB","KiB",""}
func (fs FileSize) scaleFs(scale FileSize, index int) string {
      return fmt.Sprintf("%d%v", FileSize(fs + scale / 2) / scale, fsNames[index])
}
func (fs FileSize) String() (r string) {
      switch {
      case fs >= EiB:
            r = fs.scaleFs(EiB, 0)
      case fs >= PiB:
            r = fs.scaleFs(PiB, 1)
      case fs >= TiB:
            r = fs.scaleFs(TiB, 2)
      case fs >= GiB:
            r = fs.scaleFs(GiB, 3)
      case fs >= MiB:
            r = fs.scaleFs(MiB, 4)
      case fs >= KiB:
            r = fs.scaleFs(KiB, 5)
      default:
            r = fs.scaleFs(1, 6)
      }
      return
}

func main() {
      var v1, v2 FileSize = 1_000_000, 2 * 1e9
      fmt.Printf("FS1: %v; FS2: %v\n", v1, v2)
}

Listing 6-1Format Enum Values

注意 Go 社区有库来生成这种枚举到字符串的函数。这可能非常方便。

运行时,它报告(四舍五入为整数):FS1: 977KiB; FS2: 2GiB

结构字段

在 Java 中,制作类字段private并提供访问它们的值的访问方法是很常见的。例如:

public class Data {
  private int value1, value2;
  public int getValue1() { return value1; }
  public void setValue1(int value1) { this.value1 = value1; }
  public int getValue2() { return value2; }
  public void setValue2(int value2) { this.value2 = value2; }
}

Go 没有这么强的命名约定,但是经常使用这种模式(省略 get on getter,前缀 setter with set ):

type Data struct {
       value1, value2 int
}
func (d *Data) Value1() int {
      return d.value1;
}
func (d *Data) SetValue1(value1 int) {
      d.value1 = value1
}
func (d *Data) Value2() int {
      return d.value2;
}
func (d *Data) SetValue2(value2 int) {
      d.value2 = value2
}

在 Go 中,将 struct 字段设置为公共字段比将其设置为私有字段并提供访问方法更常见,这在 Java 中很常见。只有当需要一些额外的行为(比如 setters 中的值验证)时,才使用这种方法。一些地鼠考虑总是将字段设为私有,并像 Java 一样使用访问方法。有时,甚至包含这些字段的类型也是私有的。例如,请参见这个记录在案的用例 6 (一个返回的匿名类型),用于具有公共方法的私有结构。当然,在接口类型中,访问方法是必需的,因为不能有数据字段。

这反映了 C(所有的 struct 字段总是公共的)对 Go 的强烈影响。结构不是 Java 意义上的类。因此,它们不像类那样提供相同程度的封装(或数据隐藏)。

给定定义:

type X struct {
      : some fields
}
func (x X) value(arg int) int {
      return arg
}
func (px *X) pointer(arg int) int {
      return arg
}
:
var x  X
var p *X

函数表达式X.value产生一个类型为func(x X, arg int) int的结果。

这种函数可以通过几种方式调用:

  • x.value(1)                   // like a Java instance method

  • X.value(x, 1)                // like a Java static method

  • var f = X.value; f(x, 1)     // as a function value

  • var f = x.value; f(1)        // similar to above

在最后一种情况下,f变量是一个绑定到实例的函数。这叫做方法值。这样的价值是一流的。这有点像柯里 7 的功能。

类似地,函数表达式(* X).pointer产生一个类型为func(px *X, arg int) int的函数。

这种函数可以通过几种方式调用:

  • (*p).pointer(1) // like a Java instance method

  • X.pointer(p, 1) // like a Java static method

任何结构字段都可以有标签。标签是附加到字段的字符串,通常(但不是必须)表示一组键和字符串值。这种格式化的标签是代码的元数据,它反射性地查看结构(在运行时)。标签是 Java 提供的一种简单的注释形式。标签的含义完全取决于进行字段自省的代码。标签的一般形式是

<name>:"<CSL>"...

或者

<name>:"<string>",...

一个更完整的例子:

StructField string `json:"aField" gorm:"varchar,maxLength:100"`

或者

StructField string `json:"aField" gorm:"varchar","maxLength:100"`

注意 Go 反射库有帮助函数来访问和解析标签。

这里有两个标记集,一个用于 JSON 处理器,一个用于 GORM 处理器。由处理器决定是否拥有唯一的键(不幸的是,冲突是可能的,尤其是对于短键)。请注意,使用原始字符串可以更容易地输入带有嵌套引用文本的标签。

如果按位置排列的元素可比较,则相同结构的结构可以比较(==!=>>=<<=)。如果这样做,字段的顺序就变得很重要。

结构文字

结构文字是可能的。考虑这些例子:

p := Person{Name:"Barry", Address: "123 Fourth St.", Sex: MaleSex, Age:25.5}

或者

p := Person{"Barry", "123 Fourth St.", 20.5, MaleSex}

请注意,虽然命名值可以提供给结构文字元素,但不能提供给函数参数。

当字段名存在时,所有字段名都必须有名称,但是可以按任何顺序输入。当不存在名称时,值按结构中定义的顺序赋值。任何省略的字段都有零值。请注意,在结构中添加、移除或重新排序字段会破坏任何此类位置结构文本,而添加或移除字段会破坏命名结构文本。

如果Person类有一个合适的构造函数,类似的事情可以在 Java 中完成。没有与字段名驱动的初始化等效的方法:

var p = new Person("Barry", "123 Fourth St.", 25.5, MaleSex);

上例的 Go 对应物是

p := &Person{"Barry", "123 Fourth St.", 25.5, MaleSex}

嵌套结构

Java 支持其他类型定义中的嵌套类型定义,比如其他类中的类。Go 不允许这样。Java 允许类字段引用类,包括类本身。Go 允许使用指向结构类型的指针来实现这一点。例如:

type Node struct {
      Value interface{}
      Next, Previous *Node
      Parent *Node
      Children []*Node  // maybe *[]*Node to make Children optional
}
var head *Node

一个结构可以嵌入另一个结构(作为定义的类型或结构文本)。这可以有也可以没有字段名。这与任何其他标量类型一样;该结构的字段被嵌入(可能递归地)到嵌入结构中。结构不能直接或通过传递嵌入自身。

如果没有提供字段名,就好像嵌入结构字段被复制到嵌入结构中。如果多个嵌入结构包含同名字段,这可能会导致问题。这可以通过使用嵌入的类型名作为限定符来解决。因此,如果没有标签,同一类型不能被嵌入多次。

在 Java 中,标准的对象方法是由运行时实现的。Go 没有这样的自动实现,但是根据字段类型,可以使用==(和!=)操作符。

Java 16 最近引入了一个record类型,除了实例是不可变的之外,它类似于 Go 结构。记录的行为类似于经典的元组 8 类型(一个通常不可变的异构固定大小的值集合)。一个例子是

public record Point(int x, int y) { }
:
var r = new Point(0, 0);
int x = r.x(), y = r.y();
System.out.printf("Point r: " + r + " " + x + " " + y);

这产生了

Point r: Point[x=0, y=0] 0 0

考虑 Go 等效物:

type Point struct {
      X, Y int
}

func (p *Point) String() string {
      return fmt.Sprintf("Point(%d  %d)", p.X, p.Y)
}

它是这样使用的:

var r = Point{0, 0}  // or &Point{0, 0}
x, y := r.X, r.Y
fmt.Println("Point r:", r, x, y)

这产生了

Point r: {0 0} 0 0

注意在 Java 中,适当的toString方法是由编译器生成的。在 Go 中,必须显式创建。还要注意,这些字段是公共的,因此不需要访问方法。

结构字段对齐

在 Java 中,人们无法知道类字段在内存中的排列顺序(JIT 可以选择任何顺序)。Go 结构通常按照字段声明的顺序在内存中布局。每种类型都有自己的对齐需求;通常基于字节数,该类型占用 16 字节的边界。因此,最好先放置较大的字段;否则,结构中可能会有内部间隙来重新建立对准。您可以使用特定大小的空白(名称为“_”)字段强制对齐。例如:

type Xxx struct {
      i1 int64
      b int8
      _ [3]byte  // add 3 bytes
      i2 int16
      _ [2]byte  // add 2 bytes
      i3 int32
}

这些校准在表 6-4 中定义。

表 6-4

结构字段的对齐

|

类型

|

大小(字节)

|
| --- | --- |
| byte, uint8, int8, bool | one |
| uint16, int16 | Two |
| uint32, int32, float32, rune | four |
| uint64, int64, float64, complex64 | eight |
| complex128 | Sixteen |
| int, uint | 基于机器架构的 4 或 8 |
| *<any type> | 基于机器架构的 4 或 8 |

注意大多数现代计算机使用 64 位(8 字节)字,所以intuint和指针的长度都是 8 字节。

您还可以对字段进行排序,使较大的字段(根据用于表示它们的字节数)排在前面。

接口

Go 和 Java 都支持接口,但它们支持的方式不同。这是 Go 和 Java 的一个关键区别。

在 Java 中,接口是一个抽象方法签名的集合(某些 Java 8+扩展被忽略),任何具体的类都必须实现,如果该类实现(符合)接口的话。在 Go 中也是如此,除了任何类型(不仅仅是结构)都可以实现(符合)接口方法。

最大的区别是在 Go 中没有必要正式声明类型实现接口;任何拥有接口所有方法的类型(通过签名)隐式实现(或者经常被 Gophers 称为满足;本文将使用 implements 作为 Java 术语)接口。事实上,接口可以(并且经常)在类型被创建之后创建,并且类型仍然可以符合接口。

这就是通常所说的“鸭子”(如果它走路像鸭子,叫声像鸭子,那它就是鸭子)打字。换句话说,决定类型的是对象的行为,而不是状态。如果一个对象实现了某种类型的行为,它可以被认为是该类型的一个实例。一般来说,Go 方法比 Java 方法更灵活。

在 Java 中,可以声明任何类类型来实现任意数量的接口。如果一个 Java 类没有实现它声明的所有接口方法,那么它必须被声明为abstract。任何接口都可以被另一个接口扩展,这增加了扩展接口的方法。

在 Java 中,所有的接口都是abstract。Go 也是如此。Go 的规则不一样。在 Go 中,如果一个接口的所有方法都没有被某个类型实现,那么这个类型就是没有实现这个接口。句号!反之亦然;如果实现了接口的所有方法,则该类型确实实现了接口。句号!

Java 接口调用由 JVM 指令invoke interface9实现。这个指令决定了哪个具体的方法在实际的接收者对象上实现接口签名,并调用它。在 Go 中,使用了类似的过程。接口类型的任何变量在概念上(而不是实际上;不合法的 Go)在运行时由如下结构表示:

type InterfaceValue struct {
      Type  *type   // nil or current runtime concrete type
      Value *value  // nil or current runtime value (of type *type)
}

因此,实际上所有的接口类型都是指针类型的形式(因此可以设置为指针值或者编译器自动获取地址的非指针值)。任何引用都是通过Value指针间接引用的。任何类型断言都会测试Type值。这些字段在每次分配给接口变量时设置。只有符合接口类型的类型才能赋给变量。

由于前面的设计,应该很少将变量或类型声明为指向接口类型的指针。接口类型的行为很像一个隐式指针。

实际上,在每一次给接口类型的变量赋值时,都会创建一个调度表,该调度表由接口类型的方法进行索引。指向分配类型中匹配函数的指针被设置到这个调度表中,因此可以通过它们的索引而不是名称来调用它们(这样快得多)。

注意,除了赋值给接口的函数之外,赋值类型可能还有许多其他函数。

通常,这个调度表是延迟创建的,因此,如果没有对该值调用接口方法,就不会生成调度表。它也可以被缓存以获得更好的性能。这个过程会使任何初始的方法调用变得有点慢。

前面的结构允许一个接口值有两种形式的nil值,每种形式都有细微的不同行为:

  1. 类型和值字段都是nil(或未设置)——典型情况。

  2. 如果类型是某种指针类型,只有值字段是nil

如果你做出这样的声明:

var xxx interface{}  // zero value is nil

你接第一个案子。但是如果你做一个任务,比如

var data *int  // zero value is nil
xxx = data     // dynamic type is now *int; dynamic value is nil

你接第二个案子。

考虑这些定义:

type Xxx interface {
    DoIt()
}

type XxxImpl struct {
}
func (xi *XxxImpl) DoIt() {
      :
}
:
func DoXxx(xxx Xxx) {
      if xxx != nil {
            xxx.DoIt()
      }
}

在前面的例子中,DoXxx函数可以接收一个为nil的 Xxx 实例或者一个有类型但有nil值的实例。if测试将只检测第一种情况,但是在第二种情况下,使用该值调用接口的方法可能会失败,并出现运行时死机。

接口的应用

在 Go 中,接口通常只有几个(通常只有一个)方法。这类似于 Java 中的@FunctionalInterface注释,但按照惯例。当只有一个方法并且它的名字是(比方说)Xxx时,那么这个接口通常被命名为Xxxer。例如:

type Stringer interface {
      String() string
}

type Reader interface {
      Read(n int) []byte
}

可能有多个接口具有相同的函数签名。例如:

type ToString interface {
      String() string
}

任何提供String()方法的类型都会实现所有这样的接口。

Go 运行时库广泛地定义和使用了这样的接口。许多 Go 类型实现了多个这样的接口。这些类型的实例可以发送给任何接受这样一个接口作为参数或返回该接口的函数。

例如,Go File类型实现了读取器写入器接口(以及其他接口),因此文件实例本身可以直接用于访问文件的内容。在 Java 中,必须在文件实例上使用单独的访问类(实现 Java ReaderWriter 接口)。

示例接口(定义为类型,这是典型的,也允许文字接口):

type Worker interface {
      RecordHoursWorked(hours float32) (err error)
      GetPay() (pay Payment, err error)
}

type Payment float32

注意,在 Go 中,一个接口只能包含方法原型(抽象方法的签名)。不能有字段、带体的方法或嵌套类型。这就是为什么原型没有前缀“func”的原因。

另请注意,参数和返回名称是可选的,如下所示。该定义含义相同,但不太清楚,因此不推荐使用:

type Worker interface {
      RecordHoursWorked(float32) error
      GetPay() (Payment, error)
}

在 Go 中,任何在一个接口中定义了所有方法的类型都会隐式实现该接口。不像在 Java 中,不需要implements子句。例如:

type HourlyWorker struct {
      HoursWorked    float32
      Overtime       float32
      Rate           float32
}
func (hw * HourlyWorker) RecordHoursWorked(worked float32) (err error) {
      hw.HoursWorked += worked
      return
}
func (hw * HourlyWorker) GetPay() (pay Payment, err error) {
      reg := hw.HoursWorked * hw.Rate
      ot := hw.Overtime * 1.5 * hw.Rate
      if hw.Overtime > 20 {
            ot = 20 * 1.5 * hw.Rate + (hw.Overtime - 20) * 2 * hw.Rate
      }
      pay = Payment(reg + ot)
      return
}
:
var worker Worker = &HourlyWorker{40.0, 10.5, 15.50}
var pay, err := worker.GetPay()
:

接口类型的一个特例是空接口(即它没有方法)。看起来是这样的:

interface{}

这意味着为了符合接口,您根本不需要实现任何方法。这是所有类型都可以做的事情。因此,空接口的行为类似于通用类型(很像 Java 中的对象充当通用引用类型)。任何类型,包括基本类型,都可以分配给空接口。

例如:

var x interface{}
x = 10                         // value 10 (no boxing needed as Java does)
x = "hello"                    // value "hello"
x = make([]string, 0, 10)      // an empty slice

通常,空接口类型用于可变参数,其中可以为每个参数传递不同的类型,如以下签名所示:

func Printf(format string, args... interface{})

请注意,空接口可以是不同的类型。下面的类型anyall是不同的类型:

type any interface{}
type all interface{}

复合界面

在 Go 中,就像在 Java 中一样,可以用其他接口来构建接口。在 Java 中,这是由extends完成的。在 Go 中,这是通过嵌入接口来实现的。例如,在 Java 中

public interface A {
  void a();
}
public interface B {
  void b();
}
public interface C {
  void c();
}
public interface All extends A, B, C {
  void all();
}

在 Go 中会是这样的:

type A interface {
      A()
}
type B interface {
      B()
}
type C interface {
      C()
}
type All interface {
      A
      B
      C
      All()
}

接口All有这些方法:A()B()C()All()。接口不能直接或间接嵌入自身。与 Java 一样,在 Go 中,如果多个嵌入接口有相同的方法(通过名称、参数和返回),那么嵌入接口只有一个方法。如果嵌入的方法具有相同的名称但不同的签名,则会发生编译时错误。

如果动态类型是可比较的,相同动态类型的接口的实例可以被比较(==,!=>>=<<=)。

Go's duck typing 的一个强大方面就是这个例子。给定:

type XBytes []byte

func (x *XBytes) Write(bs []byte) (n int, err error) {
      n = len(bs)
      *x = XBytes(append([]byte(*x), bs...))
      return
}

然后

var b = make(XBytes, 0, 100)
xb := &b
xb.Write([]byte("hello - "))
fmt.Fprintf(xb, "this is a %s", "test")
fmt.Printf("%q\n", *xb)

在定制的XBytes类型上工作,因为它实现了io.Writer接口。

它产生"hello - this is a test"

通道

Go 有一个通道类型,Java 没有与之对应的标准。频道与 Goroutines 相结合,是 Go 最有特色、最强大的功能之一。

通道基本上是一个有序的管道或先进先出(FIFO)队列,通过它可以发送或接收值。发送方和接收方通常运行在不同的 goroutines 中,因此彼此是异步的。一个通道只能保存一种类型的数据(但是由于它可以是一个结构,数据可以是复杂的,或者由于它可以是一个接口,数据可以是多态的)。通道可以用作单向或双向。通道声明如下:

chan <type>        // bidirectional (receive and send)
chan <type> <- // receive only
chan <- <type>  // send only

其中是任意 Go 类型(但是通道的通道很少)。

使用make构建通道,如下所示:

<chanVar> = make(<chan declaration>{, <cap>})

其中<cap>是信道的容量。默认值为零。容量决定了有多少发送的项目可以被缓冲等待接收。容量为零意味着没有缓冲;发送者和接收者在锁定步骤中操作。对于一个通道,cap()函数返回这个容量,len()函数返回当前缓冲的计数。

一些通道定义示例:

var ichan = make(chan int)
var schan = make(chan string)
var roichan = make(chan <- int)
var woichan = make(<- chan int)
type Celsius float64
var cchan = make(chan Celsius)

通道可以是开放的,也可以是封闭的。当通道打开时,可以将更多的值发送到通道。关闭时,它们不能。接收(<-)操作员可以测试通道是否打开。close()功能用于关闭一个通道。

读取通道直到它们被关闭是很常见的。这最容易做到,如下所示:

for <value> := range <channel> {
      : process next value from the channel
}

前面的逻辑通常放在某个 goroutine 中。如果信道当前为空,则循环阻塞。循环继续,直到某个发送方关闭通道。

通道可以用来帮助支持通用的请求处理器。例如,参见清单 6-2 中所示的程序。

package main

import (
      "fmt"
      "time"
)

type RequestFunc func(arg interface{}) interface{}

type GenericRequest struct {
      Handler     RequestFunc
      Args        interface{}
      Result      chan interface{}
}

func NewGenericRequest(h RequestFunc, a interface{},
      r chan interface{}) (gr * GenericRequest) {
      gr = &GenericRequest{h, a, r}
      if gr.Result == nil {
            gr.Result = make(chan interface{})
      }
      return
}

func HandleGenericRequests(requests chan *GenericRequest) {
      for req := range requests {
            req := req
            go func() {
                  req.Result <- req.Handler(req.Args)
            }()
      }
}

var Requests = make(chan *GenericRequest, 100)

func sumFloat(arg interface{}) interface{} {
      var res float64
      values, ok := arg.([]float64)
      if ok {
            for _, v := range values {
                  res += v
            }
      }
      return res
}

func main() {
      reqs := make([]*GenericRequest, 0, 10)
      reqs = append(reqs, NewGenericRequest(sumFloat, []float64{1, 2, 3}, nil))
      reqs = append(reqs, NewGenericRequest(sumFloat, []float64{5, 6, 7}, nil))
      reqs = append(reqs, NewGenericRequest(sumFloat, []float64{7, 8, 9}, nil))
      for _, r := range reqs {
            // accepts < 100  requests without blocking
            Requests <- r
      }
      go HandleGenericRequests(Requests)

      time.Sleep(5 * time.Second)  // simulate doing other work

      for i, r := range reqs {
            fmt.Printf("sum %d: %v\n", i+1, <-r.Result) // wait for each to finish
      }
      close(Requests)
}

Listing 6-2Request Handler and Summation Action

这在运行时产生

sum 1: 6
sum 2: 18
sum 3: 24

通过改变输入值和匹配函数,可以同时进行任何计算。

变量声明

像 Java 一样,Go 允许程序员声明不同类型的变量。Java 放置类型(intStringList等)。)作为语句引入者。Go 对声明有特殊的语句。在 Go 中,变量声明使用 var 语句,很像最近的 Java 10+版本对局部变量的声明。在 Go 中,类型跟在变量名后面。以下是一些例子:

var x int                   // x is int, value 0
var x, y, z int             // x, y, z are int, values all 0
var x, y, z int = 1, 2, 3   // x, y, z are int, corresponding value 1, 2, 3
var x, y, z = 1, 2, 3       // x, y, z are int (implied by values), corresponding value 1, 2, 3
var z *int = &x             // z is address of x (or var x = &x)

“风险值”可以被剔除。例如:

var (
      x int                 // x is int, value 0
      z *int = &x           // z is address of x
)

在 Go 中,如果可以通过任何提供的值推断出类型,则该类型是可选的。在 Go 中,如果省略赋值,则使用“零”值。对于布尔值来说,零值是false,对于数字来说是 0,对于字符串来说是空字符串,对于指针来说是nil指针。对于切片、贴图和通道,零值类似于nil,但其行为并不完全类似于nil。虽然它们具有零值,但作者建议不要依赖于切片、贴图或通道的零值;相反,总是明确地make他们。

Java 对于方法或块局部变量也有类似的形式,比如

var x = 10;
var s = "string";
for (var i = 0; i < 10; i++)  {...}

像在 Java 中一样,Go 的变量名(即标识符)区分大小写,通常(但不是必须)使用 camel 大小写(每个单词都有一个大写字母)。在 Go 中,不鼓励在名称中使用下划线(“_”),即使是常量。此外,不鼓励只使用大写字母(如XXX_YYY)。下划线(_)本身用于命名一个可以忽略的特殊的blank变量。

在 Go 中,声明没有可见性修饰符。相反,标识符的大小写决定了可见性。如果标识符以大写字母开头,则它是公共的(任何包中的代码都可以看到);否则标识符是包私有的(只能被同一个包中的代码看到)。这种区别对于块局部声明并不重要;首选私有样式名称。

例如:

  • T - public

  • t – private

  • Bird - public

  • aFriendOfAFriend - private

与 Java 不同,嵌入式首字母缩略词通常都是大写字母(大小写混合)。例如:

MyURLPath (vs. MyUrlPath)

因此,使用表 6-5 中所示的命名惯例。

表 6-5

情景名称案例示例

|

作用

|

规则

|

例子

|
| --- | --- | --- |
| 私有类型名 | 卡米洛宫殿 | myType |
| 公共类型名 | 卡米洛宫殿 | MyType |
| 私有字段名称 | 卡米洛宫殿 | myField |
| 公共字段名称 | 卡米洛宫殿 | MyField |
| 私有顶级名称(var、const、func) | 卡米洛宫殿 | packagePrivateValue |
| 公共顶级名称(var、const、func) | 卡米洛宫殿 | PackagePublicValue |
| 函数/块/参数/返回任何角色的本地名称 | 卡米洛宫殿 | aLocalValue |
| 包名 | 一个短单词(或“/”分隔的单词) | fmt``net/http |

虽然不是一个语言标准,但有一些关于创建名称的推荐软规则。名称需要信息丰富且不言自明,尤其是类型、字段和函数名称。它们是关于代码描述的主要形式,因此,人们应该专注于确定的、通常是多词的名称,而不是使用等价的注释。

全局名称比本地名称需要更严格的定义(因此通常也更长)。避免使用缩写(如 len 代表长度),但使用众所周知的缩写是合适的。本地名称可以更简短(长的本地名称会使阅读代码更加困难)。在这里,缩写可能更合适——例如,一个名为 len、fmt、ctx 或 err 的变量。

有限上下文的名称,比如循环索引,通常很短,比如一个字母,或者(全部小写)缩写。对于循环索引,由于历史原因, 10 通常使用从“I”开始的单个字母,嵌套循环使用下一个字母,比如“j”。

常量声明

像 Java 一样,Go 允许程序员声明不同类型的常量。在 Go 中,常量是编译时的构造(像 Java 中的文字),而在 Java 中,它最接近运行时存在的static final值(在某个类中)。常量声明类似于变量声明,只是使用了关键字const并且需要一个值。例如:

const x, y, z int = 1, 2, 3    // x, y, z are int, corresponding value 1, 2, 3
const x, y, z = 1, 2, 3        // x, y, z are int (implied by values), corresponding value 1, 2, 3

“常数”可以被分解掉。例如:

const (
      x, y, z = 1, 2, 3        // x, y, z are int (implied by values), corresponding value 1, 2, 3
)

值表达式必须是常量(即编译器可以对其求值)表达式。每个值可以是不同的类型。

一个特例是为枚举集(enums)定义标识符。与 Java 不同,Go 枚举类型不是特殊类型,应该是整数。使用“iota”值,其从先前值被分配了增量值(从 0 开始):

const (
      v1 = iota      // 0
      v2 = iota      // 1
      v3 = iota      // 2
)

或者更简短:

const (
      v1 = iota      // 0
      v2             // 1
      v3             // 2
)

其中该值暗示为前一个值的重复。该值可以是一个表达式,例如创建位掩码:

const (
      bit0mask = 1 << iota       // 1
      bit1mask                   // 2
      bit2mask                   // 4
)

前面的形式(带有隐含值)是

const (
      bit0mask = 1 << iota       // 1
      bit1mask = 1 << iota       // 2
      bit2mask = 1 << iota       // 4
)

为每个const组重置iota值。

另一个例子:

const (
      Sunday = iota
      Monday
      Tuesday
      Wednesday
      Thursday
      Friday
      Saturday
)
const (
      FirstDay  = Sunday
      HumpDay   = Wednesday
      FunDay    = Saturday
      LastDay   = Saturday
)

这些值可以用

fmt.Fprintf(os.Stdout, "%v %v %v %v %v %v %v %v %v\n",
Sunday, FirstDay, Monday, Wednesday, HumpDay, Friday, Saturday, FunDay, LastDay)

这产生了

0 0 1 3 3 5 6 6 6

铅字铸造

在 Java 中,大多数操作,比如赋值和比较,不能跨不同类型完成。只允许兼容的类型。Java 允许自动提升选择类型,以使它们在算术、比较和/或赋值方面兼容,如下所示:

  • byte -> short -> int -> long

  • byte -> char -> int -> long

  • float-> double

  • long -> double

  • int -> double

  • subclass -> superclass

  • sub interface -> super interface

  • class -> implemented interface

其他转换需要显式转换。除了数值类型转换,转换不会改变值的实际类型,只是改变它的外观。数值转换可能会改变数据表示,如下所示:

double d = (double)anInt;

但是下面的转换不会改变数据:

Object o = (Object)aString;

事实上,这些类型转换(称为向上转换)永远不需要显式编码,因为 Java 编译器可以暗示这一点。但是反向操作(称为向下转换)必须显式完成,因为编译器认为它不安全,不会自动完成:

String s = (String)anObject;

这就像从 double 转换回 int,其中数据(例如小数部分)可能会丢失:

int i = (int)aDouble;

Go 有类似的强制转换(称为转换)策略,只是所有不同的类型都必须显式强制转换;几乎没有自动铸造。所以,对于数值,我们可以这样做

i := int64(aDouble)
d := float64(anInt)  // this is automatic in Java

注转换常数会赋予它一个类型。例如:

var i = int32(1)  // i is of type int32 while 1 has no type

一些有趣的特例:

var s1 = string(123)                    // s is string "123"
var s2 = string([]byte{48, 49, 50})     // s is string "123"
var s3 = []byte("123")                  // s is []byte{48, 49, 50}

类型测试

像在 Java 中一样,对于除接口类型之外的任何转换源类型,Go 可以在编译时确定这种转换是否合法。要将接口(抽象)类型转换为非接口(具体)类型,需要一个转换表达式。在 Java 中,这采用以下形式:

var x = (<concreteType>)<value>;

在 Go 中,它采用以下形式,称为类型断言:

x := <value>.(<concreteType>)

其中<value>是某个接口类型的表达式,而<concreteType>是期望的类型。例如:

aString := couldBeAString.(string)

如果这有效,aString是一个类型为string的值。但是如果couldBeAString不是一个string值,就会引发恐慌(就像 Java 的 ClassCastException )。为了克服这一点,我们在 Java 中执行以下操作:

if(couldBeAString instanceof String) {
      var aString = (String)couldBeAString;
      // use aString
}

在 Go 中,一个人可以

aString, ok := couldBeAString.(string)
if ok {
      // use aString
}

或者更简洁、更地道地说:

if aString, ok := couldBeAString.(string); ok {
      // use aString
}

像在 Java 的instanceof测试中一样,对nil值的类型断言总是false

Go Switch 语句也可以实现这种逻辑,并且比使用 if 语句进行多类型测试更简洁:

switch <expr>.(type) {  // here type is a keyword, not a variable
  case <type> {, <type>}...:
    <statements>
  default:
    <statements>
}

其中<type>是目标类型。也可以是值nil

有一个有用的扩展:

switch <v> := <expr>.(type) {
  case <type> {, <type>}...:
    <statements>
  default:
    <statements>
}

其中<v>是转换后的值,在每种情况下,它将是匹配事例类型的值。如果每个案例列出多个类型,则铸造类型为interface{};否则,它就是案例类型。例如:

func DoSomething(v interface{}) (err error) {
      switch xv := v.(type) {
      case string:
            : process string value xv
            return
      case int:
            : process int value xv
            return
            :
      case nil:
            return errors.New("nil not supported")
      default:
            return errors.New(fmt.Sprintf("type %T not supported", v))
      }
}

注意 Java 的新的switch表达式提供了 like 转换,而没有使用前面显示的instanceof

测试类型时,需要注意的是指针类型(*T)和非指针类型(T)被认为是不同的类型,必须有自己的case测试。

派生类型声明

Go 支持 Java 中没有的功能。最接近的 Java 特性是使用继承来声明子类型。在 Go 中,新类型可以从其他(称为 base )类型中派生出来。这些新类型没有为基类型设置别名;每个派生类型都是不同的类型,即使派生自相同的基类型也是如此(因此没有强制转换就无法进行赋值)。甚至可以从派生类型派生。例如,我们可以定义一些温度类型:

type Temperature float64
type Celsius Temperature
type Fahrenheit Temperature
type Kelvin Temperature

我们还可以定义派生类型,如

type anything interface {}

或者

type Person struct {
      :
}

注在 Java 中,类型通常以大写字母开头。这在 Go 中并不总是正确的;与所有标识符一样,只有当定义的类型是公共的时,才使用首字母大写。

Go 不支持继承(Java 中的extendsimplements)。最接近的对等词是作文。例如,给定

type Address struct {
      city  string
      state string
      zip   string
}
func (a *Address) Format() string {
      return fmt.Sprintf("%s\n%s, %s", a.city, a.state, a.zip)
}

那么可以如下使用这种类型:

type Person struct {
      :
      Address
      :
}

这意味着 Address 实例(及其所有状态)直接嵌入到 Person 实例中(很像 Java 超类的字段存在于任何 Java 子类中)。此外,地址拥有的任何方法都隐式地添加到任何 Person(如果还没有为 Person 定义)。这些方法亲自将工作委托给地址类型的方法。因此,这是可行的:

p := Person{...}
x := p.city
f := p.Format()

这也适用于将嵌入类型用作限定符的情况:

p := Person{...}
x := p.Address.city
f := p.Address.Format()

这是实际的形式;前一个是方便的简化。

考虑这个选项:

type Person struct {
      :
      address Address
      :
}

这意味着地址实例(及其所有状态)作为字段address嵌入。因此,这是可行的(此处需要address):

p := Person{...}
x := p.address.city
f := p.address.Format()

但是考虑一下这个:

type Person struct {
      :
      address *Address
      :
}

其中地址实例由字段address指向。一个人没有嵌入地址实例的任何数据,只有实例的地址。这仍然有效:

p := Person{...}
x := p.address.city
f := p.address.Format()

因为这里 p .address像表达式*(p.address)一样被隐式处理。

一般来说,作者建议使用指向结构的指针而不是嵌入结构来构造复合类型。这模仿了 Java 中通过组合(相对于继承)来完成的方式。只有当嵌入类型不能脱离嵌入类型而存在时,才应该使用物理嵌入。

与 Java 不同,Go 没有嵌入式命名类型的方法继承。但是如果类型是匿名嵌入的(没有名字),如前所述,如果嵌入类型还没有这样的方法,那么嵌入类型的方法将被复制到嵌入类型中。这被称为对嵌入式方法的委托

重要的是要注意,当调用嵌入结构的方法时,即使是通过嵌入结构的委托,接收方也是嵌入结构,而不是嵌入结构,如果嵌入结构继承了嵌入结构(即,只有一个对象,而不是两个对象,一个在另一个里面),就像 Java 继承一样。

同样重要的是要注意,嵌入结构不知道嵌入结构的存在。此外,当给定字段名时,可以获取嵌入结构的地址,并使用该地址独立于嵌入器来操作嵌入结构。

当多个结构被匿名嵌入时,它们有可能都具有相同名称的字段(如果同一结构类型被多次匿名嵌入,这总是正确的,所以这是不允许的)。如果是这样,这个问题必须解决。必须在同名的嵌入结构中声明字段,以解析(并隐藏嵌入的名称)名称。或者字段的引用必须由嵌入的类型名限定。方法也是如此。只有当字段或方法被某些代码使用时,这才是一个问题。

七、错误和恐慌

在这一章中,我们将深入探讨 Go 的错误检测和恢复特性,以及它们与 Java 方法的不同之处。当你完成了这一章,你应该能够清楚地识别 Go 和 Java 错误方法之间的相似和不同之处。

代码,尤其是函数中的代码,可以通过几种方式退出:

  1. 成功-功能如预期完成。

  2. 失败——由于某种可预见的情况,功能没有按预期完成。

  3. 严重故障(又名死机)——由于一些意外或异常情况或错误代码,功能没有完成。

在像 Java 这样的每个函数只有一个返回值的语言中,情况 1 和情况 2 通常是结合在一起的,由返回值本身来决定。考虑一下String.indexOf函数,它返回目标的索引或者值< 0 来表示没有找到目标。对于返回对象的函数,通常会返回null来表示失败(如果null是合法的值,这就有问题了)。这通常是许多 NullPointerExceptions 的原因。

Go 错误

Go 函数可以返回零个或多个结果。许多 Go 函数(至少)会返回一个错误值。这是一个常见的例子:

func DoSomething() (err error) { ... }

这表示 DoSomething 函数可以返回一个error(一个内置的 Go 接口类型),在这种情况下(按照惯例和习惯用法)命名为errerr值可以是nil或某个error实例。一个更完整的例子:

func DoSomething() (err error) {
     :
     err = DoSomePart()
     if err != nil {
          return
     }
     :
     return
}

Go 有一种常用的、不太冗长的方法来编码这种模式,它结合了赋值和 if 测试:

if err = DoSomePart(); err != nil {
     return
}

每个可能失败的函数都遵循这种模式。虽然比利用异常来报告故障的典型 Java 代码冗长得多,但这遵循了 Go 使用的更透明/明显的风格。

请注意,返回没有显式值。这是可行的,因为返回值被命名为err并且err被赋值。另一种选择(作者不太喜欢)是

func DoSomething() error {
     :
     xxx := DoSomePart()  // unconventional name
     if xxx != nil {
          return xxx           // explicitly returned
     }
     :
     return xxx
}

在大多数情况下,Go 更喜欢从函数中返回一个错误值。这种模式在 Java 中通常被认为是不好的做法,因为它迫使调用者测试返回的错误。在 Go 中,这种模式被认为是最佳实践;程序员必须记住测试返回的错误。这是 Go 和 Java 编程风格之间的一个主要区别,许多刚开始用 Go 编程的 Java 程序员在习惯上有一些困难。

对于一些简单的函数,只需要一个成功/失败指示器就足够了,返回的错误值由一个布尔值代替。这通常是 Go 内置操作的情况,比如映射查找和类型断言。

陷入恐慌

在 Java 中,更严重的故障是通过抛出某个异常来指示的。对于什么时候应该抛出异常,什么时候应该返回错误,经常会有混淆(例如,当读取超过文件末尾时),Java(和许多社区)库代码做出这种不一致的选择。

Go 通过使用总是返回一个错误值作为最后(或唯一)返回值的多值函数,使这种行为更加一致。对照nil测试错误值,以确定是否出现错误。一般来说,任何其他返回值只有在没有错误的情况下才有意义。只有当函数灾难性地失败时(内存不足、被零除、索引越界、参数无效等)。)是一个恐慌提出来的。

Java 支持异常的概念(从技术上来说 Throwables ,它是异常的超类)。异常是一个对象,当一个意外的/不寻常的情况出现时,它可以被抛出。一个例子是当零被用作除数时 JVM 抛出的 DivideByZeroException。另一个更严重的例子是当 JVM 不能满足一个new操作时抛出的 OutOfMemoryError 。Java 进程在try语句的catch块中抛出可抛出对象。可抛出的实例在 Java 代码中普遍被抛出和捕获。

Go 有一个类似但不太常用的概念,叫做恐慌。死机很像一个 throwable,可以由代码(您的或某个库)通过使用 Go 内置panic(<value>)函数来引发。该值可以是任何类型(但通常是一个string或(优选的)一个error实例);不应使用nil值。

Go 代码引起恐慌的情况应该很少。在大多数情况下,代码应该返回一个错误。只有在无法预料的情况下才应该使用 panic,因为通过错误来报告它们会很麻烦,例如 Java 的 OutOfMemoryError 的 Go 等价物。

Go 不像 Java 那样有异常类型。相反,它有恐慌参数(更像是 Java 错误混合了一些运行时异常)。Go 对 RuntimeException 和非 RuntimeException throwables 之间的 Java 区别没有概念。所有这些都映射到单个紧急值。永远不要声明函数可能引发的恐慌参数。

Java 有try/finallytry/catch/finally语句集。Go 不会。它使用延迟的函数来达到finally的效果。Go 使用一种不同但相似的机制来捕捉恐慌。

与 Java 非常相似,如果没有被捕获,恐慌通常会导致程序在打印回溯后退出。为了捕捉 Go 中的异常,可以使用内置的recover()函数,该函数返回与最近的异常一起发送的值(在特定的 goroutine 中)。为此,必须在已经延迟的函数中调用recover()

就像 Java catch子句可以检查抛出的异常一样,deferred 函数可以检查值,进行一些更正,然后再次返回或引发异常。像在 Java 中一样,延迟函数可以在当前调用栈的任何地方。这里有一个简单的例子:

func DoIt() (err error) {
     defer func() {
          p := recover()
          if p != nil  {  // a panic occurred
               // process the panic by (say) testing p value
               err = nil  // make containing function not return an error
          }
     }()
     :
      // any code that can panic
     if err != nil {
          panic(errors.New(fmt.Sprintf("panic: %v", err)))
            // or equivalently
           panic(fmt.Errorf("panic: %v", err))
     }
     :
     return
}

一般来说,Go 库和 Go 运行时避免引起恐慌。您的代码还应该。一种常见的情况是利用恐慌。如果一个函数得到一个非法的参数值,它通常会被报告为死机而不是错误返回。这种情况被认为是编程错误,而不是代码应该从中恢复的情况。注意,不是所有的地鼠都遵循这种方法,因此不会验证参数并产生恐慌;其他一些问题通常会在以后出现。该代码依赖于被提供有效的输入。

注意通常应该避免在死机恢复延迟函数中引起新的死机。这就像在 Java 中避免在catchfinally子句中抛出异常一样。

捕捉恐慌的一个关键区域是 goroutines。goroutine 中未处理的死机会导致 Go 程序崩溃。所以,最好不要让它们发生。这需要系统的纪律。为了实现这一点,作者建议所有的 goroutines 都由一个助手函数创建,类似于清单 7-1 。

package main

import (
     "errors"
     "fmt"
     "time"
)
var NoError = errors.New("no error")  // special error

func GoroutineLauncher(gr func(), c *(chan error)) {
     go func(){
          defer func(){
               if p := recover(); p != nil {
                    if c != nil {
                         // ensure we send an error
                         if err, ok := p.(error); ok {
                              *c <- err
                              return
                         }
                         *c <- errors.New(fmt.Sprintf("%v", p))
                    }
                    return
               }
               if c != nil {
                    *c <- NoError  // could also send nil and test for it
               }
          }()
          gr()
     }()
}

var N = 5

func main() {
     var errchan = make(chan error, N)  // N >= 1 based on max active goroutines
     // :
     GoroutineLauncher (func(){
          time.Sleep(2 * time.Second)  // simulate complex work
          panic("panic happened!")
     }, &errchan)
     // :
     time.Sleep(5 * time.Second)        // simulate other work
     // :
     err := <- errchan  // wait for result
     if err != NoError {
          fmt.Printf("got %q" , err.Error())
     }
}

Listing 7-1Capture Panics in a Goroutine Launcher Function

请注意,如果客户端不需要错误报告,可以省略错误通道。

这在运行时产生

got "panic happened!"

图示错误和混乱

内置error类型简单。很多第三方包都扩展了它,比如 JuJu Errors1 清单 7-2 、 7-3 和 7-4 是如何扩展它的一些可能的例子。例如,收集多次出现的错误(比如在处理切片的元素时)。

type MultError []error

func (me MultError) Error() (res string) {
     res = "MultError"
     sep := " "
     for _, e := range me {
          res = fmt.Sprintf("%s%s%s", res, sep, e.Error())
          sep = "; "
     }
     return
}
func (me MultError) String() string {
     return me.Error()
}

Listing 7-2Multiple Cause Errors

当被使用时

me  := MultError(make([]error,0, 10))
for _, v := range []string{"one", "two", "three"} {
     me = append(me, errors.New(v))
}
fmt.Printf("MultipleError error: %s\n", me.Error())
fmt.Printf("MultipleError value: %v\n\n", me)

生产

MultipleError error: MultError one; two; three
MultipleError value: MultError one; two; three

或者一个错误是由另一个错误引起的(很像 Java 中所有的 Throwables 都有原因)。

type ErrorWithCause struct {
     Err   error
     Cause error
}

func NewError(err error) *ErrorWithCause {
     return NewErrorWithCause(err, nil)
}
func NewErrorWithCause(err error, cause error) *ErrorWithCause {
     if err == nil {
          err = errors.New("no error supplied")
     }
     return &ErrorWithCause{err, cause}
}
func (wc ErrorWithCause) Error() string {
     xerr := wc.Err
     xcause := wc.Cause
     if xcause == nil {
          xcause = errors.New("no root cause supplied")
     }
     return fmt.Sprintf("ErrorWithCause{%v %v}", xerr, xcause)
}
func (wc ErrorWithCause) String() string {
     return wc.Error()
}

Listing 7-3Error with a Cause

当被使用时

fmt.Printf("ErrorWithCause error: %s\n", ewc.Error())
fmt.Printf("ErrorWithCause value: %v\n\n", ewc)

生产

ErrorWithCause error: ErrorWithCause{error cause}
ErrorWithCause value: ErrorWithCause{error cause}

注意,如下所示的方法使得任何数据类型都可以充当error:

func (x <sometype>) Error() string

这是因为error类型被有效地定义为

type error interface {
     Error() string
}

Go errors 2 软件包有几个有用的实用函数:

errors.Is(<error>, <type>)–解开错误,直到它与提供的类型匹配,如果找到则返回true

errors.As(<error>, <*type>)–展开错误,直到它与提供的变量类型匹配,将错误转换为该类型,设置变量,如果找到,则返回true

errors.Unwrap(<error>)–返回任何包装的错误(类似于 Java 异常的任何原因);实际的错误类型必须有一个Unwrap(<error>)方法。

在 Go 中模拟 Java 异常行为是可能的。例如,为了引入类似于 Try/Catch/Finally 的行为,可以实现如下的小库。这里,Go 函数取代了 Java Try/Catch、Try/Finally 和 Try/Catch/Finally 语句。

每个 function 子句都是作为(典型的)函数文字提供的。没有像 Java 中那样对每个异常类型都进行捕获,因为 Go 对所有问题都只有一个单一的异常。整体函数返回 try 子句的错误。因为 try 和 catch 子句可能有错误,所以有时会返回错误对类型TryCatchError

注意直接在延迟函数中而不是在triageRecover(...)函数中发出recover()函数是很重要的。

type TryFunc func() error
type CatchFunc func(error) (rerr error, cerr error)
type FinallyFunc func()

type TryCatchError struct {
     tryError   error
     catchError error
}

func (tce *TryCatchError) Error() string {
     return tce.String()
}
func (tce *TryCatchError) String() string {
     return fmt.Sprintf("TryCatchError[%v %v]", tce.tryError, tce.catchError)
}
func (tce *TryCatchError) Cause() error {
     return tce.tryError
}
func (tce *TryCatchError) Catch() error {
     return tce.catchError
}

func TryFinally(t TryFunc, f FinallyFunc) (err error) {
     defer func() {
          f()
     }()
     err = t()
     if err != nil {
          err = &TryCatchError{err, nil}
     }
     return
}

func triageRecover(p interface{}, c CatchFunc) (err error) {
     if p != nil {
          var terr, cerr error
          if v, ok := p.(error); ok {
               terr = v
          }
          if xrerr, xcerr := c(terr); xrerr != nil {
               cerr = xcerr
               err = xrerr
          }
          if terr != nil || cerr != nil {
               err = &TryCatchError{terr, cerr}
          }
     }
     return err
}

func TryCatch(t TryFunc, c CatchFunc) (err error) {
     defer func() {
          if xerr := triageRecover(recover(), c); xerr != nil {
               err = xerr
          }
     }()
     err = t()
     return
}
func TryCatchFinally(t TryFunc, c CatchFunc, f FinallyFunc) (err error) {
     defer func() {
          f()
     }()
     defer func() {
          if xerr := triageRecover(recover(), c); xerr != nil {
               err = xerr
          }
     }()
     err = t()
     return
}

Listing 7-4Try/Catch Emulation Example (Part 1)

这可以如清单 7-5 所示使用。

err := TryCatchFinally(func() error {
     fmt.Printf("in try\n")
     panic(errors.New("forced panic"))
}, func(e error) (re, ce error) {
     fmt.Printf("in catch %v: %v %v\n", e, re, ce)
     return
}, func() {
     fmt.Printf("in finally\n")
})
fmt.Printf("TCF returned: %v\n", err)

err = TryFinally(func() error {
     fmt.Printf("in try\n")
     return errors.New("try error")
}, func() {
     fmt.Printf("in finally\n")
})
fmt.Printf("TCF returned: %v\n", err)

err = TryCatch(func() error {
     fmt.Printf("in try\n")
     panic(errors.New("forced panic"))
}, func(e error) (re, ce error) {
     fmt.Printf("in catch %v: %v %v\n", e, re, ce)
     return
})
fmt.Printf("TCF returned: %v\n", err)

err = TryCatch(func() error {
     fmt.Printf("in try\n")
     return nil
}, func(e error) (re, ce error) {
     fmt.Printf("in catch %v: %v %v\n", e, re, ce)
     return
})
fmt.Printf("TCF returned: %v\n", err)

Listing 7-5Try/Catch Emulation Example (Part 2)

这将输出以下内容:

in try
in catch forced panic: <nil> <nil>
in finally
TCF returned: TryCatchError[forced panic <nil>]
in try
in finally
TCF returned: TryCatchError[try error <nil>]
in try
in catch forced panic: <nil> <nil>
TCF returned: TryCatchError[forced panic <nil>]
in try
TCF returned: <nil>

八、Go 语句

在这一章中,我们将更详细地描述 Go 的各种语言语句。当我们完成这一章时,你应该能够清楚地识别 Go 和 Java 语言语句及其功能之间的异同。

与 Java 非常相似,在 Go 中,计算是基于命令式模型的。计算按顺序执行,并保存在变量中。Go 几乎没有 Java 也支持的函数式编程计算风格。控制流只基于条件语句和循环语句,而不是像 Java 可以用它的流库支持的那样嵌入在函数调用中。关于在 Go 中尝试函数方法的一些讨论可以在 https://github.com/robpike/filter 找到。

Go 有几个条件语句:

  • 单向或双向条件句(也可用于构成多路条件句)-if/else

  • 多路值条件开关

  • 多路通道条件–选择

Go 有一个循环语句(for ),其中包含几个子窗体:

  • 无限循环

  • 带调整索引的循环

  • 当条件为真时循环

  • 在集合中循环

Go 可以用不同的方式退出/迭代循环:

  • 回路条件测试失败

  • 突然退出——中断还是返回

  • 前进到下一个迭代–继续

像在 Java 中一样,所有的 Go 代码必须被分组到可重用的单元中,这些单元被称为函数。在 Go 中,最好的做法是保持函数简短(比如几十行,最多几行),并根据需要生成更多的函数。Go 可以通过名字调用函数,也可以通过函数值间接调用函数。Java 只能通过名字调用方法。有些函数 Java 是通过语句做的,Go 是通过内置函数调用做的。

Go 可以为每个函数返回零个或多个结果。Java 只支持零或一。像在 Java 中一样,返回可以出现在函数中的任何地方。

打包和导入语句

像 Java 一样,每个 Go 源文件都需要一个 Package 语句作为第一个声明源代码所属包的语句,比如

package main

一个包中可以有任意数量的源文件。Go 源文件名不需要与包名匹配(如果一个包有多个源文件,通常不需要),但是为了更好地组织代码,建议它们匹配,尤其是对于包含主入口点的目录。例如,建议您使用一个main.go文件来保存包含main()函数的main包源代码。注意main包约定中的main函数是必需的,这样 Go builder 就可以识别出必须构建一个可执行文件。

如果源文件使用另一个包中的任何公共声明,则必须导入该包,例如

import "math"
import "net/http"

或者这样分组:

import (
  "math"
  "net/http"
)

源文件中可以有多组导入。所有 import 语句必须在 package 语句之后,任何其他语句之前。导入可以按任何顺序进行,但通常按导入路径中的姓氏排序,尤其是在同一个导入组中。如果源文件中没有引用包中的公共项,则不能导入该包(编译器将报告错误)。

导入是在文件级而不是包级完成的,因此必须像在 Java 中一样,在使用导入的每个源文件中重复进行。同一个包中的不同源文件可以并且经常有不同的导入列表。

导入的包中对公共名称的所有引用都必须以包名为前缀,如下所示:

r := new(http.Request)

默认情况下,任何导入的包路径中的姓都用作导入的前缀名。有时,您可能希望对一个包使用不同的(比如说更短的)名称。您可以在导入过程中为包指定一个别名,如下所示:

import net "net/http"

Go 包可以有几个init()功能。有时,即使不使用包中的符号,也需要运行这些函数。为此,在导入中添加空白的别名(下划线),如下所示:

import _ "net/http"

一个包的init()函数只运行一次,不管有多少源文件导入这个包。

赋值语句

也许在 Go 中最基本的动作就是给一个变量赋值。在 Go 中,像在 Java 中一样,这是通过赋值语句显式完成的。也可以通过向函数传递参数或从函数返回值来实现。赋值可以是常量、其他变量或涉及这些项目的表达式。

最基本的任务是

<variable> = <expression>

虽然是声明而不是赋值,但也有一种方便的方法来声明和赋值,类似于赋值语句:

 <variable> := <expression>

还有这种形式的扩充(也称为复合)赋值:

<variable> <binaryOperation>= <expression>

它们被解释为

<variable> = <variable> <binaryOperation> <expression>

像在 Java 中一样,并不是所有受支持的二元运算符都可以与赋值运算符结合使用。例如,逻辑运算符(&&||不能使用,因为它们具有短路行为。

请注意以下声明:

<variable>++相当于<variable> += 1

<variable>--相当于<variable> -= 1

Go 允许以下形式的并行(即元组)赋值:

<variable1>,<variable2>,...,<variableN> = <expression1>,<expression2>,...,<expressionN>

其中 N 在每一侧必须相同。任何(但通常不是全部)<variableX>都可以用下划线(“_”)替换,以忽略表达式位置,这通常是在函数调用结果中进行的。

所有右侧的值必须与左侧的相应变量兼容(能够被赋值),没有任何隐含的转换(除了一些数字文字值)。通常,这意味着相应位置的左侧变量和右侧值必须是同一类型。

如果左侧至少有一个变量是新声明的,则允许使用声明形式:

<variable1>,<variable2>,...,<variableN> := <expression1>,<expression2>,...,<expressionN>

在前面所有的例子中,<variableX>是定义一个可赋值目标(又名左值)的任何表达式。通常,这些是简单的标识符(变量名),但也可以是索引数组、切片、映射或指针变量解引用。

声明变量

Java 允许一次声明一个变量并分组声明。Go 也是如此。在 Java 和 Go 中,任何初始值都是可选的。注:在 Java 中,可以创建没有初始值的块/方法局部变量。同样的情况在 Go 中是不可能的;如果没有指定,所有声明的值都有一个初始值(称为)。

Java 的声明:

{<vis>} {<mod>}... <type> <id> {= <value>} {, <id> {= <value>}}...;

类型是任何内置或声明的类型(类、接口、枚举等)。).这些值必须可转换为类型。如果省略,则使用默认值(块/方法局部变量除外)。该值可以是一个表达式。

仅在字段声明中允许使用<vis>修饰符。它是publicprivateprotected中的一个,或者省略(意味着默认或包受保护)。Java 的<mod>修饰符,像abstractfinal,通常只允许在字段声明中使用,没有 Go 等价物。

Go 相当于一个声明语句:

var <id> {, <id>}... <type>

或者

var <id> {, <id>}... <type> = <value> {, <value>}...

或者

var <id> {, <id>}... = <value> {, <value>}...

该类型是任何内置或声明的类型。每个值必须属于同一类型。如果该值是文本,则它必须可转换为类型。如果省略,则使用零值。只有当所有值都被省略时,类型才是必需的;如果一个值存在,它的类型将用于推断任何缺少的类型。每个位置的推断类型可能不同。任何值都可以是表达式。id 和值的数量必须相同。

如前所述,Go 没有可见性修改器。如果 id 以大写字母开头,则它是 public 否则,它是包私有的(只能被同一个包中的代码看到)。

Go 允许更简洁的声明形式:

var ({<xxx> {, <xxx>...})

其中 xxx 是没有“var”前缀的声明。结束语“)”通常单独在一行中。这是声明变量的常规方式。

例如:

var (
      p = 1
      q = "hello"
      l int
      f float64 = 0
)

在顶级声明中,任何关于 var 的注释都由组中的所有成员共享。

Go 有另一种声明形式用于块局部(非字段)声明:

<id> {, <id>}... := <value> {, <value>}...

其中 id 和值的计数必须匹配。此外,在同一个块中至少不能声明一个 id。id 的类型可以不同,由值来表示。

元组赋值(或声明)有许多用途,但一些常见的用途是

  • 不使用临时变量交换值

例如:

  • 拆分range操作的结果
var x, y = 1, 2
x, y = y, x // after x==2, y == 1

例如:

  • 拆分函数或运算符返回的结果
for index, next := range collection { ... }
- or -
for _, next := range collection { ... }

例如:

file, err := os.Open(...)
- or -
if v, ok := map[key]; ok { ... }

声明命名常数

Java 允许声明类似常量的 1 ( static final)值。Go 有真常数。Go 支持一次定义一个常量并分组定义。在 Java 和 Go 中,初始值都是必需的。

Java 的声明(在某种类型内部):

{<vis>} static final <type> <id> {= <value>} {, <id> {= <value>}}...;

这些值必须可转换为类型。该值必须是常量表达式。

仅在字段声明中允许使用<vis>。它是publicprivateprotected中的一个,或者省略(意味着默认或包受保护)。

Go 相当于一个声明语句:

const <id> {, <id>}... <type> = <value> {, <value>}...

或者

const <id> {, <id>}... = <value> {, <value>}...

该类型是任何具有文字初始值设定项的内置或声明的类型。该值必须属于同一类型。如果该值是文本,则它必须可转换为类型。该值必须是可以在编译时计算的表达式(即,所有引用的标识符都指向没有循环引用的其他常数)。id 和值的数量必须相同。

Go 没有可见性修改器。如果 id 以大写字母开头,则它是 public 否则,它是包私有的(只能被同一个包中的代码看到)。

Go 允许更简洁的声明形式:

const ({<xxx> {, <xxx>...})

其中 xxx 是没有“const”前缀的声明。结束语“)”通常单独在一行中。这是声明常数的常规方式。

例如:

const (
      p = 1
      q = "hello"
      f float64 = 0
)

If/Else 语句

If/Else 是最基本的条件测试机制。它允许代码序列中的交替流。

Java 的 if 语句:

if(<cond>) <block>

或者

if(<cond>) <block> else <block>

Java 允许除了块之外的任意可执行语句作为 if/else 目标。

Go 的 if 语句:

if {<simpleStmt>;} <cond> <block>

或者

if {<simpleStmt>;} <cond> <block>
else (<ifStmt>|<block>)

If/else 目标是语句块(这也是 Java 中的最佳实践)。Else 语句还允许另一个 if 语句作为目标;这允许多条件测试。在 Go 中,多条件测试最好通过使用 Switch 语句来完成。

可选的简单语句是

  • 空(省略–无分号)语句

  • 表达式语句

  • 发送(通道

  • Inc/dec 报表

  • 分配

  • 短变量声明(最常见的选项)

if 语句创建了一个隐含块,所以任何声明都隐藏了这样的名字,使其不包含作用域。例如:

var x, y = 0, 0
if t := x; t < 0 {  // t in new scope
      var x = 1 // a new x variable; hides x above
      y = t + x
} else {
      y = -1
}

注意else子句,如果存在,必须在与if块结束相同的行开始。

在惯用的 Go 中,else的使用被最小化。因此,从条件(比如 If)块返回是很常见的。当这样做时,使用else子句是非常规的(这是多余的)。例如:

if t < 0 {
      return true
} else {
      return false
}

更常规的写法是

if t < 0 {
      return true
}
return false

它也可以更简洁地表达为

return t < 0

通过这种方式,惯用的 Go 代码倾向于在包含函数的左边对齐,而不会嵌套太深。如果你的代码嵌套超过(比方说)两层,考虑使用returnbreakcontinue语句或者通过提取深度嵌套的代码作为一个新函数来重写它以减少层数。

Go 有很强的源代码风格规则。一个是如何测试布尔值。考虑(常见的)例子:

if v, ok := aMap[someKey]; !ok {
      return
}

和...相对

if v, ok := aMap[someKey];  ok == false {
      return
}

第一种形式(直接使用布尔值)是惯用的,通常用在第二种形式(比较布尔值)上。

Java 有一个三元表达式(?:)允许(通常很方便)条件测试。例如:

int x = input < 0 ? -input : input;  // a simple abs(input)

这是一个简短的形式

if(input < 0) x = -input; else x = input;

而是作为一种表达(相对于陈述)。

Go 没有这个表达式的对等词。人们必须这样做:

var x int
if input < 0 {
      x = -input
} else {
      x = input
}

或者

var x int = input // (or x := input)
if input < 0 {
      x = -input
}

或者,对于简单的物体(比如变量或常数),更简洁地说:

var x int; if input < 0 { x = -input } else { x = input }

或者

x := input; if input < 0 { x = -input }

注意,即使如前面所示输入,大多数 Go 源代码格式化程序也会在分号处拆分这些行。

交换语句

和 Java 一样,Go 也有 Switch 语句。总的来说,Go 的 Switch 语句更加灵活。Java Switch 语句遵循以下一般形式:

switch (<expr>) {
  case <value1>:
    :
  case <value2>:
    <statements>
    break;
  :
  default:
    <statements>
}

每组陈述可以由一个或多个案例介绍者进行。将 expr 值与每个 case 值(必须是唯一的)进行匹配(测试相等性),并执行匹配后的任何代码。如果代码中没有提供,流程继续执行以下情况,直到找到break。expr 可以是任何整数类型、字符串类型或任何枚举类型。如果没有匹配,并且default介绍器存在,则运行该代码。

上例的 Go 对应物是

switch <expr> {
  case <value> {, <value>}...:
    <statements>
  default:
    <statements>
}

switch 语句和每个 case 都创建了一个隐含的块,所以任何声明都隐藏了这样的名字,使其不包含作用域。

在 Go 案例中,每个案例使用多个匹配值,而不是多个案例介绍器。同样,在 Go 情况下,每组语句的末尾都有一个隐式的break。像在 Java 中一样,这些值必须是不同的。同样,在 Go 中,每个 case 都是它自己的块,就好像它被输入为(这在 Java 中是需要的)

case <value>: {
    <statements>
}

这意味着变量可以在那组语句中声明为局部变量。

要获得类似 Java 的无中断失败,请用 fall through 语句结束这组语句,如下所示:

switch <expr> {
  case <value1>:
    <statements>
    fallthrough
  case <value2>
    <statements>
  default:
    <statements>
}

Java 支持如下级联 if 语句:

if(<expr1>) {
  :
} else if(<expr2>) {
  :
} ... else if(<exprN>) {
:
} else {
  :
}

Go 也支持这种方法,但是惯用的方法是使用不同形式的开关:

switch {
  case <expr1>:
    <statements>
  case <expr2>
    <statements>
  :
  case <exprN>
    <statements>
  default:
    <statements>
}

表达式可以是任意的,除非它们必须是布尔类型。案例(默认案例除外)按照输入的顺序进行测试。

所以,这个 switch 语句:

var c, ditto rune = 'c', '\0'
switch c {
case 'a', 'b', 'c':
      ditto = c
default:
      ditto = 'x'
}

和这个 switch 语句是等效的:

var c, ditto rune = 'c', '\0'
switch {
case c == 'a', c == 'b', c == 'c':
      ditto = c
default:
      ditto = 'x'
}

Java 最近增加了 Switch 语句的表达式(有结果值)形式(一种增强的三元表达式)。开关可以是任何表达式中的术语。Go 没有这样的。这些开关表达式添加了不失败案例样式 Go has。此外,案件创建自己的块像 Go。与这个新的switch相关联的是返回开关值的新的yield语句。

While 语句

While 是一种基本的循环机制。它允许在代码序列中有条件地(预先测试)重复流。

Java 的 while 语句:

while (<cond>) <block>

Java 允许任意可执行语句作为 while 目标。

Go 相当于 while 语句:

for <cond> <block>

该语句的目标是一个块(这是 Java 中的最佳实践)。

例如:

var x, y = 10, 0
for x > 0 {
  y++
  x--
}

Do-While 语句

Do-While 是一种基本的循环机制。它允许在代码序列中有条件地(事后测试)重复流动。

Java 的 do-while 语句:

do <block> while (<cond>);

Go 没有与 Do-While 语句直接等效的语句。可以通过在块中包含测试的 for 语句生成一个,如下所示:

var x, y = 10, 0
for {
      y++
      x--
      if x < 0 {
            break
      }
}

For with Index 语句

For 是主要的索引循环机制。它允许一个索引跨越一个范围,并由代码序列中的重复流来操作。Go 的 For 语句提供了与 Java 的 For 语句类似的功能。

Java 的 for 语句:

for({<init>};{<cond>};{<inc>}) <block>

Java 允许任意的可执行语句作为目标。

Go 相当于 for 语句:

for {<init>};{<cond>};{<inc>} <block>

与 Java 不同,Go 不支持在<init><inc>子句中使用逗号(",")分隔的表达式。

该语句的目标是一个块(这是 Java 中的最佳实践)。<cond>子句是可选的,如果省略则为true。可选的<init><inc>组可以是

  • 空(省略–无分号)语句

  • 表达式语句

  • 发送(通道

  • Inc/dec 报表

  • 分配

  • 短变量声明

该语句创建了一个隐含块,因此任何声明都隐藏了这样的名称,使其不包含范围。

例如:

var x, y = 10, 0
for i := 0; i < 10; i++ {
      y++
}

对于超过一个集合声明

For 是迭代(可能为空)集合(或其他值流)的主要循环机制。它允许通过代码序列中的重复流程一次处理一个集合的元素。处理顺序由集合决定。

Java 的 for 语句:

for(<varDecl>: <iterable>) <block>

或者(更直白地)一个人能做的

Collection<SomeType> c = <some collection>;
Iterator<SomeType> it = c.iterator();
for(; it.hasNext();) {  // could use while here instead
  <varDecl> = it.next();
  :
}

或者(更直白地)一个人可以做的(在可索引集合上)

Collection<SomeType> c = <some collection>;
for(int i = 0, count = c.size(); i < count; i++) {
  <varDecl> = c.get(i);
  :
}

Go 相当于集合上的 for 语句:

for <indexVar>,<valueVar> := range <collection> <block>

该语句的目标是一个块(这是 Java 中的最佳实践)。可选的(至少需要一个)<indexVar><valueVar>接收下一项的索引(或键)和下一项的值。<collection>必须是某种集合或流类型,比如数组、切片、映射或通道。

Go 要求在块体中使用所有声明的变量(在:=的左边)。为了避免这一要求,如果没有引用,可以用下划线(“_”)来替换。

例如:

for _, v := range []string{"x", "y", "z"} {
      fmt.Println(v)
}

或者

aMap := make(map[string]string)
:
for k, v := range aMap {
      fmt.Printf("%s = %s", k, v)
}

Java 的等价物可能是

Map<String,String> m = <some map>;
for(Iterator<String> it = m.keySet().iterator(); it.hasNext();) {
  var k = it.next();
  var v = m.get(k);
  System.out.printf("%s = %s", k, v);
}

range为地图呈现键的顺序是不确定的,并且对于每个地图实例可以是不同的。这是故意的。要按某种顺序处理键,必须首先对它们进行显式排序,比如说排序。例如:

aMap := make(map[string]string)
:
keys := make([]string, 0, len(aMap)) // note created empty
for k, _ := range aMap {
      keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
      fmt.Printf("%s = %s", k, aMap[k])
}

注意 Java 的TreeMap类型使这变得简单多了。

钥匙片也可以做成这样:

keys := make([]string, len(aMap))  // note created full size
index := 0
for k, _ := range aMap {
      keys[index] = k  // "keys[index++] = k" not supported
      index++
}

前面的方法可能更节省时间。

永远的声明

For 是主要的无限循环机制。它允许在代码序列中无限重复地流动。

Java 的 for 语句:

for(;;) <block>  // while (true) also works

Go 相当于 for 语句:

for <block>

目标是一个块(这是 Java 中的最佳实践)。

例如:

var x, y = 10, 0
for {
      y++
      x--
      if x < 0 {
            break
      }
}

中断和继续语句

像 Java 一样,Go 也有基本上以相同方式工作的breakcontinue语句。Break 退出循环,而 continue 移动到循环的下一次迭代。通常,这些语句位于一些条件语句的主体中,如 if 或 f or。语法是

break {<label>}
continue {<label>}

如果标签存在,它一定是附加在某个包含循环上的标签。这允许从多层嵌套循环中退出。如果省略,则假定最嵌套的循环。任何循环都可以标记如下(但是该标记必须由一些 break 或 continue 引用):

{<label> :}... <forStatement>

注意,Java 在 Go 中自动使用switch语句中的break语句来退出案例,因此在 Go 的switch(或select)语句中不需要break来避免失败。可以使用breakcontinue(比如说if主体)在结束前退出箱子。

Goto 语句

Go 支持 Go-To(无条件跳转)语句;Java 没有(虽然是保留字)。它允许在同一块内跳转(但不允许跳出块或进入嵌套块)。不能使用 Go-To 跳过声明。同一块中任何带标签的语句都可以是目标。格式是

goto <label>

可以用 Go-To 代替更结构化的表单。例如,这种概念形式的循环:

for cur:=0; cur < 10; cur++ {
      : body of loop
}

可以这样创建:

cur := 0
L1: if cur >= 10 {
      goto L2
}
: body of loop
cur++
goto L1
L2:

注意在作者看来,绝对不应该使用goto语句;ifswitchfor提供足够的本地控制流量。您的代码应该遵循结构化编程的原则 2 及其 Goto 3 -less 方法。

返回语句

在 Java 中,每个方法都用一个return语句退出(可能在void方法上隐式运行,因为从void函数的末尾隐式返回)。return 语句提供要返回的值,如下所示:

return {<value>}  // <value> present only on non-void methods

在 Go 中,Return 几乎是相同的,只是可以像这样返回多个值:

return {<value>{,<value>...}}  // <value>... only on non-void methods

返回值的数量必须与函数原型上声明的返回值的数量相匹配。如果返回值是命名的,return 语句可以省略它们。

例如:

func threeInts() (int, int, int) {
      :
      return 1, 2, 3  // required explicit return values
}

or:
func threeInts() (x, y, z int) {
      :
      return 1, 2, 3  // explicit return values (ignore names)
}

or:
func threeInts() (x, y, z int) {
      x, y, z = 1, 2, 3   // set return values before returning
      :
      return // implicit return values
}

这最后一种形式是这位作者普遍推荐的。其他人可能会采取不同的立场。

延期声明

Java 有两种流行的资源清理机制:

  1. Try/Finally(或 Try/Catch/Finally)

  2. 尝试使用资源

try/最后,一般来说,看起来是这样的:

try <block>
finally <block>

无论try子句如何结束,都执行finally子句(通常,通过return或通过某些异常)。

对资源的尝试通常如下所示:

try (<declaration> = <new Resource>{;<declaration> = <new Resource>}...) {
    // use the resource(s)
}

try结束时(通常,通过返回或通过一些异常),在try子句中分配的任何资源被自动释放(在编译器编写的finally子句中)。

Go 有一个类似 try/finally 的特性,但是没有类似 try with resources 的特性。Go 使用 Defer 语句,其行为很像一个finally子句。这个语句看起来像

defer <function call>

每次执行 defer 语句时(即使是在循环中),对所提供的函数的调用都会放在调用堆栈中。当包含 defer 语句的函数退出时,延迟的函数调用以相反的顺序执行。可以有许多延迟函数。即使包含函数以return或死机(相当于抛出异常)结束,也会发生这种情况。

典型的方法如下例所示:

func someFunction() {
      // acquire some resource
      defer func() {
            // release the resource
      }()  // note the function is called
      : use the resource
}

在这个模式中,在获得任何资源之后,立即注册一个释放资源的延迟函数。延迟函数继续运行,最终返回或死机,导致延迟函数被调用。

请注意,延迟函数可以访问延迟函数(它是一个闭包)的局部变量(必须在编码defer之前声明),并且在延迟函数返回到其调用者之前被调用,这允许它更改延迟函数的返回值。这很有用,尤其是在紧急情况下(比如被零除)或其他错误恢复时。例如:

func someFunction() (result int, err error) {
      defer func() {
            if result == 0 {   // default value
                  result = -1
                  err = errors.New("bad value")
            }
      }()
      :
      result = 1
      :
      return
}

Go 语句

Go 语句启动一个 goroutine。goroutine 只是一个普通的 Go 函数,通常不返回任何值(如果返回,则被丢弃)。创建一个 goroutine,并使用 Go 语句启动,如下所示:

go <func>({arg, {arg}....})

Go 语句立即返回,函数与调用者异步(可能并行)运行。使用调用方提供的不同 goroutine 中的任何参数调用该函数。

注意,Go 中的所有代码都在某个 goroutine 中运行,包括main()函数。

通常,使用函数文字,而不是预先声明的函数,例如

go func(x int) {
      :
}(1)  // note the function is called

注作者建议用后缀“Go”(或类似的词)来命名期望与 Go 一起运行的函数,以使这个用例清晰。

选择指令

Go 有一个 Select 语句,在 Java 中没有对应的语句。Select 语句用于处理通过通道接收的项或将项发送到通道。使用“选择”之前,请确保您了解频道。Select 语句看起来很像 Switch 语句:

select {
  case <receiver>{, <receiver>}... = <- <channel>:
    <statements>
  case <identifier>, <var> := <- <channel>:
    <statements>
  case <channel> <- <expression>:
    <statements>
  default:
    <statements>
}

<receiver>是一个表达式(通常只是一个标识符),它指定一个变量来接收通道的值。Select 语句和每个 case 都创建了一个隐含的块,所以任何声明都隐藏了这样的名字,使其不包含作用域。

前两种情况在可以从渠道接收项目时触发。第三种情况是在可能向通道发送项目时触发(如果接收通道有空间)。所有病例都经过评估/测试。如果触发了任何案例,将随机选择并执行其中一个案例,并完成任何相关的赋值和/或语句。

第二种情况有一个<var>(通常命名为“ok”),用于指示源通道是否关闭。通道关闭时将是false

如果没有触发其他案例,则触发default案例。经常被省略。没有 default 子句的 Select 语句可以阻止等待接收或发送项目。

Select 语句经常在无限循环中执行,如下所示:

for {
  select {
   :
  }
}

这允许从通道接收项目,并且只要它们被发送就进行处理(即,通道是打开的)。

可以接收两个不同通道的值的示例:

var cchan chan int
var ichan chan int
var schan chan string
var scount, icount int
select {
case <- schan:                        // receive
      scount++                        // count receive
case <- ichan:                        // receive
      icount++                        // count receive
case cchan <- scount + icount:        // send current total
default:
      fmt.Println("no match")
}

九、接口的应用

在这一章中,我们将讨论 Java 中一些有趣的接口应用,以及它们与 Go 编码的关系。

界面是关键

就像在 Java 中一样,使用接口(通过具体类型)作为参数和返回类型在 Go 中很重要。它支持许多选项,比如用模拟对象 1 代替普通对象,这对于测试来说至关重要。所以,特别是当你把一个结构类型传入或传出一个函数时,看看你是否能用一个接口类型替换这个结构。如果您的函数只使用结构的方法而不使用其字段,这通常是可能的。

如果不存在与您使用的方法匹配的现有接口,请创建一个并发布给其他人使用。例如,给定这种类型:

type Xxx struct {
     :
}
func (x *Xxx) DoSomethingGood() {
     :
}
func (x *Xxx) DoSomethingBad() (err error) {
     :
}

您可以创建接口:

type DoGooder interface {
     DoSomethingGood()
}
type DoBader interface {
     DoSomethingBad() error
}

然后在某个使用Xxx的客户端,这样说:

func DoWork(xxx *Xxx) {
     xxx.DoSomethingGood()
}

你可以把它转换成

func DoWork(dg DoGooder) {
     dg.DoSomethingGood()
}

但是现在DoWork调用者可以发送一个Xxx的实例或者任何其他有DoSomethingGood()方法的类型。有时,您需要调用 struct 类型的多个方法。有两个主要选项:

  1. 给函数多个参数,每个参数对应一个不同的接口类型,调用者为所有参数传递相同的对象。

  2. 创建组合接口并传入该类型。

选项二通常比选项一更受欢迎。

对于选项一,这可以用作

func DoWork(dg DoGooder, db DoBader) {
     dg.DoSomethingGood()
     db.DoSomethingBad()
}

可以这样称呼:

var xxx *Xxx
:
DoWork(xxx, xxx)

对于选项二,组合接口可以是

type DoGoodAndBad interface {
     DoGooder
     DoBader
}

它可以这样使用:

func DoWork(dgb DoGoodAndBad) {
     dgb.DoSomethingGood()
     dgb.DoSomethingBad()
}

可以这样称呼:

var xxx *Xxx
:
DoWork(xxx)

有点令人惊讶的是,这也可以这样调用(使用一个对象,而不是指向该对象的指针):

var xxx Xxx
:
DoWork(xxx)

Go 编译器检测对象和对象指针的使用,并做正确的事情。只有接口类型的参数才会出现这种情况。

类似地,对于当前返回结构类型的函数,可以将其更改为返回多个接口或返回一个组合接口。

接口有一个问题,可能会很成问题。由于 Go 不允许相同类型的重载(相同的名称,不同的签名)函数,所以您可以很容易地用相同的方法名称创建多个接口,通常使用不同的参数和/或返回类型。但是你不能把它们组合成一个新的界面。这也意味着一个类型不能同时实现这些不同的接口。

对此没有简单的解决办法。因此,在选择接口中的方法名称时要小心,因为您最终可能会为行为保留该名称。例如,io.Writer接口声称Write方法(及其特定的参数)仅仅意味着它认为它意味着什么。在不与该接口冲突的情况下,其他接口无法为其他目的创建名为Write的方法。

例如,您可以创建这样的接口:

type MyWriter interface {
     // vs. io.Writer: Write([]byte) (int,error)
     Write([]byte, int) error
}

不可能创建同时实现了MyWriterio.Writer接口的类型。

避免这个问题的一个方法是用更长的,通常是多个单词的名字来创建方法,而把短名字留给 Go 运行时开发人员使用。

依赖注入研究

进一步考虑接口的使用,应该尽可能利用依赖注入 2 (DI)。DI 是一种设计方法,在这种方法中,代码被提供了它的依赖项,而不是为它自己获得它们(换句话说,让别人为你提供你所有的依赖项)。DI 将创建依赖关系的责任从依赖它的代码中分离出来。DI 实现通常要求注入的类型符合某种接口类型。

这种方法提供了更大的灵活性,尤其是在(1)测试代码(可以注入模拟对象)或者(2)配置对象之间的复杂关系时。第二种情况在 Java 开发中非常普遍,以至于一个主要的框架, Spring、 3 、??Spring Boot4 就是为了提供这种情况而创建的。其他选项也存在,比如谷歌的 Guice。 5

Wikipedia 对 DI 的定义如下:“依赖注入将客户端依赖的创建与客户端的行为分开,这允许程序设计松散耦合,并遵循依赖倒置 6 和单一责任 7 原则。”

Wikipedia 描述 Spring DI:Spring 框架的核心是它的反转控制 8 (IoC)容器,它提供了使用反射配置和管理 Java 对象的一致方法。容器负责管理特定对象的对象生命周期:创建这些对象,调用它们的初始化方法,并通过将它们连接在一起来配置这些对象。由容器创建的对象也称为托管对象或 beans。对象可以通过依赖关系查找或依赖关系注入的方式获得。

那么,什么是依赖呢?它是(至少)一个对象

  1. 具有状态和/或行为。

  2. 状态应该被封装(对任何用户隐藏),以便实现可以改变。因此,行为最好用接口来表示。

  3. 由一些(相关的)代码使用。

在 Spring 的例子中,有阿迪容器,它管理所谓的bean(可以链接在一起的 POJOs 9 )。容器通常像地图一样,提供可以在运行时解析的命名对象。在大多数情况下,容器基于工厂方法的注释(比如说@Bean)或外部定义(比如说 XML)来创建 bean 实例。DI 通常通过注释(比如@Inject@Wired)来告诉容器将一个源 POJO 链接(注入)到一个目标 POJO。

容器拥有订购先决条件 bean 创建和注入。通常,beans 是单例对象(在应用程序中共享一个实例)。容器通常不是程序执行期间进出的对象的来源。通常,容器扮演主程序的角色,创建 beans,然后在程序启动时将它们“连接”在一起。

Java DI 框架经常使用反射来创建要注入的对象。他们通常采用应用程序开发人员定义的 POJO,并将其包装在一个添加额外功能的代理 10 中,比如日志记录或数据库事务管理。代理概念的关键是,代理的客户端不能仅通过被代理的接口来区分它和它所代理的对象;它完全实现了被代理对象的行为契约,因此它是对象的直接替代。在大多数情况下,POJO 类必须实现一个或多个接口,这些接口可以具有在运行时动态定义的具体实现。

Go 目前不支持这种代理的动态创建,因为似乎不可能在运行时通过使用反射来定义类型,这可以用于实现符合接口的对象。这是经常使用代码生成方法的部分原因。或许这在未来会有所改变。Go 确实支持创建客户端可能知道的类似代理的外观对象。

让我们将术语 POGO 定义为 POJO 的 GO 等价物。POGOs 通常被实现为 Go 结构。

Go 没有标准的 DI 容器实现。Go 社区提供了一些,比如优步的 Dig 11 (或者 Fx 12 )和谷歌的 Wire13

Dig 描述如下:

一个基于反射的依赖注入工具包,适合于:

  • 为应用框架提供动力

  • 进程启动时解析对象图

Wire 描述如下:使用 依赖注入 自动化连接组件的代码生成工具。组件之间的依赖关系在 Wire 中表示为函数参数,鼓励显式初始化而不是全局变量。因为 Wire 在没有运行时状态或反射的情况下运行,所以编写用于 Wire 的代码即使对于手写的初始化也是有用的。

这两个示例容器举例说明了实现 Go 容器的主要方法:

  1. 使用反射(像 Spring 一样)来设置 POGOs 中的字段,以将它们连接在一起。

  2. 使用代码生成来创建逻辑(很像在main中手动完成的那样)以将弹簧连接在一起。

DI 容器特别适合于提供依赖项,比如日志记录器、数据库连接池、数据缓存、HTTP 客户端和类似的伪全局值。事实上,如果做到极致,容器本身就是应用程序中唯一的公共顶级对象;所有其他的都由容器管理。

在 Go 中,有几个注射选项:

  1. 实例初始化——在这里,依赖关系是通过在声明实例文字时设置来注入的。

  2. constructor/factory——这里,依赖关系是通过传递给一个构造函数(New...)或其他工厂方法来注入的。通常,这是首选选项。

  3. 直接字段分配–这里,通过直接分配字段来注入依赖性。通常,该字段必须是公共的(因为依赖类型通常在不同的包中)才能实现这一点。应该避免这种选择。

  4. Setter 方法——这里,依赖项是通过传递给“setter”方法来注入的。很少这样做,因为结构通常不为所有私有字段提供 get/set 方法,尤其是作为依赖项的公共接口的一部分。

前两种形式的局限性在于,不可能建立相互循环依赖的 POGOs。一般来说,最好避免这种依赖图;依赖关系应该形成一个层次结构。对于后两种情况,依赖关系是在创建实例之后设置的,因此在没有设置依赖关系时会有一个窗口。

作为手动 DI 的一个例子,考虑清单 9-1 中显示的这三种依赖类型(CacheHTTPClientLogger)。图 9-1 中通过浏览器显示的基本功能(无 DI)示例。

img/516433_1_En_9_Fig1_HTML.jpg

图 9-1

使用建议调用请求

package main

import (
     "fmt"
     "time"
)

type Cache interface {
     Get(name string) (interface{}, bool)
     Set(name string, value interface{}) error
     ClearName(name string)
     ClearAll()
}

type MapCache map[string]interface{}

func (c MapCache) Get(name string) (res interface{}, ok bool) {
     res, ok = c[name]
     return
}
func (c MapCache) Set(name string, value interface{}) (err error) {
     c[name] = value
     return
}
func (c MapCache) ClearName(name string) {
     delete(c, name)
     return
}
func (c MapCache) ClearAll() {
     for k, _ := range c {
          delete(c, k)
     }
     return
}

type HTTPClient interface {
     SendReceive(url, method string, in interface{}) (out interface{},
          err error)
}

type EchoHTTPClient struct {
}

func (c *EchoHTTPClient) SendReceive(url, method string, in interface{}) (out interface{},
     err error) {
     out = fmt.Sprintf("SENT %s %s with %v", method, url, in)
     return
}

type Logger interface {
     Log(format string, args ...interface{})
}

type StdoutLogger struct {
}

func (l *StdoutLogger) Log(format string, args ...interface{}) {
     fmt.Printf("%s - %s\n", time.Now().Format(time.StampMilli), fmt.Sprintf(format, args...))
}

type HTTPService struct { // also a HTTPClient
     log    Logger
     client HTTPClient
     cache  Cache
     // :  other fields not using dependencies
}

func NewService(client HTTPClient, log Logger,
     cache Cache) (s *HTTPService) {
     s = &HTTPService{}
     s.log = log
     s.client = client
     s.cache = cache
     // : set other fields
     return
}

func (s *HTTPService) SendReceive(url, method string,
     in interface{}) (out interface{}, err error) {
     key := fmt.Sprintf("%s:%s", method, url)
     if xout, ok := s.cache.Get(key); ok {
          out = xout
          return
     }
     out, err = s.client.SendReceive(url, method, in)
     s.log.Log("SendReceive(%s, %s, %v)=%v", method, url, in, err)
     if err != nil {
          return
     }
     err = s.cache.Set(key, out)
     return
}

func main() {
     log := StdoutLogger{}      // concrete type
     client := EchoHTTPClient{} // concrete type
     cache := MapCache{}        // concrete type
     // create a service with all dependencies injected
     s := NewService(&client, &log, cache)
     // :
     for i:= 0; i < 5; i++ {
          if i % 3 == 0 {
               cache.ClearAll()
          }
          data, err := s.SendReceive("some URL", "GET",
             fmt.Sprintf("index=%d", i))
         if err != nil {
             fmt.Printf("Failed: %v\n", err)
           continue
          }
          fmt.Printf("Received: %v\n", data)
     }
     // :
}

Listing 9-1Dependency Injection in a Go Example

前面的例子展示了如何定义三个可注入的接口,并为每个接口提供了一个简单的示例(可能称为模拟)实现,然后注入每个实现。这里,main()函数发送五个事务,并在序列中途清空缓存。注意以下输出显示了缓存的影响(五个事务中只有两个被执行):

Jul 20 09:10:40.348 - SendReceive(GET, some URL, index=0)=<nil>
Received: SENT GET some URL with index=0
Received: SENT GET some URL with index=0
Received: SENT GET some URL with index=0
Jul 20 09:10:40.349 - SendReceive(GET, some URL, index=3)=<nil>
Received: SENT GET some URL with index=3
Received: SENT GET some URL with index=3

Go 社区中的一些人认为使用 DI,尤其是当由容器管理时,对于 Go 来说并不习惯。通过容器的 DI 可以隐藏对象之间的关系,而在代码中手动创建它们(如前所示)则更加明显。这一论点有可取之处。但是,随着应用程序复杂性的增长和涉及的部件(POGOs)的增加,手动代码可能会失去控制,自动化 DI 解决方案可能是合适的(甚至是必要的)。

不管你如何结束这场争论,在作者看来,让你的代码能够支持 DI 是更好的方法。此外,如果谷歌和脸书都提供库做 DI,它一定是有用的。

关于面向方面编程

Java 支持一种叫做面向方面编程 14 (AOP)的编程风格。AOP 允许用新的行为(也就是代码)来扩充(用所谓的建议)代码(通常称为基础或原始代码)。维基百科是这样描述的:一种编程范式,旨在通过允许分离横切关注点15【XCC】来增加模块化。它通过向现有代码添加额外的行为(一个建议)来实现这一点,而不修改代码本身,而是通过“切入点”规范单独指定修改哪个代码,例如“当函数名以‘set’开头时,记录所有函数调用”。这允许将对业务逻辑不重要的行为添加到程序中,而不会弄乱作为功能核心的代码。

AOP 中有三个关键概念:

  1. 切入点——指定在哪里应用建议;通常,一些谓词(通常是一种模式,如正则表达式)选择要建议的代码或数据。切入点通常仅限于匹配一个或多个类型中的一个或多个方法,但是一些 AOP 系统也允许匹配数据字段。许多关节点可以匹配一个切入点。

  2. 建议——当切入点被触发时该做什么。建议有很多种,但最常见的是之前、之后和周围。

  3. 连接点——代码中应用建议的实际位置。

切入点和建议代码通常由一个类似类的构造定义,称为方面,这是一种描述切入点和/或所需建议的方法。使用 Java,有几种方法可以在连接点应用建议:

  1. 静态重写源代码——一些预处理器(在编译之前)编辑基本源代码。

  2. 静态重写目标代码——一些后处理器(编译后)编辑基本目标代码(这在 Go 中很难做到;如果在需要编译器更改的代码生成阶段完成,会更容易)。

  3. 动态重写目标代码——一些运行时处理器编辑目标代码,通常是在第一次加载时(这在 Go 中很难做到)。

  4. 使用动态代理——一些运行时处理器包装代码,通常是在第一次加载时(这在 Go 中很难做到)。

Java 有几种 AOP 实现。最受欢迎的是AspectJ16Spring AOP17 AspectJ 更全面,主要使用增强选项二和三。Spring AOP 主要使用增强选项四。

AOP 通常用于向代码中添加行为。常见的例子是向 web API 处理程序添加日志记录、授权检查和事务支持。这些是 XCC 的例子,它们通常不是代码的主线目的或核心关注点的一部分,但是支持上下文的需要。如果主代码中没有杂乱的代码来提供它们就更好了。

Go AOP 选项有限。标准库中没有直接支持。一些社区提供的选择是存在的,但是它们可能还不成熟。它们不像 Java 产品那样全面。目前,似乎没有任何 Go AOP 产品像 Java AOP 那样支持非侵入式地(既不改变客户机也不改变服务代码)向基类型添加通知。

AOP 风格的编程看起来很“神奇”(代码有新的行为,而行为的来源并不总是很明显)。像 DI 容器一样,AOP 风格的编程在 Go 中并不习惯。但是和 DI 一样,它也是增加支持的有力手段。

在 Go 中,类似 AOP 的行为可以通过应用代码来实现,通常称为中间件 18 (又名软件胶水)。这是通过将服务包装在符合服务原型的处理器中,在客户端和服务之间添加的功能(因此称为中间功能)。由于 Go 支持一流的功能,中间件可以相对容易地实现。

注意任何 HTTP 处理程序都必须符合在net/http中定义的接口:

type HandlerFunc func(http.ResponseWriter, *http.Request)

给定这些助手函数,如清单 9-2 和 9-3 所示(又名中间件或 around advice):

package main

import (
     "fmt"
     "log"
     "net/http"
     "time"
)

func LogWrapper(f http.HandlerFunc) http.HandlerFunc {
     return func(w http.ResponseWriter, req *http.Request) {
          method, path := req.Method, req.URL
          fmt.Printf("entered handler for %s %s\n", method, path)
          f(w, req)
          fmt.Printf("exited handler for %s %s\n", method, path)
     }
}

func ElapsedTimeWrapper(f http.HandlerFunc) http.HandlerFunc {
     return func(w http.ResponseWriter, req *http.Request) {
          method, path := req.Method, req.URL
          start := time.Now().UnixNano()
          f(w, req)
          fmt.Printf("elapsed time for %s %s: %dns\n",
               method, path, time.Now().UnixNano() - start)
     }
}

Listing 9-2Advice/Middleware for HTTP Requests (Part 1)

请注意,这些包装器函数返回调用目标服务时应用的其他函数,而不是调用包装器时应用的函数。这两种方法都是 Around advice(最常见的一种)的示例,因为它们在调用目标服务之前和服务返回之后都采取行动。

如果您需要 around 行为来避免可能出现的混乱,请像这样重写包装器:

:
defer func(){
     if p := recover(); p != nil {
          fmt.Printf("elapsed time for %s %s failed: %v\n",
               method, path, p)
          panic(p)
     }
}()
f(w, req)
:

例如,让我们看一下向 HTTP 请求处理程序添加日志记录和计时。

var spec = ":8086"  // localhost

func main() {
     // regular HTTP request handler
     handler := func(w http.ResponseWriter, req *http.Request) {
          fmt.Printf("in handler %v %v\n", req.Method, req.URL)
          time.Sleep(1 * time.Second)
          w.Write([]byte(fmt.Sprintf("In handler for %s %s", req.Method, req.URL)))
     }
     // advised handler
     http.HandleFunc("/test", LogWrapper(ElapsedTimeWrapper(handler)))
     if err := http.ListenAndServe(spec, nil); err != nil {
          log.Fatalf("Failed to start server on %s: %v", spec, err)
     }
}

Listing 9-3Advice/Middleware for HTTP Requests (Part 2)

运行者:

它生成以下日志输出:

entered handler for GET /test
in handler GET /test
elapsed time for GET /test: 1000141900ns
exited handler for GET /test

在这里,不同的中间件增加了对日志和计时的关注;原始处理程序不会受到任何影响。HTTP 引擎也不是。可以应用任意数量的包装器(以增加一些执行时间为代价)。一个成熟的 AOP 系统可能会自动应用这样的中间件,但是也可以像前面显示的那样手动应用。

十、单元测试和基准测试

对代码进行频繁而全面的单元测试是最佳实践。让可重复的(和自动化的)测试用于变更后的回归测试也是最佳实践。通常,这些实践是结合在一起的。类似地,经常运行代码性能基准测试是最佳实践。通常,一个人应该创建一个尽可能接近 100%代码覆盖率的测试套件。 1 虽然没有在本文中深入讨论,Go 标准运行时支持代码覆盖测试。一种被称为测试驱动开发 2 (TDD)的开发风格,强调在任何被测试的代码 (CUT)(即要测试的代码)被创建之前,创建所有的测试用例。

Go 标准运行时提供了运行 Go 代码的基本单元测试用例和基准的方法。它还支持 Go 程序的高级分析,但在本文中不会深入讨论。有关更多信息,请参见 Go 文档。对于 Java,类似的支持需要社区提供的库和框架。Go 测试框架类似于 JavaJUnit3框架,尤其是它的早期版本(在 Java 注释出现之前)。

Go 测试套件通常提供多个单元测试(一个单元是少量的相关代码,通常是一个函数,或者可能是一个具有相关方法的类型,或者可能是一个具有几个类型和函数的包)。您还可以创建功能测试(测试一组复杂的类型和函数,以查看它们是否像预期的那样作为一个集合工作)。其他测试,如性能、系统、安全、负载等。,但扩展了标准测试功能。与 Java 一样,Go 社区为这些更高级的测试提供了增强的测试和基准支持。

下面提供了用 Go 和 Java 创建和运行测试用例的例子。这两个语言示例都没有涵盖库的全部功能。参见 https://golang.org/pkg/testing/ 的 Go 测试文档。

Go 中的测试案例和基准

测试概念最好用例子来解释。首先,样品切割如清单 10-1 所示。

package main

import (
     "errors"
     "math/big"
     "math/rand"
     "time"
)

// a set of functions to be tested

// Echo my input

func EchoInt(in int) (out int) {
     randomSleep(50 * time.Millisecond)
     out = in
     return
}

func EchoFloat(in float64) (out float64) {
     randomSleep(50 * time.Millisecond)
     out = in
     return
}

func EchoString(in string) (out string) {
     randomSleep(50 * time.Millisecond)
     out = in
     return
}

// Sum my inputs

func SumInt(in1, in2 int) (out int) {
     randomSleep(50 * time.Millisecond)
     out = in1 + in2
     return
}

func SumFloat(in1, in2 float64) (out float64) {
     randomSleep(5)
     out = in1 + in2
     return
}

func SumString(in1, in2 string) (out string) {
     randomSleep(50 * time.Millisecond)
     out = in1 + in2
     return
}

// Factorial computation: factorial(n):
// n < 0 - undefined
// n == 0 - 1
// n > 0 - n * factorial(n-1)

var ErrInvalidInput = errors.New("invalid input")

// Factorial via iteration
func FactorialIterate(n int64) (res *big.Int, err error) {
     if n < 0 {
          err = ErrInvalidInput
          return
     }
     res = big.NewInt(1)
     if n == 0 {
          return
     }
     for  i := int64(1); i <= n; i++ {
          res.Mul(res, big.NewInt(i))
     }
     return
}

// Factorial via recursion
func FactorialRecurse(n int64) (res *big.Int, err error) {
     if n < 0 {
          err = ErrInvalidInput
          return
     }
     res = big.NewInt(1)
     if n == 0 {
          return
     }
     term := big.NewInt(n)
     facm1, err := FactorialRecurse(n - 1)
     if err != nil {
          return
     }
     res = term.Mul(term, facm1)
     return
}

// a helper

func randomSleep(dur time.Duration ) {
     time.Sleep(time.Duration((1 + rand.Intn(int(dur)))))
}

Listing 10-1Code Under Test

请注意,阶乘函数使用 big Int类型,以便可以表示阶乘的(相当大的)结果。

现在是测试用例。

每个测试用例都是这个表单的一个函数:

func TestXxx(t *testing.T) {
     expect := <expected vale>
     got := <actual value from CUT>
     if got != expect {
          reportNoMatch(t, got, expect)
     }
}

请注意,通常用“想要”这个词来代替“期望”

所有测试用例都以所需的“test”前缀和特定的测试用例名称开始。每个函数都有一个类型参数T来链接到测试库。

每个基准都是以下形式的函数:

func BenchmarkXxx(b *testing.B) {
     for i := 0; i < b.N; i++ {
          <do something to be timed>
     }
}

所有基准测试都以所需的“基准”前缀和特定的测试用例名称开始。每个函数都有一个类型参数B来链接到测试库。

测试用例与基准通常放在一个文件中,格式为XXX_test.go,其中 XXX 是测试套件的名称。“_test”后缀是必需的,因此测试用例运行人员知道不要寻找要调用的main函数。这可能很方便,因为不需要创建main包和main函数来运行代码的测试用例,而没有测试用例运行器则需要这样做。

通常,CUT 和测试套件在同一个包/目录中,如图 10-1 所示。

img/516433_1_En_10_Fig1_HTML.jpg

图 10-1

被测代码和相应的测试用例

清单 10-2 是一些样本测试用例。

请注意阶乘带来的巨大结果,即使输入值相对较小,如 100。随着输入变大,阶乘快速增长(以数字表示)。没有一个正常的整数(比如说uint64)可以开始持有这样的结果。

package main

import (
     "fmt"
     "math/big"
     "os"
     "testing"
     "time"
)
const factorialnput = 100
const factorialExpect = "93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000"

// test the functions; happy case only

func TestEchoInt(t *testing.T) {
     //fmt.Println("in TestEchoInt")
     expect := 10
     got := EchoInt(expect)
     if got != expect {
          reportNoMatch(t, got, expect)
     }
}

func TestSumInt(t *testing.T) {
     //fmt.Println("in TestSumInt")
     expect := 10
     got := SumInt(expect, expect)
     if got != expect+expect {
          reportNoMatch(t, got, expect+expect)
     }
}

func TestEchoFloat(t *testing.T) {
     //fmt.Println("in TestEchoFloat")
     expect := 10.0
     got := EchoFloat(expect)
     if got != expect {
          reportNoMatch(t, got, expect)
     }
}

func TestSumFloat(t *testing.T) {
     //fmt.Println("in TestSumFloat")
     expect := 10.0
     got := SumFloat(expect, expect)
     if got != expect+expect {
          reportNoMatch(t, got, expect+expect)
     }
}

func TestEchoString(t *testing.T) {
     fmt.Println("in TestEchoString")
     expect := "hello"
     got := EchoString(expect)
     if got != expect {
          reportNoMatch(t, got, expect)
     }
}

func TestSumString(t *testing.T) {
     //fmt.Println("in TestSumString")
     expect := "hello"
     got := SumString(expect, expect)
     if got != expect+expect {
          reportNoMatch(t, got, expect+expect)
     }
}

func TestFactorialIterate(t *testing.T) {
     //fmt.Println("in TestFactorialIterate")
     expect := big.NewInt(0)
     expect.SetString(factorialExpect, 10)
     got, err := FactorialIterate(factorialnput)
     if err != nil {
          reportFail(t, err)
     }
     if expect.Cmp(got) != 0 {
          reportNoMatch(t, got, expect)
     }
}

func TestFactorialRecurse(t *testing.T) {
     //fmt.Println("in TestFactorialRecurse")
     expect := big.NewInt(0)
     expect.SetString(factorialExpect, 10)
     got, err := FactorialRecurse(factorialnput)
     if err != nil {
          reportFail(t, err)
     }
     if expect.Cmp(got) != 0 {
          reportNoMatch(t, got, expect)
     }
}

// benchmarks

func BenchmarkFacInt(b *testing.B) {
     for i := 0; i < b.N; i++ {
          FactorialIterate(factorialnput)
     }
}

func BenchmarkFacRec(b *testing.B) {
     for i := 0; i < b.N; i++ {
          FactorialRecurse(factorialnput)
     }
}

// helpers

func reportNoMatch(t *testing.T, got interface{}, expect interface{}) {
     t.Error(fmt.Sprintf("got(%v) != expect(%v)", got, expect))
}

func reportFail(t *testing.T, err error) {
     t.Error(fmt.Sprintf("failure: %v", err))
}

var start time.Time

// do any test setup
func setup() {
     // do any setup here
     fmt.Printf("starting tests...\n")
     start = time.Now()
}

// do any test cleanup
func teardown() {
     end := time.Now()
     // do any cleanup here
     fmt.Printf("tests complete in %dms\n", end.Sub(start)/time.Millisecond)
}

// runs test with setup and cleanup
func TestMain(m *testing.M) {
     setup()
     rc := m.Run()
     teardown()
     os.Exit(rc)
}

Listing 10-2Test Cases and Benchmarks

使用go test {<option>...}命令运行测试和/或基准。一个关键选项是-bench=<re>,它指定了一个正则表达式(<re>),通常是“.”对于所有人来说。如果未指定,则不运行任何基准。请注意,基准测试会使测试套件花费大量时间运行,因此您可能不想每次都运行它们。

以下是前面在 ide 中运行的套件的结果:

GOROOT=C:\Users\Administrator\sdk\go1.14.2 #gosetup
GOPATH=C:\Users\Administrator\IdeaProjects;C:\Users\Administrator\IdeaProjects\LifeServer;C:\Users\Administrator\go #gosetup
C:\Users\Administrator\sdk\go1.14.2\bin\go.exe test -c -o C:\Users\Administrator\AppData\Local\Temp\1\___CodeUnderTest_test_go.exe samples #gosetup
C:\Users\Administrator\sdk\go1.14.2\bin\go.exe tool test2json -t C:\Users\Administrator\AppData\Local\Temp\1\___CodeUnderTest_test_go.exe -test.v -test.run "^TestEchoInt|TestSumInt|TestEchoFloat|TestSumFloat|TestEchoString|TestSumString|TestFactorialIterate|TestFactorialRecurse$" -test.bench=. #gosetup
starting tests...
=== RUN   TestEchoInt
--- PASS: TestEchoInt (0.05s)
=== RUN   TestSumInt
--- PASS: TestSumInt (0.02s)
=== RUN   TestEchoFloat
--- PASS: TestEchoFloat (0.03s)
=== RUN   TestSumFloat
--- PASS: TestSumFloat (0.00s)
=== RUN   TestEchoString
in TestEchoString
--- PASS: TestEchoString (0.01s)
=== RUN   TestSumString
--- PASS: TestSumString (0.03s)
=== RUN   TestFactorialIterate
--- PASS: TestFactorialIterate (0.00s)
=== RUN   TestFactorialRecurse
--- PASS: TestFactorialRecurse (0.00s)
goos: windows
goarch: amd64
pkg: samples
BenchmarkFacInt
BenchmarkFacInt-48            76730          15441 ns/op
BenchmarkFacRec
BenchmarkFacRec-48            52176          23093 ns/op
PASS
tests complete in 2924ms

Process finished with exit code 0

在这个例子中,所有的测试都通过了。这两个基准测试显示了迭代(较快,约 15μs)和递归(较慢,约 23μs)阶乘实现所用的显著不同的运行时间。这是意料之中的,因为递归实现会根据输入值增加大量额外的调用/返回开销。

请注意安装和拆卸代码添加的消息。还要注意,基准测试运行者根据每次迭代花费的时间选择了不同的迭代计数(for 循环中的 N)来进行测试。它通过在调用完整运行之前先做一些初步运行来做到这一点。

只是为了演示代码覆盖率,测试套件是带覆盖率运行的。图 10-2 显示了覆盖范围总结报告。

img/516433_1_En_10_Fig2_HTML.jpg

图 10-2

测试用例覆盖报告摘要

图 10-3 显示了应用了覆盖着色的剪切片段。

img/516433_1_En_10_Fig3_HTML.jpg

图 10-3

针对测试源的测试用例覆盖率指标

绿色部分(第 61、66、70–73 行)显示代码运行;黄色部分(第 62、67 行)显示只覆盖了部分路径的代码(通常是ifswitch语句)。红色部分(第 63–65、68、69 行)显示代码根本没有运行。该报告可以建议进行额外的测试用例来增加覆盖率。

一些 ide 可以运行 Go profiling 并生成报告和/或图表或结果。下面是一个针对测试套件运行概要文件的示例。在这些例子中很难看到细节(并且对我们的讨论来说不是关键的),但是仅仅使用标准的 Go 库和工具来分析代码的能力是非常强大的。在 Java 中,需要社区支持。

图 10-4 显示了 CPU 使用情况分析结果。

img/516433_1_En_10_Fig4_HTML.png

图 10-4

IDE 绘制的 CPU 配置文件结果

内存使用分析结果如图 10-5 所示。

img/516433_1_En_10_Fig5_HTML.png

图 10-5

IDE 绘制的内存分析结果

Java 测试用例

为了与 Java 进行比较,清单 10-2 结合了相似的切割和测试案例。JUnit 没有简单的基准特性,所以基准是手工构建的 4 作为测试用例。下面的 Java 例子使用了 JUnit 5。注意 JUnit 测试用例使用 Java 注释(比如@Test,vs .方法命名约定)来识别测试用例;不需要“测试”前缀。

package org.baf.test;

import static org.junit.jupiter.api.Assertions.fail;

import java.math.BigInteger;

import org.baf.CodeUnderTest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class CodeUnderTestTester {
  private static final String factorial100Expect = "93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000";

  static long start;
  static int limit = 10_000;

  @Test
  void testEchoInt() {
    int expect = 10;
    int got = CodeUnderTest.echoInt(expect);
    if (got != expect) {
      reportNoMatch(got, expect);
    }
  }

  @Test
  void testEchoFloat() {
    double expect = 10;
    double got = CodeUnderTest.echoFloat(expect);
    if (got != expect) {
      reportNoMatch(got, expect);
    }
  }

  @Test
  void testEchoString() {
    String expect = "hello";
    String got = CodeUnderTest.echoString(expect);
    if (!got.equals(expect)) {
      reportNoMatch(got, expect);
    }
  }

  @Test
  void testFactorialIterate() {
    BigInteger expect = new BigInteger(factorial100Expect);
    BigInteger got = CodeUnderTest.factorialIterative(100);
    if (!got.equals(expect)) {
      reportNoMatch(got, expect);
    }
  }

  @Test
  void testFactorialRecurse() {
    BigInteger expect = new BigInteger(factorial100Expect);
    BigInteger got = CodeUnderTest.factorialRecursive(100);
    if (!got.equals(expect)) {
      reportNoMatch(got, expect);
    }
  }

  @Test
  void benchmarkFactorialInt() {
    long start = System.currentTimeMillis();
    for (int i = 0; i < limit; i++) {
      CodeUnderTest.factorialIterative(1000);
    }
    long end = System.currentTimeMillis(), delta = end - start;
    System.out.printf("factorialIterativeve : iterations=%d, totalTime=%.2fs, per call=%.3fms%n", limit,
        (double) delta / 1000, (double) delta / limit);

  }

  @Test
  void benchmarkFactorialRec() {
    long start = System.currentTimeMillis();
    for (int i = 0; i < limit; i++) {
      CodeUnderTest.factorialRecursive(1000);
    }
    long end = System.currentTimeMillis(), delta = end - start;
    System.out.printf("factorialRecursive : iterations=%d, totalTime=%.2fs, per call=%.3fms%n", limit,
        (double) delta / 1000, (double) delta / limit);

  }

  @BeforeAll
  static void setUp() throws Exception {
    System.out.printf("starting tests...%n");
    start = System.currentTimeMillis();
  }

  @AfterAll
  static void tearDown() throws Exception {
    long end = System.currentTimeMillis();
    System.out.printf("tests complete in %dms%n", end - start);
  }

  private void reportNoMatch(Object got, Object expect) {
    fail(String.format("got(%s) != expect(%s)", got.toString(), expect.toString()));
  }

  private void reportFail(String message) {
    fail(String.format("failure: %s", message));
  }
}

Listing 10-3Java JUnit Test Cases

使用 Eclipse IDE,针对 CUT 运行测试用例。和 Go 例子一样,都通过了,如图 10-6 所示。

img/516433_1_En_10_Fig6_HTML.jpg

图 10-6

在 IDE 中运行 JUnit 报告

总结报告(展开)如图 10-7 所示。

img/516433_1_En_10_Fig7_HTML.jpg

图 10-7

IDE 中的 JUnit 运行摘要

测试输出以下摘要:

benchmarkFactorialInt : iterations=10000, totalTime=0.17s, per call=17400ns
benchmarkFactorialRec : iterations=10000, totalTime=0.11s, per call=10700ns

请注意,迭代和递归阶乘实现之间的运行时间差异很小。这意味着与计算阶乘相比,调用/返回开销很小;大部分时间是在阶乘法中。

将前面的 Java 结果与下面的 Go 结果进行对比。虽然不相同,但它们是相似的:

BenchmarkFacInt-48            76730          15441 ns/op
BenchmarkFacRec-48            52176          23093 ns/op

十一、走向未来

在我们结束学习 Go 的旅程之前,我们将简要了解一些未来可能的增强功能。然后,我们将以顶点计划的形式带来我们一起学到的东西,一个相当大的示例 web 服务器实现,希望也应该是有趣的。

下一部分中的 Go 库介绍和调查部分提供了一些关于使用单个 Go 库的背景和示例,您应该在学习 capstone 示例时回顾一下。

在我们展望未来之前,让我们回顾一下过去。和 Java 一样,Go 的起源也不像今天这样。随着时间的推移,Go 实现已经发展成熟。它有一段持续改进的历史,而且肯定会继续下去。

Go 会随着时间不断进步。对语言和运行时库有许多提议的增强。有些人可能会进入 Go。有些建议不是向后兼容的。这些提议的改变是会出现在 Go 1.x 版本中,还是等到 Go 2.x 版本(如果有的话)才会出现,还有待确定。

在众多潜在的增强功能中,有两个关键的潜在增强功能正在讨论中。

改进的错误处理

Go 处理错误的惯用方式可能会很繁琐。在 https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md 中有一个改善这种情况的提议。

今天,一段典型的 Go 代码会处理这样的错误:

func FileCopy(fromPath, toPath string) (count int64, err error) {
      reader, err := os.Open(fromPath)
      if err != nil {
            return
      }
      defer reader.Close()
      writer, err := os.Create(toPath)
      if err != nil {
            return
      }
      defer writer.Close()
      count, err = io.Copy(writer, reader)
      return
}

这是重复的。此外,代码的很大一部分是错误处理。

即使进行了所有这些检查,即使复制功能失败,输出文件也可以保留;在生产中应将其移除。此外,报告的错误可能无法提供足够的上下文信息来从原因中恢复。事情应该会好起来。提议的设计允许类似这样的事情:

func FileCopy(fromPath, toPath string) (count int64, err error) {
      handle err {
            return fmt.Errorf("copy(%q,%q) failed: %v",
            fromPath, toPath, err)
      }
      reader := check os.Open(fromPath)
      defer reader.Close()
      writer := check os.Create(toPath)
      handle err {
            writer.Close()
            os.Remove(toPath) // clean up
      }
      count = check io.Copy(writer, reader)
      return
}

另一个简单的例子:

func main() {
      handle err {
            log.Fatalf("main failed due to %v", err)
      }
      text := check ioutil.ReadAll(os.Stdin)
      check os.Stdout.Write(text)
}

这里,handle语句的工作有点像defer语句,处理错误并将它们传递给前面的句柄块,直到完成一个return。action 语句上的check前缀寻找(并消耗)一个返回的错误值,如果存在,触发handle链将错误传递给它。注意,冗长的if/return习惯用法被替换为使用check。这使得错误处理更像是紧急处理。

去仿制药

当前 Go 定义中似乎缺少的另一个关键领域是创建泛型类型的能力。这是被定义为 2 和现在被接受的 3 的增强。作为预览,这里介绍一下 Go 中的泛型会是什么样子。这篇简介并不打算涵盖泛型的所有方面。

提案中概括了这个概念: 4 我们建议 扩展 Go 语言,在类型和函数声明中添加可选的类型参数 。类型参数受接口类型的约束。当用作类型约束时,接口类型允许列出可能分配给它们的类型集。在许多情况下,通过统一算法的类型推断允许从函数调用中省略类型参数。 设计完全向后兼容 Go 1

…术语 泛型 是…一种简写,意思是采用类型参数的函数或类型。

在 Go 中,这些类型参数用方括号([...])括起来。在 Java 中,尖括号(<...>)有类似的作用。例如,考虑这个通用函数:

func DoItT any {
      :
}

其中[T any]定义了一个可以采用任何类型的类型参数。注意,这里的anyinterface{}的同义词。

Java 的对等物是

public <T> void doIt(List<T> s) {
    :
}

提案进一步细化为 5 ,并对提议的变更进行了总结:

  • 函数可以有一个额外的类型参数列表,它使用方括号,但在其他方面看起来像一个普通的参数列表: func FT any { ... }

  • 这些类型参数可以被常规参数和函数体使用。

  • 类型也可以有一个类型参数表: type MySlice[T any] []T

  • 每个类型参数都有一个类型约束,就像每个普通参数都有一个类型: func FT Constraint { ... }

  • 类型约束是接口类型。

  • 新的预声明名称 any 是一个允许任何类型的类型约束。

  • 用作类型约束的接口类型 可以有一个预声明类型的列表;只有与这些类型之一匹配的类型参数才满足约束。

  • 通用函数只能使用其类型约束所允许的操作。

  • 使用泛型函数或类型需要传递类型参数。

  • 类型推断允许在一般情况下省略函数调用的类型参数。

在 Java 中,泛型只能在引用(对象及其子类)类型上,而不能在基本类型上。Go 没有这种限制。

这个 Go 函数可以通过为 T 类型参数提供一个实类型来使用。例如:

DoItint

其中通用类型被设置为int。通常,编译器可以推断出这种类型,让它写成

DoIt([]int{1, 2, 3, 4, 5})

这类似于非一般呼叫。

通常,人们需要限制(约束)一个泛型类型可以采用的实际类型。通常,这意味着一个限制类型的接口,这样就可以调用它的方法。

假设我们想将类型参数限制为符合fmt.Stringer接口的东西。为此,有人写道

func DoItT fmt.Stringer {
      :
}

Java(接近)的对等词是

public <T extends Stringer> void doIt(List<T> s) {
    :
}

像 Java 一样,一个函数可以有多个类型参数,比如

func DoItS fmt.Stringer, R io.Reader {
      :
}

这里,这些类型参数是不同的(即使基于相同的实类型)。

以下示例摘自 Go proposal 网站的内容。

类型也可以是泛型的。考虑这种通用切片类型:

type Slice[T any] []T

要实现(或实例化)这样的类型参数,需要提供一个类型,如下所示:

var s Slice[int]

与非泛型类型一样,泛型类型也可以有方法:

func (v *Slice[T]) AddToEnd(x T) {
      *v = append(*v, x)
}

为了保持简单,实现有一些限制。方法被限制为与其作为方法的类型相同的类型参数;它不能再增加了。没有计划允许泛型类型参数或复合文本上的反射。

若要对泛型类型执行操作,必须将该类型映射到具有该操作的真实类型。在 Go 中,操作通常仅限于预定义的类型。因此,需要将泛型类型限制为一个或多个预定义的类型。通过声明枚举允许的预定义类型的接口类型可以做到这一点。例如:

type SignedInt interface {
  type int, int8, int16, int32, int64
}

它表示任何有符号整数类型或从它们派生的任何类型。这不是普通接口,不能用作基类型,只能用作约束。常用的分组接口将可能由标准的 Go 运行时提供。

any预定义约束一样,comparable约束允许任何支持相等比较的类型。

使用切片的通用函数示例: 6

func SliceMapT1, T2 any T2) (res []T2) {
      res := make([]T2, len(s))
      for i, v := range s {
            res[i] = mapper(v)
      }
      return
}
func SliceReduceT1, T2 any T2) (acc T2) {
      acc := first
      for _, v := range s {
            acc = reducer(acc, v)
      }
      return
}
func SliceFilterT any bool) (match []T) {
      match = make([]T, 0, len(s))
      for _, v := range s {
            if pred(v) {
                  match = append(match, v)
            }
      }
      return
}

注意,Go 的泛型类型没有 Java 泛型可能有的对立/协变问题 7 。这是因为 Go 不支持类型继承。在 Go 中,每个类型都是不同的,不能混合在一个泛型类型中,比如集合。

为了更早地了解泛型的作用,可以使用扩展的 Go Playground。 8 在图 11-1 中,我们看到了对整数和浮点数值的min(最小值)函数的通用版本。

img/516433_1_En_11_Fig1_HTML.jpg

图 11-1

支持泛型的操场示例

其他样品也可用,如图 11-2 所示。

img/516433_1_En_11_Fig2_HTML.jpg

图 11-2

可用的通用示例

Capstone``Game of Life

关于 Go 的最后一节提供了一个重要的编码示例,又名 capstone ,用 Java 和 Go 语言编写。Go capstone 实现演示了许多基本的 Go 函数、goroutines 的使用以及 HTTP 服务器实现。Java capstone 实现演示了如何用 Java 编写一个类似的程序来进行比较。

顶点程序玩的是约翰·康威定义的生命 9 (GoL)的游戏。这个游戏是一个零玩家游戏,在一个伪培养皿 10 一样的约束环境中模拟多代(又名回合或循环)的“微生物生命”。在这种情况下,培养皿由包含活(填充)或死(空)细胞的细胞矩形网格表示。根据康威的规则,游戏的迭代(离散时间步骤)导致细胞停留在停滞状态,繁殖或死亡。

根据网格的大小,“活着的”细胞的初始位置,以及进行的世代数,可以出现许多可能的、通常是有趣的模式。通常,这些模式最终会循环往复或变得固定不变。一些模式最终导致一个空的(全死的)网格。

游戏的一般规则是,在一代人的时间里

  • 任何有三个相邻活细胞的死细胞都会变成活细胞。

  • 任何只有两三个相邻活细胞的活细胞仍然活着;其他活细胞死亡。

  • 所有其他死亡的细胞仍然是死的。

图 11-3 提供了一个正在进行的不同 GoL 实施的示例快照。注意黑色的活细胞和灰色的死细胞。随着游戏的进行(也称为循环),细胞从活的移动到死的,或者从死的移动到活的,因此可以看起来在网格周围移动和/或改变群体形状。

img/516433_1_En_11_Fig3_HTML.png

图 11-3

生命网格的示例游戏

图 11-3 取自 http://pi.math.cornell.edu/~lipa/mec/lesson6.html

顶点高尔夫网格通过加载一个 PNG 11 图像来初始化。这可以是一个真实的图像,比如一张照片或一幅漫画,或者一些(通常)带有特定单元格集的更小的网格,通常在一个程序中创建,比如微软 Paint 如图 11-4 所示。

img/516433_1_En_11_Fig4_HTML.jpg

图 11-4

在 Microsoft Paint 中创建高尔夫网格

这可以保存为 PNG 格式,如图 11-5 所示。

img/516433_1_En_11_Fig5_HTML.jpg

图 11-5

在 Microsoft Paint 中保存高尔夫网格

图像的宽度和高度设置网格的宽度和高度(本例中为 100 x 100)。如果图像是彩色的,它将被映射为黑白(BW)。RGB 总和高于阈值的颜色被视为白色,否则被视为黑色。详情见 GoL 代码。黑色表示活细胞。一旦加载,就会运行一组预定的周期并保存在内存中(GoL 程序的生产版本可能会使用文件系统或数据库来保存以前生成的周期)。

有一个 web API(类似 REST ),允许用户在浏览器中查看生成的结果。提供了两种图像格式:

  1. GIF–允许显示一系列循环(动画)

  2. PNG–允许显示单个周期,通常比 GIF 提供的分辨率更高

请参见代码清单下面的图像示例。

程序接受这些可选的命令行标志参数(更多细节参见 Go flag包) :

  • 用来引用游戏的名称

  • 图像的 URL(文件或 HTTP)

  • 放大系数(1–20)

  • 启动 HTTP 服务器的标志(默认为真)

  • 启用游戏计时循环的标志(默认为假)

  • 启用报告呈现统计信息的标志(默认为 false)

  • 允许将生成的图像保存到文件中的标志(默认为 false)

如果存在的话,名称url 命令行参数会导致在服务器启动之前发生“播放”动作。这可以预载 GoL 结果。

注意各种标志的出现主要是为了演示命令行参数处理,而不是为了使 GoL 程序非常复杂;由于标志处理不是此顶点的关键点,因此标志可能无法可靠地实现。一个生产游戏可能会更加注意标志,并有更多的标志(如服务器端口值)。

程序的服务器部分提供了这些 API 路径:

  • GET /play–加载图像,并在图像上打高尔夫球;返回 JSON 关于播放过程的统计数据。

  • 查询参数:

  • 名称–引用游戏的名称

  • url–图像的 URL(文件或 HTTP)

  • GET /show–以图像形式返回之前运行的游戏。

  • 查询参数:

  • 名称–引用游戏的名称。

  • 表单-图像格式:GIF 或 PNG 如果是 GIF,可能是动画(包含几个循环)。

  • maxCount–返回的最大周期数(直到保存的周期数)。

  • mag–放大系数(1–20)。

  • GET /history–返回玩过游戏的历史记录。这种历史在高尔夫比赛中并不持久。

  • 清空已玩游戏的历史记录。

注意 GoL 代码通常用于本地(file:协议)初始电路板图像和网络(http:协议)图像。所有示例都使用本地图像。

前面的 API 不太符合标准的 REST 定义。它们的存在只是为了演示基本的 HTTP 请求处理。通常,带有嵌入式路径参数和其他选项的 production REST APIs 的匹配和解析会更加复杂。在这种情况下,使用 Go 标准库解析支持会很有挑战性。Go 社区有几个扩展了标准支持的库,例如, Gorilla Mux、 12 、使这变得更容易。这些库可以使 API 匹配与 Java 的 JAX-RS 和 Spring MVC 提供的相媲美。

服务器启动并监听端口 8080。

GoL 的计算量可能很大,特别是对于大网格,因为时间复杂度是 O(n ) 。因为工作很容易划分(每个单元的新周期状态只依赖于它的前一个周期的近邻),所以这是一个很好的例子来演示如何使用 goroutines 以较少的编码工作来提高程序性能。

游戏人生 Go 源码

将给出 GoL 实现的两种形式,一种用 Java,另一种用 Go。这允许对用 Go 和 Java 编写的实际程序进行详细的点对点实现比较。

两个版本中的大部分核心代码本质上是相同的。一般来说,相同的变量、类型和函数名(但大小写不同以匹配语言风格)用于帮助匹配实现部分。这种比较还显示了 Go 通常如何能够实现 Java SE 能够以相当的努力实现的大多数程序。

为了尽可能直接地进行比较,首先编写并测试 Go 表单,然后基于它创建 Java 实现。这意味着 Java 代码通常遵循 Go 习惯用法,没有充分利用 Java 语言(例如,使用 Lambdas 和 streams)。Java 版本与 Go 版本在以下关键方面有所不同:

  • 异常的使用(与 Go 风格的错误返回相比)

  • HTTP 服务器库差异

  • 图像库差异

  • 命令行解析差异(Java 没有解析命令行的标准库)

  • 将所有函数包装在某个类中

在 Java 代码中,通常只使用公共和默认可见性。这是为了匹配 Go 的可视性。大多数 Java 开发人员会选择使用私有可见性,而不是默认可见性。可见性是显式的(而不是由标识符大小写暗示的)。

与 Go 代码一样,Java 代码通常仅限于使用标准(相对于社区)库。Go 支持从结构创建标准的 JSON 和 XML。Java 没有。JSE 没有标准的 JSON 渲染器,所以实现了一个基本的,而不需要社区实现。一个等效的定制 XML 渲染器更复杂,并且对顶点的目的不重要,所以没有实现。Go 版本使用标准库 JSON 和 XML 呈现器。

Java 版本使用半标准的 HTTP 服务器com.sun.net.httpserver.HttpServer。它随 Java SDK 一起提供,但不使用标准的 Java 包名。比如说,要求社区支持 JAX-RS 或 Spring MVC 的选择违反了这个顶点的只能使用标准库的限制。只使用 Java HTTP 包来编写 HTTP 服务器将是一项非常大的工作。Go 版本使用标准的库 HTTP 服务器。

将首先呈现 Java 版本,然后是 Go 版本。一旦你理解了 Java 的实现,Go 版本应该很容易上手。您应该注意到实现中的高度相似性,这是故意的。检查代码时,在 Java 和 Go 源代码之间切换可能会有所帮助。或者你可以从 Go 实现开始,参考 Java 实现。做你觉得最舒服的事。

注意,顶点程序的 Java 代码是使用 Eclipse IDE 开发的,Go 代码是使用 IntelliJ IDEA IDE 开发的。这对代码的格式有一些影响。

Java 中的顶点项目

GoL 程序的 Java 形式由一个单独的org.baf.gol包组成,该包包含以下源文件:

  • Main.java——主命令处理器

  • Server.java——HTTP 服务器和请求处理器

  • Game.java——高尔夫的游戏逻辑

  • Utility.java-助手功能

  • AnnimatedGifWriter.java——一个将多张图片组合成 GIF 的助手

  • Logger.java——一个简单的 Go 记录器

  • ParameterProvider.java——一个帮助器接口,用于在类之间传递命令行参数(在 Go 版本中使用公共全局值)

  • Formatter.java——格式化地图的助手界面

  • JsonFormatter.java——将地图格式化成 JSON 的格式化程序

  • XmlFormatter.java——将地图格式化为 XML 的格式化程序(未实现)

注意前四个来源也存在于 Go 版本中。它们在两个版本中的功能几乎相同。需要其他资源来处理 Java 与 Go 运行时的差异。

注意这个例子使用了 Java 14 13 特性,并且必须至少在那个版本上编译和运行。在该版本中预览了一些使用的 Java 特性。

Main.java

Main.java 包含了main函数,它可以检测任何命令行参数并进行处理。它使用定制但简单的代码来处理任何命令行标志。它还可以选择启动 HTTP 服务器。

清单 11-1(Main.java)包含了这段代码。

package org.baf.gol;

import  org.baf.gol.Logger;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * Main GoL engine.
 */
public class Main implements ParameterProvider {
  // command line values
  String urlString, nameString;
  int magFactorInt = 1, gameCycles = 10;
  boolean startServerFlag, runTimingsFlag, reportFlag, saveImageFlag;
  public static String saveImageRoot = "/temp"; // change per OS type

  @Override
  public String getUrlParameter() {
    return urlString;
  }

  @Override
  public String getNameParameter() {
    return nameString;
  }

  @Override
  public int getMagFactorParameter() {
    return magFactorInt;
  }

  @Override
  public boolean startServerFlag() {
    return startServerFlag;
  }

  @Override
  public boolean runTimingsFlag() {
    return runTimingsFlag;
  }

  @Override
  public boolean reportFlag() {
    return reportFlag;
  }

  @Override
  public boolean saveImageFlag() {
    return saveImageFlag;
  }

  @Override
  public int getGameCyclesParameter() {
    return gameCycles;
  }

  /**
   * Main entry point.
   *
   * Sample: -n tiny1 -u file:/.../tiny1.png
   */
  public static void main(String[] args) {
    if (args.length == 0) {
      printHelp();
      return;
    }
    try {
      var main = new Main();
      if (!main.parseArgs(args)) {
        Logger.log.tracef("Command arguments: %s", Arrays.toString(args));
        printHelp();
        System.exit(1);
      }
      main.launch();
    } catch (Exception e) {
      Logger.log.exceptionf(e, "launched failed");
      System.exit(3);
    }
  }

  private void launch() throws Exception {
    Game.coreGame = new Game(this);
    Game.coreGame.saveImageRoot = saveImageRoot;
    Game.coreGame.maxCycles = gameCycles;

    // need timings
    if (!urlString.isEmpty()) {
      if (nameString.isEmpty()) {
        System.err.printf("a name is required when a URL is provided%n");
        System.exit(1);
      }
      if (runTimingsFlag) {
        runCycleTimings();
      }

    }

    // need server
    if (startServerFlag) {
      // launch HTTP server
      var server = new Server(this);
      server.saveImageRoot = saveImageRoot;
      server.startHttpServer();
    }
  }

  // approximation of flag package in Go
  private boolean parseArgs(String[] args) {
    boolean ok = true;
    try {
      for (int i = 0; i < args.length; i++) {
        switch (args[i].toLowerCase()) {
          case "-url":
          case "-u":
            urlString = args[++i];
            break;
          case "-name":
          case "-n":
            nameString = args[++i];
            break;
          case "-magfactor":
          case "-mf":
          case "-mag":
            magFactorInt = Integer.parseInt(args[++i]);
            if (magFactorInt < 1 || magFactorInt > 20) {
              throw new IllegalArgumentException("bad magFactor: " + magFactorInt);
            }
            break;
          case "-gamecycles":
          case "-gc":
            gameCycles = Integer.parseInt(args[++i]);
            if (gameCycles < 1 || gameCycles > 1000) {
              throw new IllegalArgumentException("bad gameCycles: " + gameCycles);
            }

            break;
          case "-start":
            startServerFlag = true;
            break;
          case "-time":
            runTimingsFlag = true;
            break;
          case "-report":
            reportFlag = true;
            break;
          case "-saveimage":
          case "-si":
            saveImageFlag = true;
            break;
          default:
            throw new IllegalArgumentException("unknown parameter key: " + args[i]);
        }
      }
    } catch (Exception e) {
      System.err.printf("parse failed: %s%n", e.getMessage());
      ok = false;
    }
    return ok;
  }

  // get execution timings
  private void runCycleTimings() throws Exception {
    var cpuCount = Runtime.getRuntime().availableProcessors();
    for (var i = 1; i <= 64; i *= 2) {
      Logger.log.tracef("Running with %d threads, %d CPUs...", i, cpuCount);
      Game coreGame = Game.coreGame;
      coreGame.threadCount = i;
      coreGame.run(getNameParameter(), getUrlParameter());

      if (reportFlag()) {
        Logger.log.tracef("Game max: %d, go count: %d:", i, coreGame.maxCycles, coreGame.threadCount);
        for (var grk : coreGame.runs.keySet()) {
          var gr = coreGame.runs.get(grk);
          Logger.log.tracef("Game Run: %s, cycle count: %d", gr.name, gr.cycles.size());
          for (var c : gr.cycles) {
            long start = c.startedAt.getTime(), end = c.endedAt.getTime();
            Logger.log.tracef("Cycle: start epoch: %dms, end epoch: %dms, elapsed: %dms", start, end, end - start);
          }

        }
      }
    }
  }

  private static void printHelp() {
    System.err.printf("%s%n%n%s%n", trimWhitespace(golDescription), trimWhitespace((golArgs)));
  }

  private static Object trimWhitespace(String lines) {
    var xlines = lines.split("\n");
    var result = new ArrayList<String>();
    for (int i = 0, c = xlines.length; i < c; i++) {
      String tline = xlines[i].trim();
      if (!tline.isEmpty()) {
        result.add(tline.replace("%n", "\n"));
      }
    }
    return String.join("\n", result);
  }

  static String golDescription = """
       Play the game of Life.
       Game boards are initialized from PNG images.
       Games play over several cycles.%n
       Optionally acts as a server to retrieve images of game boards during play.%n
       No supported positional arguments.
      """;

  static String golArgs = """
      Arguments (all start with '-'):
      url|u <url>              URL of the PNG image to load
      name|n <name>            name to refer to the game initialized by the URL
      magFactor|mf|mag <int>   magnify the grid by this factor when formatted into an image  (default 1; 1 to 20)
      gameCycles|gc <int>      sets number of cycles to run (default 10)
      start <boolean>          start the HTTP server (default false)
      time <boolean>           run game cycle timings with different thread counts (default false)
      report <boolean>         output run statistics (default false)
      saveImage|si <boolean>   save generated images into a file (default false)
      """;
}

Listing 11-1Source File 1 Main.java

Server.java

Server.java 启动了一个带有几个路径处理器的 HTTP 服务器。处理程序访问任何查询参数,然后生成和/或访问 GoL 数据。Game.coreGame代表过去玩过的游戏的历史的根。返回存储在其中的图像或 JSON/XML 统计信息。

目前,标准 JRE 中不支持 JSON 必须使用第三方实现。通常情况下,Java 版本使用起来更复杂,但是功能更强大。

一些 Java JSON 处理器的例子:

  • 杰克逊14——流行;通常是 JEE 版本中的默认实现

  • gson15——来自谷歌,还需要我们多说吗

  • Json-io 16

  • genson【17】

这个例子尽可能使用标准库支持,所以 JSON 由定制代码处理。

Server.java(清单 11-2 )包含了这段代码。

package org.baf.gol;

import static org.baf.gol.Logger.log;
import static org.baf.gol.Utility.NANOS_PER_MS;
import static org.baf.gol.Utility.isNullOrEmpty;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

/**
 * Provides a HTTP server for the GoL.<br>
 * Uses com.sun.net.httpserver.HttpServer for basic function.<br>
 * Can be opened only one time.
 **/
public class Server implements AutoCloseable {
  private static final String GIF_IMAGE_FILE_PATTERN = "/Image_%s.gif";

  String address;
  int port;
  Map<String, HttpHandler> handlers = new LinkedHashMap<>();
  HttpServer server;
  ParameterProvider pp;
  public String saveImageRoot = "/temp"; // change per OS type

  public Server(ParameterProvider pp) {
    this(pp, "localhost", 8080);
  }

  public Server(ParameterProvider pp, String address, int port) {
    this.pp = pp;
    this.address = address;
    this.port = port;
  }

  @Override
  public String toString() {
    return "Server[address=" + address + ", port=" + port + ", open=" + isOpen() + ", handlers=" + handlers.keySet() + "]";
  }

  String getRequestPath(HttpExchange ex) {
    return ex.getRequestURI().toString().split("\\?")[0];
  }

  // assumes only one value; redo if more than one possible
  String getQueryParamValue(HttpExchange ex, String name) {
    String result = null;
    var parts = ex.getRequestURI().toString().split("\\?");
    if (parts.length > 1) {
      parts = parts[1].split("&");
      for (var part : parts) {
        var xparts = part.split("=");
        if (xparts[0].equals(name)) {
          result = xparts[1];
          break;
        }
      }
    }
    return result;
  }

  /**
   * Used to allow clients outside this class to send data.
   */
  public interface ResponseDataSender {
    void sendResponseData(byte[] data) throws IOException;
  }

  public class DefaultResponseDataSender implements ResponseDataSender {
    HttpExchange ex;

    public DefaultResponseDataSender(HttpExchange ex) {
      this.ex = ex;
    }

    @Override
    public void sendResponseData(byte[] data) throws IOException {
      Server.this.sendResponseData(ex, data);
    }

  }

  void sendResponseData(HttpExchange ex, byte[] data) throws IOException {
    ex.sendResponseHeaders(200, data.length);
    var os = ex.getResponseBody();
    os.write(data);
    os.flush();
    os.close();
    log.tracef("Sent %d bytes", data.length);
  }

  void sendResponseJson(HttpExchange ex, Object data) throws IOException {
    ex.getResponseHeaders().add("Content-Type", "text/json");
    var jf = new JsonFormatter();
    sendResponseData(ex, jf.valueToText(data).getBytes());
  }

  void sendResponseXml(HttpExchange ex, Object data) throws IOException {
    ex.getResponseHeaders().add("Content-Type", "text/xml");
    var xf = new XmlFormatter();
    sendResponseData(ex, xf.valueToText(data).getBytes());
  }

  void sendStatus(HttpExchange ex, int status) throws IOException {
    ex.sendResponseHeaders(status, 0);
  }

// Show request handler.
  HttpHandler showHandler = new HttpHandler() {

    @Override
    public void handle(HttpExchange exchange) throws IOException {
      try {
        switch (exchange.getRequestMethod()) {
          case "GET": {
            if (!Objects.equals(getRequestPath(exchange), "/show")) {
              sendStatus(exchange, 404);
              return;
            }
            // process query parameters
            var name = getQueryParamValue(exchange, "name");
            if (isNullOrEmpty(name)) {
              name = "default";
            }
            var form = getQueryParamValue(exchange, "form");
            if (isNullOrEmpty(form)) {
              form = "gif";
            }
            var xmaxCount = getQueryParamValue(exchange, "maxCount");
            if (isNullOrEmpty(xmaxCount)) {
              xmaxCount = "50";
            }
            var maxCount = Integer.parseInt(xmaxCount);
            if (maxCount < 1 || maxCount > 100) {
              sendStatus(exchange, 400);
              return;
            }
            var xmag = getQueryParamValue(exchange, "mag");
            if (isNullOrEmpty(xmag)) {
              xmag = "1";
            }
            var mag = Integer.parseInt(xmag);
            var xindex = getQueryParamValue(exchange, "index");
            if (isNullOrEmpty(xindex)) {
              xindex = "0";
            }

            var index = Integer.parseInt(xindex);
            if (index < 0) {
              sendStatus(exchange, 400);
              return;
            }

            // get a game
            var gr = Game.coreGame.runs.get(name);
            if (gr == null) {
              sendStatus(exchange, 404);
              return;
            }

            // return requested image type
            switch (form) {
              case "GIF":
              case "gif": {
                var b = gr.makeGifs(maxCount, mag);
                sendResponseData(exchange, b);

                if (pp.saveImageFlag()) {
                  var imageFormat = saveImageRoot + GIF_IMAGE_FILE_PATTERN;
                  var saveFile = String.format(imageFormat, name);
                  Files.write(Paths.get(saveFile), b);
                  log.tracef("Save %s", saveFile);
                }
              }
                break;
              case "PNG":
              case "png": {
                if (index <= maxCount) {
                  var rs = new DefaultResponseDataSender(exchange);
                  gr.sendPng(rs, index, mag);
                } else {
                  sendStatus(exchange, 400);
                }
              }
                break;
              default:
                sendStatus(exchange, 405);
            }
          }

        }
      } catch (Exception e) {
        log.exceptionf(e, "show failed");
        sendStatus(exchange, 500);
      }
    }
  };

// Play request handler.
  HttpHandler playHandler = new HttpHandler() {

    @Override
    public void handle(HttpExchange exchange) throws IOException {
      try {
        switch (exchange.getRequestMethod()) {
          case "GET": {
            if (!Objects.equals(getRequestPath(exchange), "/play")) {
              sendStatus(exchange, 404);
              return;
            }
            // process query parameters
            var name = getQueryParamValue(exchange, "name");
            var url = getQueryParamValue(exchange, "url");
            if (Utility.isNullOrEmpty(name) || Utility.isNullOrEmpty(url)) {
              sendStatus(exchange, 400);
              return;
            }
            var ct = getQueryParamValue(exchange, "ct");
            if (Utility.isNullOrEmpty(ct)) {
              ct = exchange.getRequestHeaders().getFirst("Content-Type");
            }
            if (Utility.isNullOrEmpty(ct)) {
              ct = "";
            }
            ct = ct.toLowerCase();
            switch (ct) {
              case "":
                ct = "application/json";
                break;
              case "application/json":
              case "text/json":
                break;
              case "application/xml":
              case "text/xml":
                break;
              default:
                sendStatus(exchange, 400);
            }

            // run a game
            Game.coreGame.run(name, url);
            var run = makeReturnedRun(name, url);

            // return statistics as requested
            switch (ct) {
              case "application/json":
              case "text/json": {
                sendResponseJson(exchange, run);
              }
                break;
              case "application/xml":
              case "text/xml": {
                sendResponseXml(exchange, run);
              }
                break;
            }
          }
            break;
          default:
            sendStatus(exchange, 405);
        }
      } catch (Exception e) {
        log.exceptionf(e, "play failed");
        sendStatus(exchange, 500);
      }
    }
  };

// History request handler.
  HttpHandler historyHandler = new HttpHandler() {

    @Override
    public void handle(HttpExchange exchange) throws IOException {
      try {
        switch (exchange.getRequestMethod()) {
          case "GET": {
            if (!Objects.equals(getRequestPath(exchange), "/history")) {
              sendStatus(exchange, 404);
              return;
            }
            // format history
            Map<String, Object> game = new LinkedHashMap<>();
            var runs = new LinkedHashMap<>();
            game.put("Runs", runs);
            var xruns = Game.coreGame.runs;
            for (var k : xruns.keySet()) {
              runs.put(k, makeReturnedRun(k, xruns.get(k).imageUrl));
            }

            sendResponseJson(exchange, game);
          }
            break;
          case "DELETE":
            if (!Objects.equals(getRequestPath(exchange), "/history")) { // more is bad
              sendStatus(exchange, 404);
              return;
            }
            // erase history
            Game.coreGame.clear();
            sendStatus(exchange, 204);
            break;
          default:
            sendStatus(exchange, 405);
        }
      } catch (Exception e) {
        log.exceptionf(e, "history failed");
        sendStatus(exchange, 500);
      }
    }

  };

  Map<String, Object> makeReturnedRun(String name, String imageUrl) {
    var xrun = new LinkedHashMap<String, Object>();
    var run = Game.coreGame.runs.get(name);
    if (run != null) {
      xrun.put("Name", run.name);
      xrun.put("ImageURL", run.imageUrl);
      xrun.put("PlayIndex", run.playIndex);
      xrun.put("DelayIn10ms", run.delayIn10ms);
      xrun.put("Height", run.height);
      xrun.put("Width", run.width);
      xrun.put("StartedAMst", run.startedAt);
      xrun.put("EndedAMst", run.endedAt);
      xrun.put("DurationMs", run.endedAt.getTime() - run.startedAt.getTime());
      var cycles = new ArrayList<Map<String, Object>>();
      xrun.put("Cycles", cycles);
      for (var r : run.cycles) {
        var xc = new LinkedHashMap<String, Object>();
        xc.put("StartedAtNs", r.startedAt.getTime() * NANOS_PER_MS);
        xc.put("EndedAtNs", r.endedAt.getTime() * NANOS_PER_MS);
        var duration = (r.endedAt.getTime() - r.startedAt.getTime()) * NANOS_PER_MS;
        xc.put("DurationNs", duration);
        xc.put("Cycle", r.cycleCount);
        xc.put("ThreadCount", Game.coreGame.threadCount);
        xc.put("MaxCount", Game.coreGame.maxCycles);
        cycles.add(xc);
      }

    }
    return xrun;
  }

  public void startHttpServer() throws IOException {
    registerContext("/play", playHandler);
    registerContext("/show", showHandler);
    registerContext("/history", historyHandler);
    open();
    log.tracef("Server %s:%d started", address, port);
  }

  public void open() throws IOException {
    if (isOpen()) {
      throw new IllegalStateException("already open");
    }
    server = HttpServer.create(new InetSocketAddress("localhost", 8080), 0);
    for (var path : handlers.keySet()) {
      server.createContext(path, handlers.get(path));
    }

    server.start();
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      try {
        close();
      } catch (Exception e) {
        log.exceptionf(e, "shutdown failed");
      }
    }));
  }

  public boolean isOpen() {
    return server != null;
  }

  @Override
  public void close() throws Exception {
    if (isOpen()) {
      server.stop(60);
      server = null;
    }
  }

  public void registerContext(String path, HttpHandler handler) {
    if (handlers.containsKey(path)) {
      throw new IllegalArgumentException("path already exists: " + path);
    }
    handlers.put(path, handler);
  }

  public void removeContext(String path) {
    if (!handlers.containsKey(path)) {
      throw new IllegalArgumentException("unknown path: " + path);
    }

    handlers.remove(path);
  }

  public List<String> getContextPaths() {
    return handlers.keySet().stream().collect(Collectors.toUnmodifiableList());
  }
}

Listing 11-2Source File 2 Server.java

Game.java

Game.java 包含了打高尔夫球的逻辑。每个游戏由一组命名为的 GameRun 实例组成。每个 GameRun 由一组 GameCycle 实例和一些统计数据组成。每个游戏周期由一个前后网格快照和一些统计数据组成。每个网格都有单元格数据(作为byte[])和网格尺寸。REST show API 返回制作成图像的 after-grid 实例。

NextCycle方法内部的线程中调用processRows函数。这允许使用可变数量的线程。使用更多的线程可以显著加快 GoL 循环的处理速度,特别是对于较大的网格,如本节后面所示。Java 示例为 Go 版本使用 goroutine 的每个地方使用一个新线程。这在 Java 代码中是不典型的;通常使用线程池。

游戏的 Java 版本支持 Swing GUI 来显示循环。Go 版本没有对应的版本。GUI 实现包括在内,但对于与 Go 的比较并不重要。

Game.java(清单 11-3 )包含此代码。

package org.baf.gol;

import  org.baf.gol.Logger;

import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;

import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.border.TitledBorder;

import org.baf.gol.Server.ResponseDataSender;

/**
 * Represents a GoL Game with a set of Game runs.
 */
public class Game {
  public static Game coreGame; // global instance

  static int threadId;

  private int nextThreadId() {
    return ++threadId;
  }

  // play history
  public Map<String, GameRun> runs = new LinkedHashMap<>();

  public int maxCycles = 25; // max that can be played
  public int threadCount; // thread to use in timings
  ParameterProvider pp; // source of command line parameters
  public String saveImageRoot = "/temp"; // change per OS type

  public Game(ParameterProvider pp) {
    this.pp = pp;
  }

  /**
   * Represents a single run of a GoL Game.
   */
  public class GameRun {
    static final int offIndex = 255, onIndex = 0;
    static final int midvalue = 256 / 2; // separates black vs. white

    public Game parent;
    public String name;
    public String imageUrl;
    public Date startedAt, endedAt;
    public int width, height;
    public Grid initialGrid, currentGrid, finalGrid;
    public List<GameCycle> cycles = new ArrayList<>();
    public int delayIn10ms, playIndex;
    public int threadCount;

    private String author = "Unknown";

    public String getAuthor() {
      return author;
    }

    public void setAuthor(String author) {
      this.author = author;
    }

    public GameRun(Game game, String name, String url) throws Exception {
      this.parent = game;
      this.name = name;
      this.imageUrl = url;
      this.delayIn10ms = 5 * 100;

      // make the game grid and load initial state
      String[] kind = new String[1];
      BufferedImage img = Utility.loadImage(url, kind);
      Logger.log.tracef("Image kind: %s", kind[0]);
      if (!"png".equals(kind[0].toLowerCase())) {
        throw new IllegalArgumentException(
            String.format("named image %s is not a PNG", url));
      }
      var bounds = new Rectangle(img.getMinX(), img.getMinY(),
          img.getWidth(), img.getHeight());
      var size = bounds.getSize();
      initialGrid = new Grid(size.width, size.height);
      width = initialGrid.width;
      height = initialGrid.height;
      initGridFromImage(bounds.x, bounds.y, bounds.width, bounds.height, img);
      currentGrid = initialGrid.deepClone();
    }

    @Override
    public String toString() {
      return "GameRun[name=" + name + ", imageUrl=" + imageUrl +
          ", startedSt=" + startedAt + ", endedAt=" + endedAt
          + ", width=" + width + ", height=" + height +
          ", cycles=" + cycles + ", delayIn10ms=" + delayIn10ms
          + ", playIndex=" + playIndex + ", threadCount=" + threadCount + "]";
    }

    private void initGridFromImage(int minX, int minY, int maxX, int maxY,
        BufferedImage img) {
      for (int y = minY; y < maxY; y++) {
        for (int x = minX; x < maxX; x++) {
          var pixel = img.getRGB(x, y);
          int r = (pixel >> 16) & 0xFF,
              g = (pixel >> 8) & 0xFF,
              b = (pixel >> 0) & 0xFF;

          var cv = 0; // assume all dead
          if (r + g + b < midvalue * 3) {
            cv = 1; // make cell alive
          }
          initialGrid.setCell(x, y, cv);
        }
      }
    }

    public void sendPng(ResponseDataSender rs, int index, int mag)
        throws IOException {
      Grid grid = null;
      switch (index) {
        case 0:
          grid = initialGrid;
          break;
        default:
          index--;
          if (index < 0 || index >= cycles.size()) {
            throw new ArrayIndexOutOfBoundsException("bad index");
          }
          grid = cycles.get(index).afterGrid;
      }

      var img = new BufferedImage(width * mag + 1, height * mag + 1,
          BufferedImage.TYPE_BYTE_BINARY);
      fillImage(grid, mag, img);
      var b = encodePngImage(img);
      rs.sendResponseData(b);
      showImageInGui(img); // show in GUI

      if (parent.pp.saveImageFlag()) {
        var saveFile = String.format(saveImageRoot + "/Image_%s.gif", name);
        Files.write(Paths.get(saveFile), b);
        Logger.log.tracef("Save %s", saveFile);
      }
    }

    private byte[] encodePngImage(BufferedImage img) throws IOException {
      var baos = new ByteArrayOutputStream();
      var bos = new BufferedOutputStream(baos);
      var ios = new MemoryCacheImageOutputStream(bos);
      ImageIO.write(img, "png", ios);
      ios.flush();
      return baos.toByteArray();
    }

    private void fillImage(Grid grid, int mag, BufferedImage img) {
      for (var row = 0; row < grid.height; row++) {
        for (var col = 0; col < grid.width; col++) {
          var index = grid.getCell(col, row) != 0 ? onIndex : offIndex;
          // apply magnification
          for (var i = 0; i < mag; i++) {
            for (var j = 0; j < mag; j++) {
              img.setRGB(mag * col + i, mag * row + j,
                  index == onIndex ? 0 : 0x00FFFFFF);
            }
          }
        }
      }
    }

    /**
     * Run a game.
     */
    public void run() {
      this.threadCount = coreGame.threadCount;
      startedAt = new Date();
      int maxCycles = parent.maxCycles;
      for (int count = 0; count < maxCycles; count++) {
        nextCycle();
      }
      endedAt = new Date();
      Logger.log.tracef("GameRun total time: %dms, cycles: %d, thread count: %d", endedAt.getTime() - startedAt.getTime(),
          maxCycles, threadCount);
      finalGrid = currentGrid.deepClone();
    }

    // Advance and play next game cycle.
    // Updating of cycle grid rows can be done in parallel;
    // which can reduce execution time.
    private void nextCycle() {
      var gc = new GameCycle(this);
      gc.beforeGrid = currentGrid.deepClone();
      var p = gc.parent;
      var threadCount = Math.max(p.parent.threadCount, 1);
      gc.afterGrid = new Grid(gc.beforeGrid.width, gc.beforeGrid.height);
      gc.startedAt = new Date();
      var threads = new ArrayList<Thread>();
      var rowCount = (height + threadCount / 2) / threadCount;
      for (var i = 0; i < threadCount; i++) {
        var xi = i;
        var t = new Thread(() -> {
          procesRows(gc, rowCount, xi * rowCount, gc.beforeGrid, gc.afterGrid);
        }, "thread_" + nextThreadId());
        threads.add(t);
        t.setDaemon(true);
        t.start();
      }
      for (var t : threads) {
        try {
          t.join();
        } catch (InterruptedException e) {
          // ignore
        }
      }
      gc.endedAt = new Date();
      currentGrid = gc.afterGrid.deepClone();
      cycles.add(gc);
      gc.cycleCount = cycles.size();
    }

    // process all cells in a set of rows
    private void procesRows(GameCycle gc, int rowCount, int startRow,
        Grid inGrid, Grid outGrid) {
      for (var index = 0; index < rowCount; index++) {
        var rowIndex = index + startRow;
        for (var colIndex = 0; colIndex < width; colIndex++) {
          // count any neighbors
          var neighbors = 0;
          if (inGrid.getCell(colIndex - 1, rowIndex - 1) != 0) {
            neighbors++;
          }
          if (inGrid.getCell(colIndex, rowIndex - 1) != 0) {
            neighbors++;
          }
          if (inGrid.getCell(colIndex + 1, rowIndex - 1) != 0) {
            neighbors++;
          }
          if (inGrid.getCell(colIndex - 1, rowIndex) != 0) {
            neighbors++;
          }
          if (inGrid.getCell(colIndex + 1, rowIndex) != 0) {
            neighbors++;
          }
          if (inGrid.getCell(colIndex - 1, rowIndex + 1) != 0) {
            neighbors++;
          }
          if (inGrid.getCell(colIndex, rowIndex + 1) != 0) {
            neighbors++;
          }

          if (inGrid.getCell(colIndex + 1, rowIndex + 1) != 0) {
            neighbors++;
          }
          // determine next generation cell state based on neighbor count
          var pv = inGrid.getCell(colIndex, rowIndex);
          var nv = 0;
          switch (neighbors) {
            case 2:
              nv = pv;
              break;
            case 3:
              if (pv == 0) {
                nv = 1;
              }
              break;
          }
          outGrid.setCell(colIndex, rowIndex, nv);
        }
      }
    }

    /**
     * Make images from 1+ cycles into GIF form.
     */
    public byte[] makeGifs(int count, int mag) throws IOException {
      var cycleCount = cycles.size();
      var xcycles = Math.min(count, cycleCount + 1);
      List<BufferedImage> bia = new ArrayList<>();
      var added = addGridSafe(initialGrid, 0, xcycles, mag, bia);
      for (int i = 0; i < cycleCount; i++) {
        added = addGridSafe(cycles.get(i).afterGrid, added, xcycles, mag, bia);
      }
      return packGifs(added, mag, delayIn10ms,
          bia.toArray(new BufferedImage[bia.size()]));
    }

    int addGridSafe(Grid grid, int added, int max, int mag, List<BufferedImage> bia) {
      var img = new BufferedImage(mag * width + 1, mag * height + 1,
          BufferedImage.TYPE_BYTE_BINARY);
      if (added < max) {
        fillImage(grid, mag, img);
        bia.add(img);
        added++;
      }
      return added;
    }

    byte[] packGifs(int count, int mag, int delay, BufferedImage[] bia)
        throws IOException {
      showImagesInGui(bia);

      var baos = new ByteArrayOutputStream();
      var bos = new BufferedOutputStream(baos);
      var ios = new MemoryCacheImageOutputStream(bos);
      AnnimatedGifWriter.createGifs(ios, delay, author, bia);
      ios.flush();
      return baos.toByteArray();
    }

    // not in Go version.
    void showImagesInGui(BufferedImage[] bia) {
      // create a Swing Frame to show a row of images
      var frame = new JFrame("Show Images rendered at " + new Date());
      frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
      JPanel imagePanel = new JPanel(new GridLayout());
      var sp = new JScrollPane(imagePanel);
      frame.setContentPane(sp);
      frame.setSize(1000, 800);

      var index = 1;
      for (var bi : bia) {
        var icon = new ImageIcon(bi);
        JLabel labelledIcon = new JLabel(icon);
        labelledIcon.setBorder(new TitledBorder(String.format("Image: %d (%dx%d)", index++,
 icon.getIconWidth(), icon.getIconHeight())));
        imagePanel.add(labelledIcon);
      }
      frame.setVisible(true);
    }

    // not in Go version.
    void showImageInGui(BufferedImage bi) {
      var frame = new JFrame("Show Image rendered at " + new Date());
      JPanel imagePanel = new JPanel(new GridLayout());
      var sp = new JScrollPane(imagePanel);
      frame.setContentPane(sp);
      frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
      frame.setSize(1000, 800);
      var icon = new ImageIcon(bi);
      JLabel labelledIcon = new JLabel(icon);
      labelledIcon .setBorder(new TitledBorder(String.format("Image: (%dx%d)",
              icon.getIconWidth(), icon.getIconHeight())));
      imagePanel.add(labelledIcon);
      frame.setVisible(true);
    }

  }

  /**
   * Clear all runs.
   */
  public void clear() {
    runs.clear();
  }

  /**
   * Run a game.
   */
  public void run(String name, String url) throws Exception {
    var gr = new GameRun(this, name, url);
    runs.put(gr.name, gr);
    gr.run();
  }

  /**
   * Represents a GoL Game grid.
   */
  public static class Grid {
    public byte[] data;
    public int width, height;

    public Grid(int width, int height) {
      this.width = width;
      this.height = height;
      data = new byte[width * height];
    }

    @Override
    public String toString() {
      return "Grid[width=" + width + ", height=" + height + "]";
    }

    public int getCell(int x, int y) {
      if (x < 0 || x >= width || y < 0 || y >= height) {
        return 0;
      }
      return data[x + y * width];
    }

    public void setCell(int x, int y, int cv) {
      if (x < 0 || x >= width || y < 0 || y >= height) {
        return;
      }
      data[x + y * width] = (byte) cv;
    }

    public Grid deepClone() {
      var ng = new Grid(width, height);
      for (int i = 0; i < data.length; i++) {
        ng.data[i] = data[i];
      }
      ng.width = width;
      ng.height = height;
      return ng;
    }
  }

  /**
   * Represents a GoL Game cycle.
   */
  public static class GameCycle {
    public GameRun parent;
    public int cycleCount;
    public Date startedAt, endedAt;
    public Grid beforeGrid, afterGrid;

    public GameCycle(GameRun parent) {
      this.parent = parent;
    }

    @Override
    public String toString() {
      return "GameCycle[cycle=" + cycleCount + ", "
          + "startedAt=" + startedAt + ", endedAt=" + endedAt + "]";
    }
  }

}

Listing 11-3Source File 3 Game.java

Utility.java

Utility.java(清单 11-4 )提供了一些辅助函数和共享值。

Utility.java 包含此代码。

package org.baf.gol;

import static org.baf.gol.Logger.log;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;

import javax.imageio.ImageIO;

public class Utility {
  public static final int NANOS_PER_MS = 1_000_000;
  public static final String FILE_PREFIX = "file:";

  public static boolean isNullOrEmpty(CharSequence cs) {
    return cs == null || cs.length() == 0;
  }

  public static boolean isNullOrEmptyTrim(String cs) {
    return cs == null || cs.trim().length() == 0;
  }

  public static BufferedImage loadImage(String url, String[] kind) throws IOException {
    BufferedImage bi = null;
    if (url.startsWith(FILE_PREFIX)) {
      String name = url.substring(FILE_PREFIX.length());
      log.tracef("loadImage %s; %s", url, name);
      bi = ImageIO.read(new File(name));
    } else {
      var xurl = new URL(url);
      bi = ImageIO.read(xurl);
    }
    var posn = url.lastIndexOf(".");
    kind[0] = posn >= 0 ? url.substring(posn + 1) : "gif";
    return bi;
  }

}

Listing 11-4Source File 4 Utility.java

Formatter.java

{Json|Xml}Formatter.java 为响应格式化提供了一些帮助函数。JsonFormatter 直接完成所有格式化。未实现 XmlFormatter。

Formatter.java(清单 11-5 )包含了这段代码。

package org.baf.gol;

/**
 * Define a formatter (object to text).
 */
@FunctionalInterface
public interface Formatter {

  String valueToText(Object v);

}

Listing 11-5Formatter.java

JsonFormatter.java(清单 11-6 )包含了这段代码。

package org.baf.gol;

import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * A simple (but restricted) JSON object formatter.
 */
public class JsonFormatter implements Formatter {
  boolean pretty;
  String eol;

  public JsonFormatter(boolean pretty) {
    this.pretty = pretty;
    this.eol = pretty ? "\n" : "";
  }

  public JsonFormatter() {
    this(true);
  }

  @Override
  public String toString() {
    return "JsonFormatter[pretty=" + pretty + "]";
  }

  @Override
  public String valueToText(Object v) {
    StringBuilder sb = new StringBuilder();
    var size = 0;
    if (v instanceof List) {
      size = ((List) v).size();
    } else if (v instanceof Map) {
      size = ((Map) v).size();
    }
    valueToText(v, 0, "  ", "", size, ",  ", sb);
    return sb.toString();
  }

  // Format worker.
  void valueToText(Object v, int depth, String indent, String label, int len, String join, StringBuilder out) {
    if (join == null) {
      join = ", ";
    }

    var xindent = indent.repeat(depth);
    out.append(xindent);
    if (!label.isEmpty()) {
      out.append(label);
      out.append(": ");
    }
    if (v == null) {
      out.append("null");
      return;
    }
    // treat all implementations the same
    var c = v.getClass();
    var cname = c.getName();
    if (v instanceof List) {
      cname = List.class.getName();
    } else if (v instanceof Map) {
      cname = Map.class.getName();
    }
    // process all supported embedded types
    switch (cname) {
      case "java.util.Date":
        out.append(((Date) v).getTime());
        break;
      case "java.lang.String":
        v = '"' + v.toString().replace("\"", "\\\"") + '"';
      case "java.lang.Byte":
      case "java.lang.Short":
      case "java.lang.Integer":
      case "java.lang.Long":
      case "java.lang.Double":
      case "java.lang.Float":
      case "java.lang.Boolean":
        out.append(v.toString());
        break;
      case "java.util.List":
        out.append("[\n");
        List list = (List) v;
        for (int i = 0, xc = list.size(); i < xc; i++) {
          valueToText(list.get(i), depth + 1, indent, "", xc, join, out);
          out.append(i < len - 1 ? join : "");
          out.append(eol);
        }

        out.append(xindent + "]");
        break;
      case "java.util.Map":
        out.append("{\n");
        Map map = (Map) v;
        int i = 0, xc = map.size();
        for (var k : map.keySet()) {
          valueToText(map.get(k), depth + 1, indent, "\"" + k + "\"", xc, join, out);
          out.append(i < len - 1 ? join : "");
          i++;
          out.append(eol);
        }
        out.append(xindent + "}");
        break;
      default:
        throw new IllegalArgumentException("unknown type: " + cname);
    }
  }
}

Listing 11-6JsonFormatter.java

XmlFormatter.java(清单 11-7 )包含了这段代码。

package org.baf.gol;

public class XmlFormatter implements Formatter {

  @Override
  public String valueToText(Object v) {
    throw new IllegalThreadStateException("not implemented");
  }
}

Listing 11-7XmlFormatter.java

ParameterProvider.java

ParameterProvider.java 提供对命令参数的访问。

ParameterProvider.java(清单 11-8 )包含此代码。

package org.baf.gol;

/**
 * Provides a selected set of parameter values.
 */
public interface ParameterProvider {
  String getUrlParameter();

  String getNameParameter();

  int getMagFactorParameter();

  int getGameCyclesParameter();

  boolean startServerFlag();

  boolean runTimingsFlag();

  boolean reportFlag();

  boolean saveImageFlag();
}

Listing 11-8ParameterProvider.java

annimatedgifwriter . java

AnnimatedGifWriter.java 支持组合图像。

AnnimatedGifWriter.java(清单 11-9 )包含了这段代码。

package org.baf.gol;

import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;

/**
 * Supports combining multiple images into a single animated GIF.
 *
 */
public class AnnimatedGifWriter implements java.io.Closeable {
  private static final String CODE = "2.0";
  private static final String ID = "NETSCAPE";
  private static final String ZERO_INDEX = "0";
  private static final String NONE = "none";
  private static final String FALSE = "FALSE";

  protected IIOMetadata metadata;
  protected ImageWriter writer;
  protected ImageWriteParam params;

  public AnnimatedGifWriter(ImageOutputStream ios, int imageType, boolean showAsLoop, int delayMs, String author)
      throws IOException {
    var imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType);
    writer = ImageIO.getImageWritersBySuffix("gif").next();
    params = writer.getDefaultWriteParam();
    metadata = writer.getDefaultImageMetadata(imageTypeSpecifier, params);
    configMetadata(delayMs, showAsLoop, "Author: " + author);

    writer.setOutput(ios);
    writer.prepareWriteSequence(null);
  }

  @Override
  public void close() throws IOException {
    writer.endWriteSequence();
  }

  /**
   * Creates an animated GIF from 1+ images.
   */
  public static void createGifs(ImageOutputStream ios, int delay, String author, BufferedImage... images)
      throws IOException {
    if (delay < 0) {
      delay = 5 * 1000;
    }
    if (images.length < 1) {
      throw new IllegalArgumentException("at least one image is required");
    }
    try (var writer = new AnnimatedGifWriter(ios, images[0].getType(), true, delay, author)) {
      for (var image : images) {
        writer.addImage(image);
      }
    }
  }

  // configure self
  void configMetadata(int delay, boolean loop, String comment) throws IOException {
    var name = metadata.getNativeMetadataFormatName();
    var root = (IIOMetadataNode) metadata.getAsTree(name);
    metadata.setFromTree(name, root);

    var cel = findOrAddMetadata(root, "CommentExtensions");
    cel.setAttribute("CommentExtension", comment);

    var gce = findOrAddMetadata(root, "GraphicControlExtension");
    gce.setAttribute("transparentColorIndex", ZERO_INDEX);
    gce.setAttribute("userInputFlag", FALSE);
    gce.setAttribute("transparentColorFlag", FALSE);
    gce.setAttribute("delayTime", Integer.toString(delay / 10));
    gce.setAttribute("disposalMethod", NONE);

    byte[] bytes = new byte[] { 1, (byte) (loop ? 0 : 1), 0 };
    var ael = findOrAddMetadata(root, "ApplicationExtensions");
    var ae = new IIOMetadataNode("ApplicationExtension");
    ae.setUserObject(bytes);
    ae.setAttribute("authenticationCode", CODE);
    ae.setAttribute("applicationID", ID);
    ael.appendChild(ae);
  }

  static IIOMetadataNode findOrAddMetadata(IIOMetadataNode root, String metadataType) {
    for (int i = 0, c = root.getLength(); i < c; i++) {
      if (root.item(i).getNodeName().equalsIgnoreCase(metadataType)) {
        return (IIOMetadataNode) root.item(i);
      }
    }
    var node = new IIOMetadataNode(metadataType);
    root.appendChild(node);
    return (node);
  }

  void addImage(RenderedImage img) throws IOException {
    writer.writeToSequence(new IIOImage(img, null, metadata), params);
  }
}

Listing 11-9AnimatedGifWriter.java

Logger.java

Logger.java 支持模仿(不完全是)标准的 Go logger。

Logger.java(清单 11-10 )包含了这段代码。

package org.baf.gol;

import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Approximates the default Go logger function.
 *
 */
public class Logger {
  static public Logger log = new Logger();

  public PrintStream ps = System.out;
  public String lineFormat = "%-25s %-20s %-8s %-30s %s%n";
  public String contextFormat = "%s#%s@%d";
  public String threadFormat = "%s:%s";
  public SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

  public void fatalf(String format, Object... args) {
    output(2, "FATAL", format, args);
    System.exit(3);
  }

  public void exceptionf(Exception e, String format, Object... args) {
    output(2, "EXCPT", "%s; caused by %s", String.format(format, args), e.getMessage());
    e.printStackTrace(ps);
  }

  public void errorf(String format, Object... args) {
    output(2, "ERROR", format, args);
  }

  public void tracef(String format, Object... args) {
    output(2, "TRACE", format, args);
  }

  void output(int level, String severity, String format, Object... args) {
    var text = String.format(format, args);
    Thread ct = Thread.currentThread();
    var st = ct.getStackTrace();
    StackTraceElement ste = st[level + 1];
    var tn = String.format(threadFormat, ct.getThreadGroup().getName(), ct.getName());
    var ctx = String.format(contextFormat, reduce(ste.getClassName()), ste.getMethodName(), ste.getLineNumber());
    ps.printf(lineFormat, df.format(new Date()), tn, severity, ctx, text);
  }

  String reduce(String name) {
    var posn = name.lastIndexOf(".");
    return posn >= 0 ? name.substring(posn + 1) : name;
  }
}

Listing 11-10Logger.java

虽然本文不是关于构建和运行 Java 程序的教程,但是 Java GoL 程序可以通过如下命令来构建和运行。命令形式会因您的设置和使用的工具而异(在本文中,作者使用的是 Eclipse IDE,而不是命令行工具)。假设源根目录是当前目录:

javac -d . -sourcepath ./org/baf/gol *.java
java -cp . org.baf.gol.Main -start -url ''

如果 GoL 程序构建在一个可运行的 JAR 中,它可以像这样启动:

$>java --enable-preview -jar gol.jar

注意 GoL 程序使用了新的 Java 特性。

该命令产生以下输出:

Play the game of Life.
Game boards are initialized from PNG images.
Games play over several cycles.
Optionally acts as a server to retrieve images of game boards during play.
No supported positional arguments.
Arguments (all start with '-'):
url|u <url>              URL of the PNG image to load
name|n <name>            name to refer to the game initialized by the URL
magFactor|mf|mag <int>   magnify the grid by this factor when formatted into an image  (default 1; 1 to 20)
gameCycles|gc <int>      sets number of cycles to run (default 10)
start <boolean>          start the HTTP server (default false)
time <boolean>           run game cycle timings with different thread counts (default false)
report <boolean>         output run statistics (default false)
saveImage|si <boolean>   save generated images into a file (default false)

这段代码生成如下所示的跟踪行(在 Go 和 Java 版本之间有所不同,在文本列处分开):

2021-01-16 09:49:17.686   main:main            TRACE    Server#startHttpServer@337
Server localhost:8080 started

2021-01-16 09:49:22.166   main:HTTP-Dispatcher TRACE    Utility#loadImage@28
loadImage file:/.../tiny1.png; /.../tiny1.png

2021-01-16 09:49:22.204   main:HTTP-Dispatcher TRACE    Game$GameRun#<init>@69
Image kind: png

2021-01-16 09:49:22.257   main:HTTP-Dispatcher TRACE    Game$GameRun#run@169
GameRun total time: 45ms, cycles: 10, thread count: 0

2021-01-16 09:49:22.259   main:HTTP-Dispatcher EXCPT    Server$2#handle@257
play failed; caused by not implemented

Go 中的顶点项目

GoL 程序的 Go 形式由一个单独的main包组成,该包包含以下源文件:

  • main . go–主命令处理器

  • server . go–HTTP 服务器和请求处理程序

  • game . go–高尔夫游戏逻辑

  • utility . go–助手功能

注意所有前面的 Go 源代码都在main包中。在更具生产风格的实现中,每个源代码可能都在自己的包中。使用单独的包可能需要改变一些名字的大小写以使它们成为公共的。

注意这个 Go 例子是在 Go 1.14 上测试的。至少使用该版本的工具和运行时。

Main.go

main.go文件包含main函数,它检测任何命令行参数并处理它们。它使用flag包来处理任何命令行标志(名称以...Flag结尾)。它还可以选择(但默认情况下)启动 HTTP 服务器。请注意,命令行参数nameurl仅在请求运行计时时使用。

清单 11-11 (Main.go)包含了这段代码。

import (
      "flag"
      "fmt"
      "os"
      "runtime"
      "strings"
)

// Command line flags.
var (
      urlFlag         string
      nameFlag        string
      gridFlag        string
      magFactorFlag   int
      startServerFlag bool
      runTimingsFlag  bool
      reportFlag      bool
      saveImageFlag   bool
)

// Command line help strings
const (
      urlHelp       = "URL of the PNG image to load"
      nameHelp      = "name to refer to the game initialized by the URL"
      magFactorHelp = "magnify the grid by this factor when formatted into an image"
      gridHelp      = "specify the layout grid (for PNG images); MxN, default 1x1"
      startHelp     = "start the HTTP server (default true)"
      timingHelp    = "run game cycle timings with different goroutine counts"
      reportHelp    = "output run statistics"
      saveImageHelp = "save generated images into a file"
)

// Define command line flags.
// Some are aliases (short forms).
func init() {
      flag.StringVar(&urlFlag, "url", "", urlHelp)
      flag.StringVar(&urlFlag, "u", "", urlHelp)
      flag.StringVar(&nameFlag, "name", "", nameHelp)
      flag.StringVar(&nameFlag, "n", "", nameHelp)
      flag.StringVar(&gridFlag, "grid", "1x1", gridHelp)
      flag.IntVar(&magFactorFlag, "magFactor", 1, magFactorHelp)
      flag.IntVar(&magFactorFlag, "mf", 1, magFactorHelp)
      flag.IntVar(&magFactorFlag, "mag", 1, magFactorHelp)
      flag.BoolVar(&startServerFlag, "start", true, startHelp)
      flag.BoolVar(&runTimingsFlag, "time", false, timingHelp)
      flag.BoolVar(&reportFlag, "report", false, reportHelp)
      flag.BoolVar(&saveImageFlag, "saveImage", false, saveImageHelp)
      flag.BoolVar(&saveImageFlag, "si", false, saveImageHelp)
}

const golDescription = `
Play the game of Life.
Game boards are initialized from PNG images.
Games play over cycles.
Optionally acts as a server to retrieve images of game boards during play.
No supported positional arguments. Supported flags (some have short forms):
`

// Main entry point.
// Sample: -n bart -u file:/Users/Administrator/Downloads/bart.png
func main() {
      if len(os.Args) <= 1 {
            fmt.Fprintln(os.Stderr, strings.TrimSpace(golDescription))
            flag.PrintDefaults()
            os.Exit(0)
      }
      fmt.Printf("Command arguments: %v\n", os.Args[1:])
      fmt.Printf("Go version: %v\n", runtime.Version())
      flag.Parse() // parse any flags
      if len(flag.Args()) > 0 {
            fatalIfError(fmt.Fprintf(os.Stderr,
                  "positional command arguments (%v) not accepted\n", flag.Args()))
            os.Exit(1)
      }
      launch()
}

func launch() {
      if len(urlFlag) > 0 {
            if len(nameFlag) == 0 {
                  fatalIfError(fmt.Fprintln(os.Stderr,
                        "a name is required when a URL is provided"))
            }

            if runTimingsFlag {
                  runCycleTimings()
            }
      }

      if startServerFlag {
            startHTTPServer()
      }
}

// launch the HTTP server.
func startHTTPServer() {
      err := startServer()
      if err != nil {
            fmt.Printf("start Server failed: %v\n", err)
            os.Exit(3)
      }

}

// Output information about recorded cycles.
func runCycleTimings() {
      cpuCount := runtime.NumCPU()
      for i := 1; i <= 64; i *= 2 {
            fmt.Printf("Running with %d goroutines, %d CPUs...\n", i, cpuCount)
            CoreGame.GoroutineCount = i
            err := CoreGame.Run(nameFlag, urlFlag)
            if err != nil {
                  fmt.Printf("Program failed: %v\n", err)
                  os.Exit(2)
            }
            if reportFlag {
                  fmt.Printf("Game max: %d, go count: %d:\n",
                        CoreGame.MaxCycles, CoreGame.GoroutineCount)
                  for _, gr := range CoreGame.Runs {
                        fmt.Printf("Game Run: %v, cycle count: %d\n", gr.Name, len(gr.Cycles))
                        for _, c := range gr.Cycles {
                              start, end :=
                                    c.StartedAt.UnixNano()/NanosPerMs,
                                    c.EndedAt.UnixNano()/NanosPerMs
                              fmt.Printf(
                                    "Cycle: start epoch: %dms, end epoch: %dms, elapsed: %dms\n",
                                    start, end, end-start)
                        }
                  }
            }
      }
}

Listing 11-11Main.go

Server.go

server.go文件启动一个带有几个路径处理程序的 HTTP 服务器。处理程序访问任何查询参数,然后生成和/或访问 GoL 数据。AllGames代表过去玩过的游戏的历史的根。返回存储在其中的图像或 JSON/XML 统计信息。

服务器以 JSON 或 XML 格式返回统计数据。请注意,这两种格式都只需要很少的代码。还要注意每种格式的代码有多相似。大部分工作是在任何要转换为文本的结构上定义标记。一般来说,这里的代码比任何 Java 等效实现所需的代码都要少。

返回的数据结构有控制数据格式的标记。注意返回的数据名称有时与字段名称不同。

Server.go(清单 11-12 )包含了这段代码。

package main

import (
      "bytes"
      "encoding/json"
      "encoding/xml"
      "fmt"
      "image/gif"
      "io/ioutil"
      "log"
      "net/http"
      "os"
      "regexp"
      "strconv"
      "strings"
)

var spec = ":8080" // means localhost:8080

// launch HTTP server for th GoL.
func startServer() (err error) {
      http.HandleFunc("/play", playHandler)
      http.HandleFunc("/show", showHandler)
      http.HandleFunc("/history", historyHandler)
      fmt.Printf("Starting Server %v...\n", spec)
      err = http.ListenAndServe(spec, nil)
      return
}

// XYyyy types are returned to clients as JSON or XML.
// They are subset of Yyyy types used by the game player.
// They have no reference loops (i.e., to parents) not allowed in JSON and
// omit large fields.
// The tags define how the data is named and formatted

// Represents a game.
type XGame struct {
      Runs map[string]*XGameRun
}

type XGameCycle struct {
      Cycle           int   `json:"cycle" xml:"Cycle"`
      StartedAt       int64 `json:"startedAtNS" xml:"StartedAtEpochNS"`
      EndedAt         int64 `json:"endedAtNS" xml:"EndedAtEpochNS"`
      Duration        int64 `json:"durationMS" xml:"DurationMS"`
      GorountineCount int   `json:"goroutineCount" xml:"GorountineCount"`
      MaxCycles       int   `json:"maximumCycles" xml:"MaximumCycles"`
}

type XGameRun struct {
      Name        string        `json:"name" xml:"Name"`
      ImageURL    string        `json:"imageURL" xml:"ImageURL"`
      StartedAt   int64         `json:"startedAtNS" xml:"StartedAtEpochNS"`
      EndedAt     int64         `json:"endedAtNS" xml:"EndedAtEpochNS"`
      Duration    int64         `json:"durationMS" xml:"DurationMS"`
      Width       int           `json:"width" xml:"Width"`
      Height      int           `json:"height" xml:"Height"`
      Cycles      []*XGameCycle `json:"gameCycles" xml:"GameCycles>GameCycle,omitempty"`
      DelayIn10ms int           `json:"delay10MS" xml:"Delay10MS"`
      PlayIndex   int           `json:"playIndex" xml:"PlayIndex"`
}

func getLead(s string) (res string) {
      res = s
      posn := strings.Index(s, "?")
      if posn >= 0 {
            res = s[0:posn]
      }
      return
}

// History request handler
func historyHandler(writer http.ResponseWriter, request *http.Request) {
      switch request.Method {
      case "GET":
            if getLead(request.RequestURI) != "/history" {
                  writer.WriteHeader(405)
                  return
            }
            game := &XGame{}
            game.Runs = make(map[string]*XGameRun)
            for k, g := range CoreGame.Runs {
                  game.Runs[k] = makeReturnedRun(k, g.ImageURL)
            }
            ba, err := json.MarshalIndent(game, "", "  ")
            if err != nil {
                  writer.WriteHeader(500)
                  return
            }
            writer.Header().Add("Content-Type", "text/json")
            writer.WriteHeader(200)
            writer.Write(ba) // send response; error ignored
      case "DELETE":
            if request.RequestURI != "/history" {
                  writer.WriteHeader(405)
                  return
            }

            for k, _ := range CoreGame.Runs {
                  delete(CoreGame.Runs, k)
            }
            writer.WriteHeader(204)
      default:
            writer.WriteHeader(405)
      }
}

// Play request handler.
func playHandler(writer http.ResponseWriter, request *http.Request) {
      if request.Method != "GET" || getLead(request.RequestURI) != "/play" {
            writer.WriteHeader(405)
            return
      }
      err := request.ParseForm() // get query parameters
      if err != nil {
            writer.WriteHeader(400)
            return
      }
      name := request.Form.Get("name")
      url := request.Form.Get("url")
      if len(url) == 0 || len(name) == 0 {
            writer.WriteHeader(400)
            return
      }
      ct := request.Form.Get("ct")
      if len(ct) == 0 {
            ct = request.Header.Get("content-type")
      }
      ct = strings.ToLower(ct)
      switch ct {
      case "":
            ct = "application/json"
      case "application/json", "text/json":
      case "application/xml", "text/xml":
      default:
            writer.WriteHeader(400)
            return
      }

      err = CoreGame.Run(name, url)
      if err != nil {
            writer.WriteHeader(500)
            return
      }
      run := makeReturnedRun(name, url)

      var ba []byte
      switch ct {
      case "application/json", "text/json":
            ba, err = json.MarshalIndent(run, "", "  ")
            if err != nil {
                  writer.WriteHeader(500)
                  return
            }
            writer.Header().Add("Content-Type", "text/json")
      case "application/xml", "text/xml":
            ba, err = xml.MarshalIndent(run, "", "  ")
            if err != nil {
                  writer.WriteHeader(500)
                  return
            }
            writer.Header().Add("Content-Type", "text/xml")
      }

      writer.WriteHeader(200)
      writer.Write(ba) // send response; error ignored
}

// Build data for returned run.
func makeReturnedRun(name, url string) *XGameRun {
      run := CoreGame.Runs[name]
      xrun := &XGameRun{}
      xrun.Name = run.Name
      xrun.ImageURL = url
      xrun.PlayIndex = run.PlayIndex
      xrun.DelayIn10ms = run.DelayIn10ms
      xrun.Height = run.Height
      xrun.Width = run.Width
      xrun.StartedAt = run.StartedAt.UnixNano()
      xrun.EndedAt = run.EndedAt.UnixNano()
      xrun.Duration = (xrun.EndedAt - xrun.StartedAt + NanosPerMs/2) / NanosPerMs
      xrun.Cycles = make([]*XGameCycle, 0, 100)

      for _, r := range run.Cycles {
            xc := &XGameCycle{}
            xc.StartedAt = r.StartedAt.UnixNano()
            xc.EndedAt = r.EndedAt.UnixNano()
            xc.Duration = (xc.EndedAt - xc.StartedAt + NanosPerMs/2) / NanosPerMs
            xc.Cycle = r.Cycle
            xc.GorountineCount = CoreGame.GoroutineCount
            xc.MaxCycles = CoreGame.MaxCycles
            xrun.Cycles = append(xrun.Cycles, xc)
      }

      return xrun
}

var re = regexp.MustCompile(`^(\d+)x(\d+)$`)

// Show request handler.
func showHandler(writer http.ResponseWriter, request *http.Request) {
      if request.Method != "GET" || getLead(request.RequestURI) != "/show" {
            writer.WriteHeader(405)
            return
      }
      err := request.ParseForm() // get query parameters
      if err != nil {
            writer.WriteHeader(400)
            return
      }
      name := request.Form.Get("name")
      if len(name) == 0 {
            name = "default"
      }
      form := request.Form.Get("form")
      if len(form) == 0 {
            form = "gif"
      }
      xmaxCount := request.Form.Get("maxCount")
      if len(xmaxCount) == 0 {
            xmaxCount = "20"
      }
      maxCount, err := strconv.Atoi(xmaxCount)
      if err != nil || maxCount < 1 || maxCount > 100 {
            writer.WriteHeader(400)
            return
      }
      xmag := request.Form.Get("mag")
      if len(xmag) > 0 {
            mag, err := strconv.Atoi(xmag)
            if err != nil || mag < 1 || mag > 20 {
                  writer.WriteHeader(400)
                  return
            }

            magFactorFlag = mag
      }

      index := 0
      // verify parameters based on type
      switch form {
      case "gif", "GIF":
      case "png", "PNG":
            xindex := request.Form.Get("index")
            if len(xindex) == 0 {
                  xindex = "0"
            }
            index, err = strconv.Atoi(xindex)
            if err != nil {
                  writer.WriteHeader(400)
                  return
            }
            xgrid := request.Form.Get("grid")
            if len(xgrid) > 0 {
                  parts := re.FindStringSubmatch(xgrid)
                  if len(parts) != 2 {
                        writer.WriteHeader(400)
                        return
                  }
                  gridFlag = fmt.Sprintf("%sx%s", parts[0], parts[1])
            }
      default:
            writer.WriteHeader(400)
            return
      }

      gr, ok := CoreGame.Runs[name]
      if ! ok {
            writer.WriteHeader(404)
            return
      }

      // return requested image type
      switch form {
      case "gif", "GIF":
            gifs, err := gr.MakeGIFs(maxCount)
            if err != nil {
                  writer.WriteHeader(500)
                  return
            }
            var buf bytes.Buffer
            err = gif.EncodeAll(&buf, gifs)
            if err != nil {
                  writer.WriteHeader(500)
                  return
            }
            count, err := writer.Write(buf.Bytes()) // send response
            log.Printf("Returned GIF, size=%d\n", count)
            if saveImageFlag {
                  saveFile := fmt.Sprintf("/temp/Image_%s.gif", name)
                  xerr := ioutil.WriteFile(saveFile, buf.Bytes(), os.ModePerm)
                  fmt.Printf("Save %s: %v\n", saveFile, xerr)
            }
      case "png", "PNG":
            if gridFlag == "1x1" {
                  if index <= maxCount {
                        var buf bytes.Buffer
                        err = gr.MakePNG(&buf, index)
                        if err != nil {
                              code := 500
                              if err == BadIndexError {
                                    code = 400
                              }
                              writer.WriteHeader(code)
                              return
                        }
                        writer.Write(buf.Bytes()) // send response; error ignored
                  } else {
                        writer.WriteHeader(400)
                  }
            } else {
                  // currently not implemented
                  writer.WriteHeader(400)
            }
      }
}

Listing 11-12Server.go

Game.go

Game.go 包含打高尔夫球的逻辑。每个游戏由一组命名为 GameRun 的实例组成。每个 GameRun 由一组 GameCycle 实例和一些统计数据组成。每个游戏周期由一个前后网格快照和一些统计数据组成。每个网格都有单元格数据(作为[]byte)和网格尺寸。REST show API 返回制作成图像的 after-grid 实例。

NextCycle方法内的 goroutine 中调用processRows函数。这允许使用可变数量的 goroutines。使用更多的 goroutines 可以显著加快 GoL 循环的处理速度,特别是对于较大的网格,如本节后面所示。

Game.go(清单 11-13 )包含了这段代码。

package main

import (
      "bytes"
      "errors"
      "fmt"
      "image"
      "image/color"
      "image/gif"
      "image/png"
      "io"
      "io/ioutil"
      "log"
      "os"
      "sync"
      "time"
)

// Default game history.
var CoreGame = &Game{
      make(map[string]*GameRun),
      10,
      0,
      1}

// Represents a game.
type Game struct {
      Runs           map[string]*GameRun
      MaxCycles      int
      SkipCycles     int // not currently used
      GoroutineCount int
}

// Run a set of cycles from the grid defined by an image.
func (g *Game) Run(name, url string) (err error) {
      gr, err := NewGameRun(name, url, g)
      if err != nil {
            return
      }
      g.Runs[gr.Name] = gr
      err = gr.Run()
      return
}

// Clear a game.
func (g *Game) Clear() {
      for k, _ := range g.Runs {
            delete(g.Runs, k)
      }
}

// Represents a single run of a game.
type GameRun struct {
      Parent         *Game
      Name           string
      ImageURL       string
      StartedAt      time.Time
      EndedAt        time.Time
      Width, Height  int
      InitialGrid    *Grid
      CurrentGrid    *Grid
      FinalGrid      *Grid
      Cycles         []*GameCycle
      DelayIn10ms    int
      PlayIndex      int
      GoroutineCount int
}

// B & W color indexes
const (
      offIndex = 0
      onIndex  = 1
)

// B & W color palette
var paletteBW = []color.Color{color.White, color.Black}

// Generate a PNG result (single frame).
func (gr *GameRun) MakePNG(writer io.Writer, index int) (err error) {
      var grid *Grid
      switch index {
      case 0:
            grid = gr.InitialGrid
      default:
            index--
            if index < 0 || index >= len(gr.Cycles) {
                  err = BadIndexError
                  return
            }
            grid = gr.Cycles[index].AfterGrid
      }
      mag := magFactorFlag
      rect := image.Rect(0, 0, mag*gr.Width+1, mag*gr.Height+1)
      img := image.NewPaletted(rect, paletteBW)
      gr.FillImage(grid, img)
      b, err := gr.encodePNGImage(img)
      if err != nil {
            return
      }

      count, err := writer.Write(b.Bytes())
      log.Printf("Returned PNG, size= %d\n", count)
      if saveImageFlag {
            saveFile := fmt.Sprintf("/temp/Image_%s_%d.png", gr.Name, index)
            xerr := ioutil.WriteFile(saveFile, b.Bytes(), os.ModePerm)
            fmt.Printf("Save %s: %v\n", saveFile, xerr)
      }
      return
}

// Make a PNG image.
func (gr *GameRun) encodePNGImage(img *image.Paletted) (b bytes.Buffer, err error) {
      var e png.Encoder
      e.CompressionLevel = png.NoCompression
      err = e.Encode(&b, img)
      return
}

// Generate a GIF result (>= 1 frame).
func (gr *GameRun) MakeGIFs(count int) (agif *gif.GIF, err error) {
      mag := magFactorFlag
      cycles := len(gr.Cycles)
      xcount := cycles + 1
      if xcount > count {
            xcount = count
      }
      added := 0
      agif = &gif.GIF{LoopCount: 5}

      rect := image.Rect(0, 0, mag*gr.Width+1, mag*gr.Height+1)
      img := image.NewPaletted(rect, paletteBW)
      if added < xcount {
            gr.AddGrid(gr.InitialGrid, img, agif)
            added++
      }
      for i := 0; i < cycles; i++ {
            if added < xcount {
                  img = image.NewPaletted(rect, paletteBW)
                  gc := gr.Cycles[i]
                  grid := gc.AfterGrid
                  gr.AddGrid(grid, img, agif)
                  added++
            }
      }
      return
}

// Fill in and record a cycle image in an animated GIF.
func (gr *GameRun) AddGrid(grid *Grid, img *image.Paletted, agif *gif.GIF) {
      gr.FillImage(grid, img)
      agif.Image = append(agif.Image, img)
      agif.Delay = append(agif.Delay, gr.DelayIn10ms)
}

// Fill in an image from a grid.
func (gr *GameRun) FillImage(grid *Grid, img *image.Paletted) {
      mag := magFactorFlag
      for row := 0; row < grid.Height; row++ {
            for col := 0; col < grid.Width; col++ {
                  index := offIndex
                  if grid.getCell(col, row) != 0 {
                        index = onIndex
                  }
                  // apply magnification
                  for i := 0; i < mag; i++ {
                        for j := 0; j < mag; j++ {
                              img.SetColorIndex(mag*row+i, mag*col+j, uint8(index))
                        }
                  }
            }
      }
}

const midValue = 256 / 2 // middle color value

// Error values.
var (
      NotPNGError   = errors.New("not a png")
      NotRGBAError  = errors.New("not RGBA color")
      BadIndexError = errors.New("bad index")
)

// Start a new game run.
func NewGameRun(name, url string, parent *Game) (gr *GameRun, err error) {
      gr = &GameRun{}
      gr.Parent = parent
      gr.Name = name
      gr.GoroutineCount = CoreGame.GoroutineCount
      gr.ImageURL = url
      gr.DelayIn10ms = 5 * 100
      var img image.Image
      var kind string
      img, kind, err = LoadImage(url)
      if err != nil {
            return
      }
      fmt.Printf("Image kind:  %v\n", kind)
      if kind != "png" {
            return nil, NotPNGError
      }
      bounds := img.Bounds()
      minX, minY, maxX, maxY := bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y
      size := bounds.Size()
      //xsize := size.X * size.Y
      gr.InitialGrid = NewEmptyGrid(size.X, size.Y)
      gr.Width = gr.InitialGrid.Width
      gr.Height = gr.InitialGrid.Height

      err = gr.InitGridFromImage(minX, maxX, minY, maxY, img)
      if err != nil {
            return
      }
      gr.CurrentGrid = gr.InitialGrid.DeepCloneGrid()
      return
}

// Fill in a grid from an image.
// Map color images to B&W.  Only RGBA images allowed.
func (gr *GameRun) InitGridFromImage(minX, maxX, minY, maxY int,
      img image.Image) (err error) {
      setCount, totalCount := 0, 0
      for y := minY; y < maxY; y++ {
            for x := minX; x < maxX; x++ {
                  //                  r, g, b, a := img.At(x, y).RGBA()
                  rgba := img.At(x, y)
                  var r, g, b uint8
                  switch v := rgba.(type) {
                  case color.NRGBA:
                        r, g, b, _ = v.R, v.G, v.B, v.A
                  case color.RGBA:
                        r, g, b, _ = v.R, v.G, v.B, v.A
                  default:
                        err = NotRGBAError
                        return
                  }

                  cv := byte(0) // assume cell dead
                  if int(r)+int(g)+int(b) < midValue*3 {
                        cv = byte(1) // make cell alive
                        setCount++
                  }
                  gr.InitialGrid.setCell(x, y, cv)
                  totalCount++
            }
      }
      return
}

// Play a game.
// Run requested cycle count.
func (gr *GameRun) Run() (err error) {
      gr.StartedAt = time.Now()
      for count := 0; count < gr.Parent.MaxCycles; count++ {
            err = gr.NextCycle()
            if err != nil {
                  return
            }
      }
      gr.EndedAt = time.Now()
      fmt.Printf("GameRun total time: %dms, goroutine count: %d\n",
            (gr.EndedAt.Sub(gr.StartedAt)+NanosPerMs)/NanosPerMs, gr.GoroutineCount)
      gr.FinalGrid = gr.CurrentGrid.DeepCloneGrid()
      return
}

// Represents a single cycle of a game.
type GameCycle struct {
      Parent     *GameRun
      Cycle      int
      StartedAt  time.Time
      EndedAt    time.Time
      BeforeGrid *Grid
      AfterGrid  *Grid
}

func NewGameCycle(parent *GameRun) (gc *GameCycle) {
      gc = &GameCycle{}
      gc.Parent = parent
      return
}

// Advance and play next game cycle.
// Updating of cycle grid rows can be done in parallel;
// which can reduce execution time.
func (gr *GameRun) NextCycle() (err error) {
      gc := NewGameCycle(gr)
      gc.BeforeGrid = gr.CurrentGrid.DeepCloneGrid()
      p := gc.Parent
      goroutineCount := p.Parent.GoroutineCount
      if goroutineCount <= 0 {
            goroutineCount = 1
      }
      gc.AfterGrid = NewEmptyGrid(gc.BeforeGrid.Width, gc.BeforeGrid.Height)
      gc.StartedAt = time.Now()
      // process rows across  allowed goroutines
      rowCount := (gr.Height + goroutineCount/2) / goroutineCount
      var wg sync.WaitGroup
      for i := 0; i < goroutineCount; i++ {
            wg.Add(1)
            go processRows(&wg, gc, rowCount, i*rowCount, gc.BeforeGrid, gc.AfterGrid)
      }
      wg.Wait() // let all finish
      gc.EndedAt = time.Now()
      gr.CurrentGrid = gc.AfterGrid.DeepCloneGrid()
      gr.Cycles = append(gr.Cycles, gc)
      gc.Cycle = len(gr.Cycles)
      return
}

// Represents a 2-dimensional game grid (abstract, not as an image).
type Grid struct {
      Data          []byte
      Width, Height int
}

func NewEmptyGrid(w, h int) (g *Grid) {
      g = &Grid{}
      g.Data = make([]byte, w*h)
      g.Width = w
      g.Height = h
      return
}

func (g *Grid) DeepCloneGrid() (c *Grid) {
      c = &Grid{}
      lg := len(g.Data)
      c.Data = make([]byte, lg, lg)
      for i, b := range g.Data {
            c.Data[i] = b
      }
      c.Width = g.Width
      c.Height = g.Height
      return
}

func (g *Grid) getCell(x, y int) (b byte) {
      if x < 0 || x >= g.Width || y < 0 || y >= g.Height {
            return
      }
      return g.Data[x+y*g.Width]
}
func (g *Grid) setCell(x, y int, b byte) {
      if x < 0 || x >= g.Width || y < 0 || y >= g.Height {
            return
      }
      g.Data[x+y*g.Width] = b
}

// Play game as subset of grid rows (so can be done in parallel).
func processRows(wg *sync.WaitGroup, gc *GameCycle, rowCount int,
      startRow int, inGrid, outGrid *Grid) {
      defer wg.Done()
      gr := gc.Parent
      for index := 0; index < rowCount; index++ {
            rowIndex := index + startRow
            for colIndex := 0; colIndex < gr.Width; colIndex++ {
                  // count any neighbors
                  neighbors := 0
                  if inGrid.getCell(colIndex-1, rowIndex-1) != 0 {
                        neighbors++
                  }
                  if inGrid.getCell(colIndex, rowIndex-1) != 0 {
                        neighbors++
                  }

                  if inGrid.getCell(colIndex+1, rowIndex-1) != 0 {
                        neighbors++
                  }
                  if inGrid.getCell(colIndex-1, rowIndex) != 0 {
                        neighbors++
                  }
                  if inGrid.getCell(colIndex+1, rowIndex) != 0 {
                        neighbors++
                  }
                  if inGrid.getCell(colIndex-1, rowIndex+1) != 0 {
                        neighbors++
                  }
                  if inGrid.getCell(colIndex, rowIndex+1) != 0 {
                        neighbors++
                  }
                  if inGrid.getCell(colIndex+1, rowIndex+1) != 0 {
                        neighbors++
                  }

                  // determine next generation cell state based on neighbor count
                  pv := inGrid.getCell(colIndex, rowIndex)
                  nv := uint8(0) // assume dead
                  switch neighbors {
                  case 2:
                        nv = pv // unchanged
                  case 3:
                        if pv == 0 {
                              nv = 1 // make alive
                        }
                  }
                  outGrid.setCell(colIndex, rowIndex, nv)
            }
      }
}

Listing 11-13Game.go

Utility.go

Utility.go 提供了一些辅助函数和共享值。

Utility.go(清单 11-14 )包含了这段代码。

package main

import (
      "bytes"
      "image"
      "io/ioutil"
      "log"
      "net/http"
      "strings"
)

const NanosPerMs = 1_000_000
const FilePrefix = "file:" // local (vs. HTTP) file

func LoadImage(url string) (img image.Image, kind string, err error) {
      switch {
      case strings.HasPrefix(url, FilePrefix):
            url = url[len(FilePrefix):]
            var b []byte
            b, err = ioutil.ReadFile(url) // read image from file
            if err != nil {
                  return
            }
            r := bytes.NewReader(b)
            img, kind, err = image.Decode(r)
            if err != nil {
                  return
            }
      default:
            var resp *http.Response
            resp, err = http.Get(url) // get image from network
            if err != nil {
                  return
            }

            img, kind, err = image.Decode(resp.Body)
            resp.Body.Close() // error ignored
            if err != nil {
                  return
            }
      }
      return
}

// Fail if passed an error.
func fatalIfError(v ...interface{}) {
      if v != nil && len(v) > 0 {
            if err, ok := v[len(v)-1].(error); ok && err != nil {
                  log.Fatalf("unexpected error: %v\n", err)
            }
      }
}

Listing 11-14Utility.go

转到文档输出

虽然命令行程序通常不会这样做,但下面是运行该命令的输出

go doc -cmd -u

关于 GoL 代码库:

package main // import "."
const offIndex = 0 ...
const urlHelp = "URL of the PNG image to load" ...
const FilePrefix = "file:"
const NanosPerMs = 1_000_000
const golDescription = ...
const midValue = 256 / 2
var NotPNGError = errors.New("not a png") ...
var urlFlag string ...
var AllGames = &Game{ ... }
var paletteBW = []color.Color{ ... }
var spec = ":8080"
func LoadImage(url string) (img image.Image, kind string, err error)
func fatalIfError(v ...interface{})
func init()
func main()
func playHandler(writer http.ResponseWriter, request *http.Request)
func processRows(wg *sync.WaitGroup, gc *GameCycle, rowCount int, startRow int, ...)
func runCycleTimings()
func showHandler(writer http.ResponseWriter, request *http.Request)
func startHTTPServer()
func startServer() (err error)
type Game struct{ ... }
type GameCycle struct{ ... }
    func NewGameCycle(parent *GameRun) (gc *GameCycle)
type GameRun struct{ ... }
    func NewGameRun(name, url string, parent *Game) (gr *GameRun, err error)
type Grid struct{ ... }
    func NewEmptyGrid(w, h int) (g *Grid)
type XGameCycle struct{ ... }
type XGameRun struct{ ... }
    func makeReturnedRun(name, url string) *XGameRun

这个输出被工具本身省略了(因此是不完整的),但是它很好地概括了程序的关键部分。可以深入每个报告的struct以获得更多细节。有关更多详细信息,请参见 Go 文档。

与 Java 中的 JavaDoc 工具(它预先生成 HTML 输出)不同,这种文档的更丰富的在线 HTML 形式需要运行一个 go doc 服务器来生成。请参阅联机文档,了解如何启动这样的服务器。

API 输出

一个play动作的结果如图 11-6 所示,显示返回的样本统计。这个 JSON 是由encoding/json包中的 Go 函数创建的。

img/516433_1_En_11_Fig6_HTML.jpg

图 11-6

在 JSON 中播放输出

前面 JSON 的 XML 等价物是由encoding/xml包中的 Go 函数创建的(图 11-7 )。

img/516433_1_En_11_Fig7_HTML.jpg

图 11-7

以 XML 格式播放输出

图 11-8 中所示的history动作的结果显示了已知玩过的游戏。

img/516433_1_En_11_Fig8_HTML.jpg

图 11-8

JSON 中的历史输出

在服务器启动时或者在DELETE /history之后,看起来如图 11-9 所示。

img/516433_1_En_11_Fig9_HTML.jpg

图 11-9

空历史输出

游戏输出

在下面的图中,一个show动作的结果可以在几个游戏循环的样本集中看到,从原始图像开始。如果允许有许多周期(这里没有显示),这个原始的周期最终进入一个无限延续的交替模式。

图 11-10 提供了一个原始的样本图像,一个涂鸦和一些简单形状的混合,用于在浏览器中以小比例打高尔夫球。

img/516433_1_En_11_Fig10_HTML.jpg

图 11-10

初始游戏状态的浏览器视图

图 11-11 显示了一个类似 PNG 的十循环游戏,由 Swing GUI 中的 Java 实现呈现(在 Go 版本中没有对等的 GUI)。第一幅图像是输入图像。其他人在新的游戏周期后。宽输出被分成以下两个部分。请注意,不规则的图案往往会很快消失,而规则的图案会保持更长时间。

img/516433_1_En_11_Fig11_HTML.jpg

图 11-11

游戏周期历史(前九个周期)

注意图像在宽度和高度上有 1 个像素的空白(黑色)。

由于网格尺寸较小,前一场高尔夫球赛处理一个循环所需的时间很少。类似的 Go 运行,如表 11-1 所示。2000 x 2000)宽度和高度示例图像显示了这些循环计时结果。

Table 11-1. Timings for Different Goroutine Counts

GameRun total time: 2995ms, goroutine count:  1
GameRun total time: 1621ms, goroutine count:  2
GameRun total time:  922ms, goroutine count:  4
GameRun total time:  581ms, goroutine count:  8
GameRun total time:  487ms, goroutine count: 16
GameRun total time:  363ms, goroutine count: 32

请注意,这些数字对映像的大小和运行服务器的处理器容量很敏感。您的里程可能会有所不同。

图 11-12 中的图表显示了以运行时间(以毫秒为单位)与 goroutine 计数绘制的性能数字。人们可以清楚地看到更多 goroutines 在改善执行时间方面的优势。改进的程度取决于可用的内核数量,但即使几个内核也有很大帮助。最终,如图所示,添加更多的 goroutines 并不会提高太多的性能。

这种改善与 goroutines 的数量不是线性的 18 ,但却是实质性的。在这里,最短的观察时间只有最长观察时间的 16%。给定最小的额外费用(大约。10 行)代码需要添加 goroutines,这是一个很好的投资回报。

img/516433_1_En_11_Fig12_HTML.png

图 11-12

不同 goroutine 计数的计时

十二、关键包的比较

本章总结了几个关键的(常用的和广泛使用的)Java 包。如果可行,任何等效的 Go 包或函数都会被注明。由于 Go 和 Java 在它们的可用库中不是一一对应的,如果在标准的 Go 库中存在 Go 等价类,那么让每个 Java API(方法)与 Go 等价类完全匹配是不切实际的。

Java 语言包

Java 标准版 (JSE)有很多类型和方法的捆绑包。可用的方法(也称为 API)数以千计。Go 也有一套标准包,其类型和功能多达数百种。在这些包和类型的集合之间,库行为有很大的重叠,但组织结构(行为所在的包、类型或功能)没有重叠。

仅仅列出(对比描述)所有的 JSE 包和它们包含的方法类型就需要上百页。本书不会试图这样做。相反,它将列出 JSE 包和类型的精选子集。对于其中的一些,它将把关键 Java 类型可用的方法与任何 Go 等价物进行比较。

JRE 在java.lang包中有一些关键类型。下面的列表描述了任何 Go 环境的等价物。

接口摘要

  • 可追加–可以追加到该类型的实例中;由 Go 片隐式支持。

  • auto closeable–可以通过使用资源进行尝试来关闭;没有直接的 Go 对应物。

  • char sequence–字符序列(如字符串或 StringBuilder);没有直接的 Go 对应物。

  • 可克隆–实现Object.clone();没有直接的 Go 对应物。

  • 可比的–可以支持一个compareTo()方法;没有直接的 Go 对应物;许多类型是隐式可比的。

  • iterable–可以迭代;一些 Go 类型:数组、切片、映射、通道。

  • 可读–可以将字符读入缓冲区;io.Reader为 UTF-8。

  • runnable——可以作为线程体运行;在 Go 中,任何函数都可以作为 goroutine 运行。

课程总结

  • 布尔–布尔包装器;Go 系列不需要。

  • 字节-字节包装器;Go 系列不需要。

  • 字符-字符包装器;Go 系列不需要。

  • –类的运行时视图;用于反射的特征;Go 有反射包。

  • class loader–在运行时加载/管理类;在 Go 中不需要,没有运行时类。

  • 双层包装纸;Go 系列不需要。

  • 枚举>–所有枚举类型的基本类型;在 Go 中不需要(int是大多数 Go 枚举的基础)。

  • 浮动——浮动包装;Go 系列不需要。

  • integer–Int 包装器;Go 系列不需要。

  • 长长的包装纸;Go 系列不需要。

  • 数学——一个有一套数学工具的类;Go 也有类似的math包。

  • 模块——模块的运行时视图;没有直接的 Go 对应物。

  • number——数值包装类型的超类;没有直接的 Go 对应物。

  • object–所有对象类型的超类;没有直接的 Go 对应物;interface{}最接近。

  • 包–包的运行时视图;没有直接的 Go 对应物。

  • 进程——外部程序的运行时视图;一个 Go exec包有类似的。

  • ProcessBuilder 运行外部程序的助手;一个 Go exec包有类似的。

  • 记录(新)-类似类的结构;Go 有struct型。

  • 运行时——管理运行程序的实用程序;Go 有一个runtime包。

  • runtime permission——控制对类中函数的访问;Go 没有对等物。

  • security manager——控制对类中某个函数的访问的方法;Go 没有对等物。

  • 短——短包装纸;Go 系列不需要。

  • StackTraceElement–描述一个调用堆栈元素;Go 也有类似的结构类型。

  • stack walker–抓取堆栈;没有直接的 Go 对应物;一个可以写。

  • strict Math——像一门数学课,有更多关于算法如何工作的规则;没有直接的 Go 对应物。

  • 字符串–字符串类型;Go 有string类型和stringsstrconvfmt包。

  • StringBuffer,StringBuilder–一个可变的字符串类型;Go 有一个strings.Builder型。

  • 系统-管理运行程序的实用程序;Go 有runtimetimeio包。

  • 线程——操作系统线程;没有直接的 Go 对应物;go 有 goroutines。

  • thread group–相关线程的集合;没有直接的 Go 对应物。

  • thread local–具有线程相关值的变量;没有直接的 Go 对应物;可以制造。

  • throwable——可以被抛出的类型;Go 有恐慌。

  • void–不等同于 Go;Go 函数可以不返回任何值(相对于 void)。

  • 数学。big integer–不定精度整数;Go 属于 math.Int 类型。

  • 数学。BigDecimal–不确定精度的十进制浮点值;Go 有数学。Float(不过是二进制,不是十进制)。

系统类:

静态字段摘要

  • PrintStream err – STDERR – Go os.Stderr

  • 输入流输入 – STDIN – 转到 os.Stdin

  • 打印流出 – 标准输出 – 转到 os.Stdout

方法总结。如果没有提到,则没有直接的 Go 等价物。

  • array Copy(…)–复制一个数组;Go 运算符:数组[:]和copy(..)函数

  • clearProperty(字符串键)–空系统属性

  • console()–访问操作系统控制台

  • currentTimeMillis()–获取当前纪元时间;转到time.Now()

  • exit(…)–查看运行时类;转到os.Exit(...)

  • GC()–查看运行时类;转到runtime.GC()

  • getenv()–获取所有环境值;转到os.Environ()

  • getenv(…)–获取单个环境值

  • Get logger(…)–获取一个命名的记录器;转到log

  • Get properties()–获取所有属性

  • getProperty(…)–获取一个命名的属性

  • getSecurityManager()–获取一个 JVM 安全管理器

  • identity hashcode(Object x)-获取对象的标识;在 Go 中,使用& x 运算符

  • line separator()–获取一个操作系统行分隔符(例如,NL,CR+NL)

  • load(…),loadLibrary(…)–参见运行时类

  • nano time()–获取以纳秒为单位的运行时间;转到time

  • run finalization()–查看运行时类

  • set err(…)-更改 STDERR

  • setIn(...) – 更改 STDIN

  • setOut(…)–更改标准输出

  • setProperties(属性属性)–设置许多系统属性

  • setProperty(字符串键,字符串值)-设置系统属性

  • setSecurityManager(…)–设置 JVM 安全管理器

属性有社区实现。参见 github.com/magiconair/properties 使用类似 Java 属性的文件格式的例子。

运行时类:

  • addShutdownHook(…)–在 JVM 的出口运行一个线程;没有直接的 Go 对应物;Go 可以陷 OS 信号;Go 可以捕捉恐慌。

  • available processors()–获取 CPU(核心)计数;去runtime.NumCPU()

  • exec(...)–启动外部流程的方法系列;去exec.Cmd

  • Exit(…)–用 cleanup 退出 JVM 没有 Go 对应物;去罐大约。通过使用os.Exit()

  • free memory()–获取 JVM 可用的空闲内存;去runtime.MemStats

  • GC()–运行垃圾收集;去runtime.GC ()

  • Get runtime()–获取该类的 singleton 没有 Go 等价物。

  • halt(…)–不清理就退出 JVM 去os.Exit(...)

  • load(…),loadLibrary(…)–加载外部代码库;没有 Go 等价物。

  • maxMemory()–获取 JVM 可用的最大内存;去runtime.MemStats

  • removeShutdownHook(…)–移除出口挂钩;没有 Go 等价物。

  • run finalization()–强制对象终结;没有 Go 等价物。

  • total memory()–获取 JVM 使用的内存;去runtime.MemStats

  • version()–获取 JVM 版本;去runtime.version()

由于 Go 是在构建时生成的一个完整的可执行文件,所以不需要加载系统库。

Java IO 包

JRE 在java.io包中有一些关键类。下面的列表描述了任何 Go 环境的等价物。

界面摘要

  • 可关闭–可以关闭。由 try with resources 使用;没有直接的 Go 对应物。

  • Data input–数据可以作为二进制编码值流读取。一些 Go 编码库提供了类似的功能。

  • Data output–数据可以作为二进制编码值流写入。一些 Go 编码库提供了类似的功能。

  • 可外部化——可以使用非标准编码将数据读写到流中;没有直接的 Go 对应物。

  • file filter–选择与过滤器回调匹配的目录路径;没有直接的 Go 对应物。

  • filename filter–选择与过滤器回调匹配的文件名;没有直接的 Go 对应物。

  • 可刷新–可以刷新(持久保存缓冲数据);一些 Go io包接口提供了这个操作。

  • object input–可以读取 Java 序列化对象(DataInput 的超集);没有直接的 Go 对应物。

  • object output–可以编写 Java 序列化对象(DataOutput 的超集);没有直接的 Go 对应物。

  • serializable–通过默认编码将类型声明为可序列化;没有直接的 Go 对应物。

  • 课程总结

  • BufferedInputStream–带缓冲区的输入流(字节数);Go bufio包提供了类似的支持。

  • BufferedOutputStream–带缓冲区的输出流(字节数);Go bufio包提供了类似的支持。

  • buffered reader–带缓冲区的输入编写器(在字符上);Go bufio包提供了类似的支持。

  • buffered writer–带缓冲区的输出编写器(在字符上);Go bufio包提供了类似的支持。

  • ByteArrayInputStream–从 byte[]读取;Go io包提供了类似的支持。

  • ByteArrayOutputStream–在 byte[]上写入;Go io包提供了类似的支持。

  • CharArrayReader–在 char[]上写入;Go io包提供了类似的支持。

  • CharArrayWriter–在 char[]上写入;Go io包提供了类似的支持。

  • 控制台——标准输入、标准输出和标准错误的抽象。Go io包提供了类似的支持。

  • data inputstream–读取二进制编码值的流;一些 Go 编码库提供了类似的功能。

  • data output stream–写入二进制编码值的流;一些 Go 编码库提供了类似的功能。

  • 文件-对文件(或目录)的访问;Go ioos包提供了类似的支持。

  • 文件描述符-访问主机操作系统文件;Go ioos包提供了类似的支持。

  • file inputstream–从文件中读取字节;Go ioos包提供了类似的支持。

  • file output stream–将字节写入文件;Go ioos包提供了类似的支持。

  • file permission–访问文件权限;Go ioos包提供了类似的支持。

  • 从文件中读取字符;Go ioos包提供了类似的支持。

  • FileWriter–将字符写入文件;Go ioos包提供了类似的支持。

  • InputStream–读取字节;Go ioos包提供了类似的支持。

  • InputStreamReader–将字节输入转换为字符输入;Go ioos包提供了类似的支持。

  • ObjectInputStream 读取序列化对象;没有直接的 Go 对应物。

  • object output stream–编写序列化对象;没有直接的 Go 对应物。

  • output stream–写入字节;Go ioos包提供了类似的支持。

  • output streamwriter–将字符转换为字节。

  • PrintStream–格式化的字节输出;Go fmtioos包提供了类似的支持。

  • PrintWriter–格式化字符输出;Go fmtioos包提供了类似的支持。

  • RandomAccessFile–支持查找的文件;Go ioos包提供了类似的支持。

  • 阅读器-阅读字符;Go fmtioos包提供了类似的支持。

  • SequenceInputStream–连接输入流;Go ioos包提供了类似的支持。

  • stream tokenizer–标记流输入;Go fmtioos包提供了类似的支持。

  • string reader–从字符串中读取字符;Go fmtioos包提供了类似的支持。

  • 作家——写人物;Go ioos包提供了类似的支持。

Java 还有一个 NIO(新 IO)包,提供更高级的文件(例如,监控文件更改)和目录服务。这本书不会涵盖他们。Go 库有一些功能可以与一些 NIO 类提供的功能相媲美。

Java 文本包

JRE 在java.text包中有一些关键类。这个包提供了文本序列和消息格式的双向迭代。下面的列表描述了任何 Go 环境的等价物。一些 Go 扩展库和社区库提供了类似的支持。

界面摘要

  • AttributedCharacterIterator–对属性化文本序列进行双向迭代;没有直接的 Go 对应物。

  • character iterator——文本序列的双向迭代;没有直接的 Go 对应物;utf8utf16包有一定的功能。

课堂总结。除非特别说明,Go 没有直接的对等词。

  • 注释–类似注释的文本属性。

  • attributed String–带注释的字符串。

  • 双向——提供双向遍历规则。

  • break iterator–迭代不同类型的断点(单词、行等)。).

  • choice format–帮助格式化具有不同计数和复数的消息。

  • CollationElementIterator–根据区域设置规则遍历中的字符。

  • collation Key–基于区域设置的排序规则的键。

  • collator–基于区域设置的排序规则的基类。

  • compact number format–使数字变小的十进制格式。

  • date format–格式化日期和时间;Go 有一个类似功能的time包。

  • decimal format–格式化十进制数字。

  • 格式–各种格式类别的基本类别。

  • message format–用替换来格式化消息。

  • 规格化器–规格化 Unicode 文本以帮助排序。

  • number format–数字格式化程序的基类。

  • RuleBasedCollator–规则表驱动程序整理器。

  • simple Date format–日期格式,具有可配置的日期和时间结构;Go 有一个类似功能的time包。

  • StringCharacterIterator–迭代字符串中的字符。

注意 Go 的fmt包可以用来完成各种格式类型的一些任务。此外,在 Java String.format()和 Go fmt.Sprintf()中可以做格式化程序做的很多事情。

Java 时间包

JRE 在java.time包及其子包中有一些关键类。下面的列表描述了任何 Go 环境的等价物。

界面总结。Go 有time包可以解决其中的一些问题;大多是作为函数,而不是类型;这个函数只有一小部分出现在 Go 中。一些 Go 扩展库和社区库提供了类似的支持。

  • ChronoLocalDate–一些年表中的日期

  • ChronoLocalDateTime–某些年表中的日期时间(时间戳)

  • 年表——一个日历系统(比如公历)

  • ChronoPeriod–一个时间段

  • ChronoZonedDateTime>–某些年表中的时区日期时间(时间戳)

  • 纪元——一些年表中的有界范围(如公元前)

  • 时间的;操作日期和时间的步骤如下

  • 临时处理器

  • 临时调整

  • 临时账户

  • 临时字段

  • 风暴查询

  • 临时的

课堂总结。Go 有如下一些行为的time包:主要是作为函数,而不是类型。

  • 时钟-访问日期和时间

  • 持续时间——一段时间;Go 具有持续时间类型

  • 瞬间——一瞬间

  • local date–本地时区中的日期

  • local datetime–本地时区中的日期和时间(也称为时间戳)

  • local time–当地时区中的时间

  • 月日-一个月中的一天

  • offset datetime–相对于 UTC 的日期和时间(即时间戳)偏移量

  • offset time–相对于 UTC 的时间偏移量

  • 周期–以日历单位表示的持续时间

  • 年–以年为单位的持续时间

  • year month–月份解析的瞬间

  • zoned DateTime–时区中的日期时间

  • datetime formatter–格式化日期时间

  • DateTimeFormatterBuilder–创建格式化程序

  • 年表——年表(日历系统)的基础

  • 年代学,年代学,等时学

  • 日本年表,日本日期,日本

  • 民国年表

  • ThaiBuddhistChronology,ThaiBuddhistDate

Java Util 包

JRE 在java.util包及其子包中有一些关键类。子包处理对象集合、遗留日期和时间处理、并发(线程)处理以及对对象的并发访问。下面的列表描述了任何 Go 环境的等价物。

界面总结。大多数没有直接的 Go 等价物。这个函数的大部分是由 Go 内置类型提供的。一些 Go 扩展库和社区库提供了类似的支持。

  • 集合–e 类型的可迭代集合。

  • 比较器–比较 t 类型的两个可比较对象。

  • deque–E 类双头队列;Go 切片已接近。

  • 枚举–支持对 e 类型集合的正向迭代。

  • event listener–形式化事件侦听器的类型(回调)。

  • format table–可以格式化。

  • 迭代器–支持对 e 类型集合的双向迭代。

  • 列表–E 型可转位集合;Go 切片已接近。

  • 地图,地图。条目–具有键 K 的类型 V 的关联集合;Go 地图已经很接近了。

  • 队列–E 类队列(FIFO);Go 切片已接近。

  • set–一套 K 型;Go 地图已经很接近了。

  • sorted Map–带有排序关键字的地图。

  • sorted Set–用已排序的元素设置。

课堂总结。大多数没有直接的 Go 等价物。

  • abstract collection–以下是该类型的基本实现。

  • 抽象列表

  • 摘要图〔??〕

  • 摘要队列〔??〕

  • 抽象序列列表

  • 抽象集

  • array deque【】阵列上的任何位置。

  • ArrayList–数组上的列表;Go 具有切片类型。

  • 数组——数组访问的助手。

  • Base64。解码器–解码 Base64 字符串;Go 有一个base64包。

  • Base64。编码器–对 Base64 字符串进行编码;Go 有一个base64包。

  • 比特集–比特的集合;Go 有一个bits包。

  • 日历–日历;Go 有一个time包。

  • 集合——集合的助手。

  • 货币——一种货币。

  • 日期——一个日期;Go 有一个time包。

  • 字典–一种基本的地图类型;Go 有一个map型。

  • 枚举按钮,v>

  • 枚举集〔??〕

  • event list pro xy〔??〕

  • 事件对象

  • 格式程序

  • 公历-西历。

  • HashMap–默认地图类型;Go 有一个地图类型。

  • 哈希集合–默认集合类型。

  • 哈希表–线程安全的哈希表。

  • identity hashmap–以对象标识为关键字的映射;Go 有一个 map[uintptr]类型。

  • linked hashmap–按照加法顺序迭代的地图。

  • linked hashset–按加法顺序迭代的集合。

  • linked List–由链表支持的列表。

  • 语言环境–定义区分语言环境的设置和行为。

  • 对象–所有引用类型的帮助。

  • 可选的–空安全包装器。

  • 优先级队列–按优先级排序的列表。

  • properties–具有持久形式的键/值集合。

  • 扫描仪-读取格式化输入;去fmt包。

  • simple time zone–一个时区实现。

  • 堆栈–按后进先出顺序处理的列表。

  • String joiner–字符串助手。

  • string tokenizer–简单的字符串解析器;去fmt包。

  • 定时器——每隔一段时间驱动事件(回调)。

  • TimerTask–间隔驱动事件(回调)。

  • 时区–时区的基准。

  • 树形图–按关键字排序的地图。

  • TreeSet–按关键字排序的集合。

  • UUID-UUID 型;可从第三方获得。

  • vector–线程安全数组列表。

  • weak hashmap–不阻止键 GC 的映射。

  • 界面摘要

  • 阻塞队列–使用多个消费者线程进行排队。

  • blocking Queue–具有多个消费者线程的队列。

  • 可调用–线程可以异步调用。

  • concurrent map–线程安全的高并发映射。

  • ConcurrentNavigableMap–线程安全的高并发映射。

  • 执行者——多线程的管理者。

  • ExecutorService–管理多个线程。

  • 流动。处理器–反应式编程流程处理器。

  • 流动。发布者–反应式编程流程发布者。

  • 流动。订户–反应式编程流程订户。

  • 流动。订阅–反应式编程流订阅。

  • 未来–可以在未来完成的异步任务。

  • 条件–带锁的外部化条件;去sync.Cond

  • 锁定-锁定对关键部分的访问;去sync.Mutex

  • 读写锁–多读,单写锁。

课堂总结。上述接口的部分或全部实现。名称通常描述该功能。许多没有直接的 Go 等价物。LoadXStoreX函数提供了类似 Java volatile修饰符的行为。

  • 抽象执行服务

  • array blockqueue

  • 可完成的未来〔??〕

  • 并发哈希表

  • 并发〔??〕

  • 并发队列〔??〕

  • concurrentskiplistmap〔??〕

  • concurrents kiplistset〔??〕

  • CopyOnWriteArrayList

  • CopyOnWriteArraySet

  • CountDownLatch–等待计数;Gosync.WaitGroup也差不多。

  • cyclic barrier–允许多个线程到达同步点。

  • delay Queue–延迟的队列(在特定时间使能)。

  • 交换器–允许线程交换项目。

  • executors–创建多线程的管理器。

  • ForkJoinPool–在多线程中分而治之。

  • 未来–可以在未来完成的异步任务。

  • linkblockgdeque〔??〕

  • linkeding queue〔??〕

  • link transfer queue

  • phaser–线程同步;增强型 CyclicBarrier 或 CountDownLatch。

  • 优先级阻塞队列

  • ScheduledThreadPoolExecutor

  • 信号量——对关键会话的基本控制访问;Go 有一个lock包。

  • 同步队列

  • 线程池执行器

  • Atomic boolean——Atomic 允许线程间安全的读-修改-写循环;Go 有一个async包。

  • atomic integer–Go 有一个async包。

  • 数组也能无锁

  • atomicintegerfieldupdater 更新程式

  • AtomicLong–Go 有一个async包。

  • 原子克隆阵列

  • atomiclongfieldupdater 更新程式

  • AtomicMarkableReference

  • atomic reference–Go 有一个async包。

  • 原子分离机阵列〔??〕

  • 原子参考场更新

  • AtomicStampedReference

  • 双累加器——累加器/加法器支持线程间的安全读-修改-写循环。

  • double adder–Go 有一个async包。

  • long accumulator–Go 有一个async包。

  • long adder–Go 有一个async包。

  • 锁定支持–锁定助手。

  • reentrant Lock–锁实现。

  • 可重入读写锁

注意 Java 中的锁(和synchronized访问)是可重入的;同一个线程可以多次获取锁。在 Go 中,锁是不可重入的,试图重新获取锁的同一个 goroutine 会阻塞(死锁)自己。

十三、关键方法/函数比较

这个库调查并不打算作为程序员的参考,更多的是一个介绍。它将有一些有用的库函数的例子,应该可以帮助你在 Go 库中获得基本的能力。要更深入地理解提到的函数和任何省略的函数,请参阅在线 Go 软件包文档。该文档描述了每个包及其类型和它们提供的功能。也有一些例子。

Go 和 Java 库有很大的不同,因为 Java 是面向对象的,而 Go 不是。在 Java 中,许多函数是某个接收者类型的实例方法(隐含的this参数)。在 Go 中,它们通常是通用函数,将接收者类型作为第一个参数。这更像是 Java 中的static方法。例如,以将字符串转换为全大写的函数为例。

在 Java 中,这是

var uc = "some string".toUpperCase();

在 Go 中,这是

var uc = strings.ToUpper("some string")

这些函数的最大区别在于接收方传递给函数的方式。Java 实例方法可以被定义为

public class String {
  public static String toUpperCase(String s) {
    :
  }
}

其访问方式如下

var uc = String.toUpperCase("some string")

因此,很像 Go 的风格,但是 Java 设计者选择不这样做。

Java 有很多很多的库函数。下面的表格将总结几个常用的 Java 函数,跨越几个 Java 包和类型,列出它们的 Java 和 Go 等价物(其行为可能不总是与 Java 函数完全一样)。有关 Go 函数的更完整列表,请参见“Go 库调查”部分。

静态 Java 方法和 Go 包顶层函数都是以句点(" . "),如表 13-1 至 13-8 所示。实例方法有一些变量(接收者)。

表 13-1

关键 Java 对象类方法

|

Java 函数

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- |
| o.toString() | o.String() | Many | 如果由接收类型定义 |
| o.wait() | c.Wait() | sync.Cond |   |
| o.notify() | c.Signal() | sync.Cond |   |
| o.notifyAll() | c.Broadcast() | sync.Cond |   |
| o.hashCode |   |   | 没有 Go 等价物 |
| o.equals(x) | x ==1T5y |   | Go 有一个运算符 |

表 13-2

关键 Java 运行时类方法

|

Java 函数

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- |
| add/removeShutdownHook | Signal | os | Go 只监听信号 |
| availableProcessors | NumCPU() | runtime |   |
| exec(...) | Exec(...) | runtime | 许多变体 |
| exit(n) |   |   | 没有 Go 等价物 |
| freeMemory() | ReadMemoryStats() | runtime |   |
| maxMemory() | ReadMemoryStats() | runtime |   |
| gc() | GC() | runtime |   |
| halt() | Exit(n) | runtime |   |
| runFinalization() |   |   | 没有 Go 等价物 |
| version() | Version() | runtime |   |

表 13-3

关键的 Java 系统类方法(运行时的重复被省略)

|

Java 函数

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- |
| .arraycopy(a,...) | a[:] |   | Go 运算符 |
| .clear/set/getProperty``.getProperties() |   |   | 没有 Go 等价物 |
| .console |   |   | 没有 Go 等价物 |
| .currentTimeMillis | Now() | time |   |
| .getenv() | Environ() | os |   |
| .getLogger() |   | log | Go 使用记录器方法 |
| .get/SetSecurityManager() |   |   | 没有 Go 等价物 |
| .identityHashCode() |   |   | 没有 Go 等价物 |
| .lineSeparator |   |   | Go 一般忽略 CR |
| .nanoTime() | Now() | time |   |
| SetIn/Out/Err |   |   | 可以设置 os.Stdin/out/err 值 |

表 13-4

关键 Java 字符串类方法

|

Java 函数

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- |
| s.substr(s,e) | s[s:e] |   | Go 运算符 |
| s.charAt(i) | s[i] |   | Go 运算符 |
| s.indexOf(s2) | .Index(s, o) | strings |   |
| s.lastIndexOf(s2) |   | strings |   |
| s.indexOf(c) |   | strings |   |
| s.lastIndexOf(c) |   | strings |   |
| s.toUpperCase() | .ToUpper(s) | strings |   |
| s.toLowerCase() | .ToLower(s) | strings |   |
| s.toCharArray() | []byte(s) |   | 转到转换 |
| s.length() | len(s) |   | 内置 |
| s.compareTo(o) | s op o |   | Go 运算符:< <= == != > >= |
| s.startsWith(s2) | .HasPrefix(s1, s2) | strings |   |
| s.endsWith(s2) | .HasSuffix(s1, s2) | strings |   |
| s.contains(s2) | .Index(s,s2) >= 0 | strings |   |
| s1 + s2 | s1 + s2 |   | Go 运算符 |
| .join(delim,s...) | .join(delim,s...) | strings |   |
| s.getBytes() | []byte(s) |   | 转到转换 |
| s.matches(s1) | .matches(s, s1) | regex |   |
| s.repeat(n) | .Repeat(s, n) | strings |   |
| s.replace(c1,c2) |   |   | 没有直接的对等物 |
| s.replace(s1,s2) | .ReplaceAll(s, s1, s2) | strings |   |
| s.replaceAll(p,s2) |   | regex |   |
| s.replaceFirst(p,s2) |   | regex |   |
|   | .Split(s, s2) | strings |   |
|   | .Split(s, s2, n) |   |   |
| s.split(p) |   |   | 没有直接的对等物 |
| s.split(p,n) | .Split(s, n) | regex |   |
| s.trim()``s.strip()``s.stripLeading()``s.stripTrailing() | .TrimSpaces(s) | strings |   |
| s.substring(p)``s.substring(p,e)``s.substring(0,e)``s.substring(0,s.length()) | s[p:]``s[p:e]``s[:e]``s[:] |   | Go 运算符 |
| .valueOf(x)``.format(f, ...) | .Sprintf("%v",x)``.Sprintf(f,...) | fmt |   |

表 13-5

关键 Java StringBuilder 类方法

|

Java 函数

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- |
| sb.append(o) | b.Write``String(s) | strings.``Builder |   |
| sb.length() | b.Size() | strings.``Builder |   |

表 13-6

关键 Java 列表接口方法

|

Java 函数

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- |
| l.add(x) | append(s,x) |   | 内置 |
| l.size() | len(l) |   | 内置 |
| l.get(i) | l[i] |   | Go 运算符 |
| l.set(I, x) | l[i] = x |   | Go 运算符 |

表 13-7

关键的 Java Map 接口方法

|

Java 函数

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- |
| m.put(k,v) | m[k] = v |   | Go 运算符 |
| m.size() | len(m) |   | 内置 |
| m.get(k) | m[k] |   | 内置缺失测试 |

表 13-8

关键 Java PrintWriter 类方法

|

Java 函数

|

Java 类型

|

Go 等效

|

Go 包

|

笔记

|
| --- | --- | --- | --- | --- |
| pw.println(o) | Print``Writer | w.Write(o)``w.Write('\n') | io.Writer |   |

十四、Go 包调查

本章包含几个 Go 软件包的简要介绍。接下来的其他章节将更详细地讨论选择包。

使用 Go 访问文件

Go 和 Java 有所不同的一个常用领域是访问文件和目录。Java 的设计比 Go 更倾向于将文件访问抽象化。Java 提供了多种访问模式:

  1. 字节/字符流-文件看起来像字节或字符序列。

  2. 字节/字符通道–支持块模式访问;面向随机和异步访问;可以跨进程共享内存。

  3. 高级文件操作抽象,如复制文件或遍历目录树。

Go 提供了不太抽象但通常等价的访问,特别是对于流风格的操作。Go access 类似于 Unix 风格的文件处理。存在与 Unix 文件 API 非常匹配的 API。文件几乎总是被视为字节序列。Go 还提供了与 Unix APIs 几乎完全匹配的低级 API。

这些差异体现在 Java 和 Go APIs 的风格上。

例如,使用基本的 Java 流,可以复制这样一个文件:

public static long copyFile(String fromPath, String toPath)
    throws IOException {
  try (var bis = new BufferedInputStream(new FileInputStream(
     new File(fromPath)))) {
    try (var bos = new BufferedOutputStream(new FileOutputStream(
        new File(toPath)))) {
      return copyContent(bis, bos);
    }
  }
}

注意两个 try 语句可以合并成一个 try 语句:

public static long copyFile(String fromPath, String toPath)
    throws IOException {
  try (var bis = new BufferedInputStream(new FileInputStream(
        new File(fromPath)));
      var bos = new BufferedOutputStream(new FileOutputStream(
           new File(toPath)))) {
    return copyContent(bis, bos);
  }
}

文件数据由复制

private static long copyContent(InputStream is, OutputStream os)
    throws IOException {
  var total = 0L;
  var buf = new byte[N]; // N at least several KB
  for (;;) {
    var count = is.read(buf, 0, buf.length);
    if (count == 0)
      break;
    os.write(buf);
    total +=buf.length;
  }
  return total;
}

或者更简洁地说,通过使用 JRE 库等价物:

private static long copyContent(InputStream is, OutputStream os)
    throws IOException {
  return is.transferTo(os);
}

在 Go 中,这可能是

func CopyFile(fromPath, toPath string) (count int64, err error) {
      var from, to *os.File
      if from, err = os.Open(fromPath); err != nil {
            return
      }
      defer from.Close()
      if to, err = os.Create(toPath)
            err != nil {
            return
      }
      defer to.Close()
      count, err = io.Copy(to, from)
      return
}

因此,我们看到 Go 倾向于更直接地使用文件类型。这是因为File类型实现了io.Readerio.Writer接口。我们还看到了如何用多个defer语句替换try语句。

Go 和 Java 都可以处理目录。例如,要输出从某个根目录开始的所有文件及其大小,在 Java 中可以这样做(使用 Java 流):

public static void PrintAllNames(String path) throws IOException {
  try (var walk = Files.walk(Paths.get(path))) {
    walk.filter(Files::isRegularFile).
        map(p -> String.format("%s %d", p, p.toFile().length())).
        forEach(System.out::println);
  }
}

这样叫:

PrintAllNames(".")

在 Go 中,这可能是

var printName = func(path string, info os.FileInfo, xerr error) error{
      if xerr != nil {  // exit fast if entered with an error
            return xerr
      }
      if info.Mode().IsRegular() {
            fmt.Println(path, info.Size())
      }
      return nil
}
func PrintAllNames(path string) (err error) {
      err = filepath.Walk(path, printName)
      return
}

这样叫:

PrintAllNames(".")

Java 代码使用 Java 的函数流和方法引用/lambdas(作为回调)。Go 代码使用了一个回调函数。在这两种情况下,回调只选择文件并格式化数据。

压缩服务

本节概述了一些用于压缩和归档的 Go 包。

存档包

与 Java 一样,Go 提供了读写归档文件的功能。Java 专注于 ZIP 存档格式。Go 也支持 TAR 格式。每种格式都有子程序包:

  • tar–读写 tar 档案

  • zip–读写 ZIP 档案

archive/tar包提供了这些类型:

  • type Format–代表支持的 tar 格式 USTAR、PAX 和 GNU 的枚举

  • type Header–表示 tar 文件中的标题

  • 类型读取器–提供对 tar 的读取权限

  • 类型编写器—提供对 tar 的写访问权限

标题类型有以下方法:

  • func FileInfoHeader(fi os。FileInfo,链接字符串)(*Header,error)–从文件信息创建标题

  • func (h *Header) FileInfo() os。FileInfo–从文件头获取文件信息

读取器类型有以下方法:

  • func NewReader(r io。读者)*读者–成为读者

  • func (tr Reader) Next() (Header,error)-前进到下一个文件

  • func(tr * Reader)Read(b[]byte)(int,error)-从文件中读取数据

编写器类型有以下方法:

  • func NewWriter(w io。作家)*作家–成为作家

  • func (tw *Writer) Close()错误–刷新并关闭文件

  • func (tw *Writer) Flush()错误–写入缓冲数据

  • func(tw * Writer)Write(b[]byte)(int,error)–带缓冲写入数据

  • func(tw * Writer)Write Header(HDR * Header)错误–写入文件头

archive/zip包提供了这些类型:

  • type Compressor–将编写器转换为压缩编写器/关闭器的功能

  • 类型解压缩器–将阅读器转换为解压缩阅读器/关闭器的功能

  • 类型文件–表示压缩文件;包装文件头

  • type file header–表示 zip 文件中的文件;有许多有用的领域

  • ReadCloser 标牌-可读取和关闭

  • 类型阅读器-可以阅读

  • 打字者-可以书写

文件类型有这样的方法:

  • func (f *File) Open() (io。ReadCloser,error)–打开压缩文件

FileHeader 类型有以下方法:

  • func FileInfoHeader(fi os。FileInfo) (*FileHeader,error)-创建文件头

  • func (h *FileHeader) FileInfo() os。文件关于

  • func (h *FileHeader) ModTime()时间。时间

  • func(h * file header)mode()(OS 模式)。FileMode)

  • func(h * file header)setmodtime(t time)。时间)

  • func(h * file header)set mode(OS 模式)。FileMode)

ReadCloser 类型有以下方法:

  • func OpenReader(名称字符串)(*ReadCloser,错误)–访问压缩条目

  • func (rc *ReadCloser) Close()错误–关闭压缩条目

读取器类型有这个方法:

  • func NewReader(r io。ReaderAt,size int64) (*Reader,error)-创建一个阅读器

编写器类型有以下方法:

  • func NewWriter(w io。作家)*作家–成为作家

  • func (w *Writer) Close()错误–关闭压缩条目

  • func (w *Writer) Create(名称字符串)(io。编写器,错误)–添加压缩条目

  • func(w * Writer)create header(FH * file header)(io。编写器,错误)

  • func (w *Writer) Flush()错误–刷新任何缓冲的输出

  • func (w *Writer) SetComment(注释字符串)错误

压缩包

Java 支持多种环境下的数据压缩。对于存档文件,通常会这样做。Go 使压缩成为一个更加独立和通用的动作。它支持多种形式的压缩。Go 支持(通过包)这些形式的压缩:

  • bzip2–bzip2 解压缩

  • gzip–gzip 压缩数据的读写

  • zlib–zlib 压缩数据的读写

  • flate–放气压缩

  • 伦佩尔-齐夫-韦尔奇压缩公司

这些可用于压缩/解压缩字节流,通常是压缩/解压缩到文件中。它们由归档包使用。有关更多详细信息,请参见 Go 包文档。

作为使用这些包(和os包)的例子,让我们将一个文件压缩成另一个 GZ 格式的文件。下面是一个可能的实现:

func CompressFileToNewGZIPFile(path string) (err error) {
      var inFile, gzFile *os.File
      // access input file
      if inFile, err = os.Open(path); err != nil {
            return
      }
      defer inFile.Close()
      // create output file
      if gzFile, err = os.Create(path + ".gz"); err != nil {
            return
      }
      defer gzFile.Close()
      // copy input to output, compressing as copied
      w := gzip.NewWriter(gzFile)
      defer w.Close()
      _, err = io.Copy(w, inFile)
      return
}

请注意,如果此函数返回一个错误,则可能会用无效数据创建输出文件。

如果你替换

w := gzip.NewWriter(gzFile)

随着

w := gzFile

并且去掉最后一个defer语句,你会得到一个未压缩的文件副本。

图像

image包及其子包为读取、绘制和格式化图像提供了支持。image包包含使用各种形式/大小的颜色的图像表示,例如 Alpha、Alpha16、CMYK、Gray、Gray16、NRGBA、NRGBA64、NYCbCrA、Paletted、RGBA、YCbCr。它还支持几种图像类型:image、PalatedImage 和 Uniform。它还有一些关键的图像相关类型,如点和矩形。

有关使用此软件包的一些示例,请参见顶点计划。

映像包具有以下关键接口和结构:

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

image有一些关键方法:

  • func Decode(rio.Reader) (Image, string, error)–读取图像

  • func Encode(wio.Writer, mimage.Image) error——写一个形象

type Point struct {
    X, Y int
}
type Rectangle struct {
    Min, Max Point
}

点和矩形具有创建、调整和比较值的方法:

  • func Pt(X,Y int)Point–创建一个点

  • 功能(p 点)添加(q 点)点–添加点

  • 函数(p 点)除法(k 整数)点–将 p 除以 k

  • func (p 点)Eq(q 点)bool–测试相等

  • func (p 点)In(r 矩形)bool–测试 p 是否在 r 中

  • func (p 点)Mod(r 矩形)点–p 在 r 中的模数

  • 函数(p 点)乘法(k 整数)点–将 p 乘以 k

  • 功能(p 点)子(q 点)点–减去一个点

  • func Rect(x0,y0,x1,y1 int)Rectangle–制作一个矩形

  • 功能(r 矩形)添加(p 点)矩形–添加矩形

  • func (r 矩形)At(x,y int) color。颜色测试–获取某一点的颜色

  • func(r Rectangle)Bounds()Rectangle–获取边界

  • func(r Rectangle)Canon()Rectangle–制作 r 的规范版本

  • func (r 矩形)ColorModel()颜色。模型–获取颜色模型

  • func(r Rectangle)Dx()int–获取宽度

  • func(r Rectangle)Dy()int–获取高度

  • func(r Rectangle)Empty()bool–测试是否没有包含点

  • func(r Rectangle)Eq(s Rectangle)bool–测试相等

  • func(r Rectangle)In(s Rectangle)bool–r all In s

  • func(r Rectangle)Inset(n int)Rectangle–返回 r inset 乘 n

  • func (r 矩形)Intersect(s 矩形)Rectangle–返回最大的交集

  • 函数(r 矩形)重叠(s 矩形)布尔–如果交集不为空

  • func (r 矩形)大小()点

  • 函数(r 矩形)子(p 点)矩形–将 r 平移-p

  • func(r Rectangle)Union(s Rectangle)Rectangle–返回覆盖 r 和 s 的矩形

所有的图像形式至少都有这些方法(有些超出了图像接口):

  • func (p * ) At(x,y int) color。颜色-获取某一点的值

  • func(p *)Bounds()Rectangle–获取图像边界

  • func (p * ) ColorModel()颜色。模型–获取图像颜色模型

  • func(p *)Opaque()bool–查看图像是否不透明(没有透明单元格)

  • func (p * ) PixOffset(x,y int)int–获取该点在像素列表中的偏移量

  • func (p * ) Set(x,y int,c color。颜色)–在一个点上设置值

  • func (p * ) SetAlpha(x,y int,c color。Alpha)–仅设置一个点的 alpha

  • func (p * )子图像(r 矩形)图像–获取图像的子集

输入/输出

ioioutil包提供了基本的 I/O 操作,这些操作抽象了基本操作系统(OS)提供的动作。这个io包主要由接口组成,而不是实现。

Go 有几个关键接口,当它们组合在一起时,允许丰富的输入/输出(I/O)功能。在最基本的层面上,I/O,就像在 Java 中一样,通常是在字节流上完成的,有时被解释为 UTF-8 字符。支持其他字符编码。

  • 读取器–可以读取一个字节或一系列字节

  • writer–可以写入一个字节或一系列字节

  • seeker–可以改变流中的读/写位置(一种随机 I/O 形式)

  • closer–可以关闭对流的访问

  • 前述接口的组合

许多 Go 类型都实现了这些接口,大多数用于字节,一些用于字符。例如,Go File类型,它允许使用一个打开的文件实例来访问它所代表的操作系统文件的内容。

ioutil包提供了常见文件和目录操作的实现。

Go 和 Java 都支持读写文件(或类似文件的对象)。Java 通过字节或字符流读/写器访问来支持这一点。Java 在其 NIO(新 I/O)包中也有更高的、通常更具性能的选项。Go 的访问级别通常更低,但是 Java 的缓冲流的等价物是可用的。

Go bufio包在非缓冲 I/O 的基础上实现了缓冲 I/O。它提供了类似 Java 支持的各种缓冲流的支持。

io包提供了这些类型(主要是接口)和功能:

  • ByteReader–读取一个字节

  • bytes scanner–读取和未读取的字节

  • 打字机–写入一个字节

  • 接近–接近

  • limited reader——有限制的阅读器

  • piped reader——来自管道的阅读器

  • piped writer–管道的编写器

  • read closer–可以读取和关闭

  • read seeker–可以阅读和搜索

  • 读写关闭——可以读、写、关闭

  • read write seeker–可以读、写、查找

  • 读写——可以读、写

  • 读者–可以阅读

  • reader at——在某一位置的读取受限

  • reader from–剩余读取

  • 阅读符文

  • 阅读和未阅读符文

  • section reader–读取一段(跨度)字节

  • 探索者-可以设定一个位置

  • string writer–编写一个字符串

  • write closer–可以写,关闭

  • write seeker–可以写、查找

  • 写入器–可以写入字节

  • WriterAt–可以在一个位置写入一个字节

  • writer to–可以写入有限的字节

io包有这个键值:

var EOF = errors.New("EOF")

io包具有以下功能:

  • func Copy(dst Writer,src Reader)(写入 int64,err error)-将字节 src 复制到 dst

  • func CopyBuffer(dst Writer,src Reader,buf []byte)(写入 int64,err error)–带缓冲区复制

  • func CopyN(dst Writer,src Reader,n int64) (written int64,err error)-有限制的复制

  • func Pipe() (PipeReader, Pipe writer)-在管道之间复制

  • func Read 至少(r Reader,buf []byte,min int) (n int,err error)-有限制地读取

  • func ReadFull(r Reader,buf []byte) (n int,err error)–读取所有可用的

  • func WriteString(w Writer,s string) (n int,err error)-写入一个字符串

  • func LimitReader(r Reader,n int 64)Reader–创建一个最多接受 n 个字节的读取器

  • func MultiReader(阅读器...Reader)阅读器——制作一个结合所有阅读器的阅读器

  • func TeeReader(r Reader,w Writer)Reader——创建一个将 r 复制到 w 的阅读器

  • func NewSectionReader(r ReaderAt,off int64,n int 64)* section reader–创建节阅读器

ioutil包有这个键值:

  • var Discard io .Writer用来丢弃任何写入其中的东西

ioutil包定义了这些函数:

  • func no closer(r io。读者)io。read closer–空阅读器

  • func ReadAll(r io。reader)([]字节,错误)–读取所有剩余字节

  • func ReadDir(目录名字符串)([]os。FileInfo,error)–读取目录

  • func ReadFile(文件名字符串)([]字节,错误)-读取整个文件

  • func TempDir(dir,pattern string)(名称字符串,err 错误)-创建唯一的命名目录

  • func TempFile(目录,模式字符串)(f *os。文件,err 错误)–创建一个唯一的命名文件

  • func WriteFile(文件名字符串,数据[]字节,perm os。FileMode)错误–写入整个文件

bufio包定义了这些函数(提供了各种扫描仪):

  • func ScanBytes(data []byte,atEOF bool) (advance int,token []byte,err error)

  • func ScanLines(data []byte,atEOF bool) (advance int,token []byte,err error)

  • func 扫描(data []byte,atEOF bool)(前进 int,token []byte,err error)

  • func 扫描字(data []byte,atEOF bool) (advance int,token []byte,err error)

ReadWriter类型实现了读和写接口:

  • func NewReadWriter(r *Reader,w *Writer) *ReadWriter

Reader类型实现了阅读器接口:

  • func NewReader(rd io。读者)*读者

  • func NewReaderSize(rd io。阅读器,尺寸 int)*阅读器

  • func(b *读取器)Buffered() int

  • func (b *Reader) Discard(n int)(丢弃的 int,err error)

  • func(b * Reader)Peek(n int)([]字节,错误)

  • func(b * Reader)Read(p[]byte)(n int,err error)

  • func (b *Reader) ReadByte()(字节,错误)

  • func (b *Reader)读取字节(delim 字节)([]字节,错误)

  • func(b * Reader)ReadLine()(line[]byte,isPrefix bool,err error)

  • func (b *Reader) ReadRune() (r rune,size int,err error)

  • func (b *Reader) ReadSlice(delim 字节)(line[]字节,err 错误)

  • func (b *Reader) ReadString(delim 字节)(String,error)

  • func(b *阅读器)复位(r io。读者)

  • func (b *Reader) Size() int

  • func (b *Reader) UnreadByte()错误

  • func(b * Reader)un readune()错误

  • func (b *Reader) WriteTo(w io。Writer) (n int64,err error)

Scanner类型实现扫描仪接口:

  • func 新闻扫描器(r io。阅读器)*扫描仪

  • func (s *Scanner)缓冲区(buf []byte,max int)

  • func (s *Scanner)字节()[]字节

  • func (s *Scanner) Err()错误

  • func(s *扫描仪)Scan() bool

  • func(s *扫描仪)分割(split SplitFunc)

  • func (s *Scanner) Text()字符串

Writer类型实现了编写器接口:

  • func NewWriter(w io。作家)*作家

  • func NewWriterSize(w io。Writer,size int) *Writer

  • func (b *Writer) Available() int

  • func (b *Writer)缓冲的()int

  • func (b *Writer) Flush()错误

  • func (b *Writer) ReadFrom(r io。读取器)(n int64,err error)

  • func (b *Writer)复位(w io。作家)

  • func (b *Writer) Size() int

  • func (b *Writer)写入(p[]字节)(nn int,err error)

  • func (b *Writer) WriteByte(c 字节)错误

  • func(b * Writer)write rune(r rune)(size int,err error)

  • func(b * Writer)WriteString(s string)(int,error)

作为使用osbufio和其他包的一个例子,清单 14-1 和 14-2 展示了一个函数,它可以计算一个单词在某个文本文件中出现的次数。

func CountWordsInFile(path string) (counts map[string]int, err error) {
      var f *os.File
      if f, err = os.Open(path); err != nil {
            return
      }
      defer f.Close()
      counts, err = scan(f)
      return
}

func scan(r io.Reader) (counts map[string]int, err error) {
      counts = make(map[string]int)
      s := bufio.NewScanner(r)
      s.Split(bufio.ScanWords) // make into words
      for s.Scan() {           // true while words left
            lcw := strings.ToLower(s.Text()) // get last scanned word
            counts[lcw] = counts[lcw] + 1 // missing is zero value
      }
      err = s.Err() // notice any error
      return
}

Listing 14-1Word Count Example (Part 1)

这将返回文件中每个(忽略大小写)单词的计数图。一个扫描器的工作方式很像一个 Java 迭代器(通过它的hasNextnext方法)。函数ScanWords被传递给扫描器,以确定如何解析出单词。其他几种分割方法被预定义为按字节、符号或行扫描。有状态扫描器通过Text()(下一个扫描的字符串)和Err()(任何扫描错误)方法返回结果。如果没有错误,则设置文本。扫描在第一个错误时停止。

path := `...\words.txt` // point to a real file
counts, err := CountWordsInFile(path)
if err != nil {
      fmt.Printf("Count failed: %v\n", err)
      return
}

fmt.Printf("Counts for %q:\n", path)
for k, v := range counts {
      fmt.Printf("  %-20s = %v\n", k, v)
}

Listing 14-2Word Count Example (Part 2)

现在是时候来帮助我们的同胞了!

它产生以下内容:

Counts for ".../words.txt":
  Now                  = 1
  time                 = 1
  come                 = 1
  countrymen!          = 1
  our                  = 1
  is                   = 1
  the                  = 2
  to                   = 2
  aid                  = 1
  of                   = 1

字节包

在 Go 中,类型[]byte经常被使用,尤其是作为输入源或输出目标(I/O)。在 Java 中,使用字节数组作为目标的情况较少;相反,使用字节数组(或字符)上的流。Go bytes包提供了对字节片进行 I/O 的函数。

bytes包提供了这些类型和功能。大多数都是不言自明的,并且与strings包中的函数相匹配,因为字节片段通常可以被视为 ASCII 字符串,或者更具挑战性的 UTF 8 字符:

  • func Compare(a,b []byte) int

  • func 包含(b,subslice []byte) bool

  • func 包含三洋(b[]字节,char string)bool

  • func containsruna(b[]字节,r run)bool

  • 函数计数(s,sep []byte) int

  • func Equal(a,b[]字节)布尔值

  • func EqualFold(s,t[]byte)bool–case 折叠 s 和 t 后等于

  • func 字段(s[]字节)[][]字节

  • func field sfunc(s[]字节,f func(rune)bool[])字节

  • func HasPrefix(s, prefix []byte) bool

  • func HasSuffix(s,后缀[]字节)布尔

  • 函数索引(s,sep []byte) int

  • func index ny(s[]字节,char string)int

  • func 索引字节(b[]字节、c 字节)int

  • func 索引 unc(s[]字节,f func(r rune) bool) int

  • func index rune(s[]字节,r rune) int

  • func Join(s[][]字节,sep[]字节)[]字节

  • func LastIndex(s,sep []byte) int

  • func lastindexany(s[]字节,char string)int

  • func 负载索引字节(带[]字节、c 字节)int

  • func lastindexfunc(s[]字节,f func(r rune) bool) int

  • func Map(映射 func(r rune) rune,s []byte) []byte

  • func 重复(b[]字节,count int)[]字节

  • func Replace(s,old,new[]字节,n int)[]字节

  • func ReplaceAll(s,旧,新[]字节)[]字节

  • func runes(s[]字节)[]rune

  • func Split(s,sep[]byte)[][]byte–在 sep 处拆分,删除 sep

  • func SplitAfter(s,sep[]byte)[][]byte–在所有 sep 之后拆分

  • func SplitAfterN(s,sep []byte,n int)[][]byte–sep 后分割受 n 限制

  • func SplitN(s,sep []byte,n int) [][]byte

  • func Title(s[]byte)[]byte–字开始到标题大小写

  • func to lower(s[]字节)[]字节

  • func to title(s[]byte)[]byte–全部为标题大小写

  • func ToUpper(s []byte) []byte

  • func ToValidUTF8(s,替换[]字节)[]字节

  • func Trim(s[]字节,割集字符串)[]字节

  • func trimfunc(s[]字节,f func(r rune)bool[]字节

  • func trim left(s[]字节,割集字符串)[]字节

  • func trimleftfunc(s[]字节,f func(r rune)bool)[]字节

  • func 前缀(s,前缀[]字节)[]字节

  • func trim right(s[]字节,割集字符串)[]字节

  • func trimrghtfunc(s[]字节,f func(r rune)bool)[]字节

  • func TrimSpace(s []byte) []byte

  • func 三字尾(s,字尾[]位元组)[]位元组

缓冲器类型提供这些功能。大多数是不言自明的。Buffer类型提供了一种缓冲 I/O 的方法,通常来自文件或网络:

  • func new Buffer(buf[]byte)* Buffer

  • func NewBufferString(s 字符串)*缓冲区

  • func (b *Buffer) Bytes() []byte

  • func (b *Buffer) Cap() int

  • func (b *Buffer) Grow(n int)

  • func (b *Buffer) Len() int

  • func(b * Buffer)Next(n int)[]字节

  • func (b *Buffer)读取(p[]字节)(n int,err error)

  • func (b *Buffer) ReadByte()(字节,错误)

  • func(b * Buffer)Read bytes(delim byte)(line[]byte,err error)-一直读到 delim(通常是换行)

  • func (b *Buffer) ReadFrom(r io。读取器)(n int64,err error)

  • func (b *Buffer) ReadRune() (r rune,size int,err error)

  • func (b *Buffer) ReadString(delim 字节)(行字符串,err 错误)

  • func(b * Buffer)Reset()–返回起点

  • func (b *Buffer) Truncate(n int)

  • func (b *Buffer) UnreadByte()错误

  • func(b * Buffer)un readune()错误

  • func (b *Buffer)写(p[]字节)(n int,err error)

  • func (b *Buffer)写字节(c 字节)错误

  • func(b * Buffer)write rune(r rune)(n int,err error)

  • func(b * Buffer)WriteString(s string)(n int,err error)

  • func (b *Buffer) WriteTo(w io。Writer) (n int64,err error)

读取器类型提供了这些功能。大多数是不言自明的:

  • func new Reader(b[]字节)*Reader

  • func (r *Reader) Len() int

  • func (r *Reader)读取(b[]字节)(n int,err error)

  • func (r *Reader) ReadAt(b []byte,off int64) (n int,err error)

  • func (r *Reader) ReadByte()(字节,错误)

  • func(r * Reader)ReadRune()(ch rune,size int,err error)

  • func(r *读取器)复位(b[]字节)

  • func (r *Reader) Seek(偏移量 int64,where int)(int 64,error)

  • func (r *Reader) Size() int64

  • func (r *Reader) UnreadByte()错误

  • func(r * Reader)un readune()错误

  • func (r *Reader) WriteTo(w io。Writer) (n int64,err error)

格式化包

fmt包提供了一个格式化的字符串和 I/O 函数,很像 Java 的String.formatPrintStream/PrintWriterprintf函数。

scanner包提供了文本扫描和标记化。

tabwriter包提供了一种简单但功能较低的方式来生成表格(列化)文本输出。社区提供更高的功能支持。

fmt包提供了这些功能:

  • func Errorf(格式字符串,a...interface { })error–从格式化字符串中出错

  • func Fprint(w io。作家...interface{}) (n int,err error)–输出到 w

  • func Fprintf(w io。编写器,格式字符串,一个...interface{}) (n int,err error)–输出到 w

  • func Fprintln(w io。作家...interface{}) (n int,err error)–输出到 w 并添加 NL

  • 函数 Fscan(r io。读者 a...interface{}) (n int,err error)–按类型扫描来自 r 的输入

  • func Fscanf(r io。读取器,格式字符串,一个...interface{}) (n int,err error)–按类型扫描来自 r 的输入

  • func Fscanln(r io。读者 a...interface{}) (n int,err error)–按类型从 r 扫描输入行

  • 功能打印(a...interface{}) (n int,err error)–输出到 STDOUT

  • func Printf(格式字符串,a...interface{}) (n int,err error)–输出到 STDOUT

  • func Println(a...interface{}) (n int,err error)–输出到添加了 NL 的 STDOUT

  • 功能扫描(a...interface{}) (n int,err error)–扫描来自 STDIN 的输入

  • func Scanf(格式字符串,a...interface{}) (n int,err error)–扫描来自 STDIN 的输入

  • 函数扫描(a...interface{}) (n int,err error)–从 STDIN 扫描行输入

  • func Sprint(a...interface { })string–输出字符串

  • func Sprintf(格式字符串,a...interface { })string–输出字符串

  • func Sprintln(a...interface { })string–输出添加了 NL 的字符串

  • func Sscan(字符串 string,a...interface{}) (n int,err error)–从字符串中扫描输入

  • func Sscanf(字符串,格式字符串,一个...interface{}) (n int,err error)–从字符串中扫描输入

  • func Sscanln(字符串 string,a...interface{}) (n int,err error)–从字符串中扫描行输入

fmt包提供了这些类型:

  • 类型格式化程序–一种可以自我格式化的类型

  • GoStringer 类型——这种类型可以支持根据%#v用细节格式化自身

  • 扫描仪类型–可以自定义扫描方法的类型

  • Stringer 类型–可以将自身格式化为字符串的类型

scanner包提供了这些类型和功能。

Scannerio.Reader中读取字符和标记:

  • func (s Scanner) Init(src io。阅读器)扫描仪–制作扫描仪

  • func(s * Scanner)Next()rune–获取下一个字符

  • func(s * Scanner)Peek()rune–检查下一个字符

  • func(s * Scanner)Pos()(Pos Position)-输出位置信息

  • func(s * Scanner)Scan()rune–获取下一个令牌

  • func(s * Scanner)token text()string–获取刚刚扫描的令牌的文本

tabwriter包提供了这些类型和功能。

Writer 是一个具有列对齐功能的io.Writer:

  • func NewWriter(输出 io。Writer,minwidth,tabwidth,padding int,padchar byte,flags uint)* Writer–创建一个编写器

  • func (b *Writer) Flush()错误

  • func (b Writer) Init(输出 io。Writer,minwidth,tabwidth,padding int,padchar byte,flags uint) Writer–重置一个 Writer

  • func (b *Writer)写(buf []byte) (n int,err error)

数据收集

与 Java 不同,Go 不太依赖一组标准的集合(列表、地图、集合等)。)类型和关联的实现。slicemap类型满足了大部分需求。但是 Go 包含了一些特殊用途的容器库。有关更多详细信息,请参见在线软件包文档:

  • heap为任何堆接口实现者提供操作;仅接口。

  • list提供了一个双向链表。

  • ring提供循环列表。

包堆提供了一些实现类型必须提供的类型和函数:

  • func Fix(h Interface,I int)–元素值更改后修复

  • 功能初始化(h 接口)–初始化

  • func Pop(h Interface)Interface { }–获取最低值

  • func Push(h 接口,x 接口{ })–添加新值

  • func Remove(h Interface,I int)Interface { }–删除第 I 个值

list提供了这些类型和功能。

元素是列表成员:

  • func(e * Element)Next()* Element–获取任何前任

  • func(e * Element)Prev()* Element–获取任何后续元素

List包含元素。这些方法不言自明:

  • func New()* List–制作列表

  • func(l * List)Back()*元素–向后移动

  • func(l * List)Front()*元素–获取第一个

  • func(l * List)Init()* List –{ Re }初始化(清空)列表

  • func(l * List)InsertAfter(v interface { },mark * Element)*元素

  • func(l * List)insert before(v interface { },mark * Element)*元素

  • func(l * List)Len()int–获取长度

  • func (l *List) MoveAfter(e,mark *Element)

  • func (l *List) MoveBefore(e,mark *Element)

  • func(l * List)move to back(e * Element)

  • func(l * List)move to front(e * Element)

  • func (l List)推回(v interface { })元素

  • func (l List)推回列表(其他List)

  • func(l * List)push front(v interface { })*元素

  • func (l List) PushFrontList(其他List)

  • func (l *List) Remove(e *Element)接口{}

例如,以相反的顺序输出列表中的所有元素:

var l = list.New()
for _, x := range []int{1,2,3,4,5} {
      l.PushFront(x)
}
for v := l.Front(); v != nil; v = v.Next() {
      fmt.Print(v.Value)
}
fmt.Println()

从而产生54321

ringRing类型提供了这些功能。每个环元素都有一个Value字段。没有什么元素是特殊的:

  • func New(n int)* Ring–创建一个包含 n 个元素的环

  • func(r * Ring)Do(f func(interface { })–对每个元素运行一个函数

  • func(r * Ring)Len()int–获取长度

  • func(r * Ring)Link(s * Ring)* Ring–将 s 插入 r

  • func(r * Ring)Move(n int)* Ring–前进 n 个元素

  • func(r * Ring)Next()* Ring–前进一个元素

  • func(r * Ring)Prev()* Ring–后退一个元素

  • func(r * Ring)Unlink(n int)* Ring–将接下来的 n 个元素组成一个环

下面是一个打印整数环的示例:

 N := 5
ring := ring.New(N)  // some capacity
count := ring.Len()
// set each element to square root the element index cubed
for i := 0; i < count; i++ {
      ring.Value = math.Sqrt(float64(i * i * i))
      ring = ring.Next()
}
// now output the values; now back at start
x := 0
ring.Do(func(v interface{}) {
      fmt.Printf("Root of cube %v = %v\n", x, v)
      x++
})

这产生了

Root of cube 0 = 0
Root of cube 1 = 1
Root of cube 2 = 2.8284271247461903
Root of cube 3 = 5.196152422706632
Root of cube 4 = 8

整理

sort包提供了对可比较类型的片和支持特定接口的用户定义集合进行排序的方法。

sort包提供了这些类型和功能:

  • func float 64s(a[]float 64)-Sort[]float 64

  • func float 64 sorted(a[]float64)bool–测试[]float 64 是否已经排序

  • func ints(a[]int)-输出[]int

  • func int sorted(a[]int)bool–测试[]int 是否已经排序

  • func IsSorted(数据接口)bool–测试是否已经排序

  • func Search(n int,f func(int)bool)int–二分搜索法 0…N-1 查找 f 为真的值

  • func SearchFloat64s(a []float64,x float 64)int–二分搜索法为 x 排序的[]float64

  • func SearchInts(a []int,x int)int–二分搜索法为 x 排序的[]int

  • func SearchStrings(a []string,x string)int–x 的二分搜索法排序[]字符串

  • func Slice(slice interface{},less func(i,j int)bool)–Sort[]?使用 less 函数

  • func slice sorted(slice interface { },less func(i,j int)bool)bool–测试是否已经排序

  • func slice table(slice interface { },less func(i,j int)bool)–稳定排序[]?使用 less 函数

  • 函数排序(数据接口)-排序数据

  • 函数稳定(数据接口)–稳定排序数据

  • 函数字符串(一个[]字符串)–排序[]字符串

  • func strings aresorted(a[]string)bool–测试是否已经排序

  • func 反转(数据接口)接口–反转数据

Type Interface (一个接口,不是最好的名字选择)定义了用于排序的方法。类型需要实现此接口才能排序。这就像 Java 的Comparable接口:

  • len()int–支持集合的长度

  • Less(i,j int)bool–比较 I 和 j 处的元素

  • Swap(i,j int)–交换 I 和 j 处的元素

Float64Slice 充当实现Interface接口的[]float64:

  • func (p Float64Slice)搜索(x float64) int

  • func (p Float64Slice)排序()

IntSlice 充当实现Interface接口的[]int:

  • func (p IntSlice)搜索(x int) int

  • func (p IntSlice)排序()

StringSlice 充当实现Interface接口的[]字符串:

  • func (p StringSlice)搜索(x string) int

  • func (p 字符串片)排序)

例如,要对一段字符串进行排序:

in :=[...]string{"32", "-1", "0", "a"}
out :=[...]string{"32", "-1", "0", "a"}
var xout = sort.StringSlice(out[:])
xout.Sort()
fmt.Printf("in:  %v\nout: %v\n", in, xout)

这产生了

in:  [32 -1 0 a]
out: [-1 0 32 a]

另一个简单的特例是对一段字符串进行排序:

var sortable = []string{"32", "-1", "0", "a"}
sort.Strings(sortable)
fmt.Printf("out: %v\n", sortable)

这产生了

out: [-1 0 32 a]

上下文包

Go 对异步行为的支持与 Java 不同。Java 没有标准的方法来观察或取消这样的动作;每个库都有不同的做法。同样,在 Java 中,类似的支持是由库和/或框架提供的,比如 Spring 框架,标准库中不包括它。

在 Go 中,context包支持这样做,其中上下文类型携带超时、中断和跨本地和可能远程 API 边界和可能进程的作用域值。许多长期运行的 Go APIs(通常期望在 goroutine 中运行)接受一个上下文参数,允许它们被异步取消或在完成时通过通道通知调用者。

context包具有以下类型和功能:

  • func WithCancel(父上下文)(ctx 上下文,cancel cancel func)-添加一个取消函数

  • func WithDeadline(父上下文,d 时间。Time) (Context,cancel func)-添加截止日期

  • func WithTimeout(父上下文,超时时间。Duration)(上下文,cancel func)-添加超时

在哪里

  • Cancel 函数——调用此函数(由客户端)取消操作或(由自己)在操作完成时释放资源;应该被称为“CancelOrDoneFunc ”,因为这更好地反映了它的作用。

  • 截止时间–设置未来中止未完成操作的时间。

  • 超时–设置延迟,在此之后中止未完成的操作(替代截止时间)。

上下文保存上下文状态。它提供以下功能:

  • func Background()Context–返回用作 With…操作的第一个输入的基本上下文;最常用的。

  • func TODO()Context–返回一个 TODO(需要更多工作)上下文;用法很少。

  • func WithValue(父上下文,键,val 接口{})上下文–向上下文添加值。

上下文具有以下特殊功能:

  • done()–返回一个通道,该通道将在操作完成时接收消息

  • err()–返回操作中发生的任何错误;通常为零

清单 14-3 演示了一个简单的使用上下文来取消一个无限值生成器。

func generateIntValues(ctx context.Context, values chan<- int) {
      loop: for {
            v, err := genIntValue()
            if err != nil {
                  fmt.Printf("genIntValue error: %v\n", err)
                  close(values)
                  break
            }
            select {
            case values <- v: // output value
                  fmt.Printf("generateIntValues sent: %v\n", v)
            case <-ctx.Done():
                  break loop // done when something received
            }
      }
}
func genIntValue() (v int, err error) {
      test := rand.Intn(20) % 5
      if test == 0 {
            err = errors.New(fmt.Sprintf("fake some error"))
            return
      }
      v = rand.Intn(100)
      fmt.Printf("genIntValue next: %d\n", v)
      return
}

Listing 14-3Random Int Generator (Part 1)

通过清单 14-4 中所示的代码运行。

values := make(chan int, 10)
ctx, cf := context.WithTimeout(context.Background(), 5 * time.Second)
go generateIntValues(ctx, values)
for v := range values {  // get all generated
      fmt.Printf("generateIntValues received: %d\n", v)
}
cf()
fmt.Printf("generateIntValues done\n")

Listing 14-4Random Int Generator (Part 2)

它会产生这样的结果:

genIntValue next: 87
generateIntValues sent: 87
genIntValue next: 59
generateIntValues sent: 59
genIntValue next: 18
generateIntValues sent: 18
genIntValue error: fake some error

generateIntValues received: 87
generateIntValues received: 59
generateIntValues received: 18
generateIntValues done

注意超时确保生成在某个时候结束;在这个例子中没有出现这种情况。注意,因为通道有许多(比如 100 个)槽,所以生成都发生在处理一个值之前。如果通道容量设置为零,处理顺序会变得更加混杂:

genIntValue next: 87
generateIntValues sent: 87
genIntValue next: 59
generateIntValues received: 87
generateIntValues received: 59
generateIntValues sent: 59
genIntValue next: 18
generateIntValues sent: 18
generateIntValues received: 18
genIntValue error: fake some error
generateIntValues done

密码术、散列法和数据编码

Go 内置了重要的加密和散列函数。这包括对多种算法和功能的支持。每个都有自己的包装。本书不会详细介绍这些子包。请参见在线软件包文档。

  • adler32提供 Adler-32 校验和。

  • aes提供 AES 加密。

  • cipher提供标准的分组密码模式,包装较低级别的密码实现。

  • crc32提供 32 位循环冗余校验校验和。

  • crc64提供 64 位循环冗余校验校验和。

  • crypto提供加密常数。

  • des提供数据加密标准和三重数据加密算法。

  • dsa提供数字签名算法。

  • ecdsa提供椭圆曲线数字签名算法。

  • ed25519提供 Ed25519 签名算法。

  • elliptic提供了素数域上的几条标准椭圆曲线。

  • fnv提供 FNV 哈希函数。

  • hash为哈希函数提供接口。

  • hmac提供键控散列消息认证码。

  • maphash提供字节序列的哈希函数。

  • md5提供 MD5 哈希算法。

  • pkix提供 ASN.1 解析。

  • rand提供密码安全的随机数生成器。

  • rc4提供 RC4 加密。

  • rsa提供 RSA 加密。

  • sha1提供 SHA 哈希算法。

  • sha256提供了几种 SHA 哈希算法。

  • sha512提供了几种 SHA 哈希算法。

  • subtle为密码代码提供帮助函数。

  • tls提供 TLS 1.2 和 TLS 1.3。

  • x509提供 X.509 编码的密钥和证书。

编码包

encoding包提供了定义如何在字节级和类似字符串的表示之间转换数据的接口。有几个不同支持的子包。本书将只详细介绍其中的几个子包:

  • ascii85提供 ascii85 数据编码。

  • asn1提供 ASN.1 数据结构的解析。

  • base32提供 base32 编码。

  • base64提供 base64 编码。

  • binary提供数字和字节序列之间的转换。

  • csv读写逗号分隔值(CSV)文件。

  • 管理 gob 流,一种二进制交换的形式。

  • hex提供十六进制编码和解码。

  • pem提供 PEM 数据编码。

  • json提供 JSON 编码和解码。

  • xml提供支持 XML 名称空间的 XML 解析器。

csv包有这些类型和功能。

读取器解析 CSV 输入:

  • func NewReader(r io。读者)*读者–成为读者

  • func(r * Reader)Read()(record[]string,err error)-读取一行

  • func(r * Reader)Read all()(records[][]string,err error)-读取所有行

Writer 生成 CSV 输出:

  • func NewWriter(w io。作家)*作家–成为作家

  • func(w * Writer)Flush()–提交写入的内容

  • func(w * Writer)Write(record[]string)错误–写入一行

  • func(w * Writer)Write all(records[][]string)错误–写入多行

hex包具有以下功能:

  • func NewDecoder(r io。读者)io。读者——成为读者

  • func NewEncoder(w io。作家)木卫一。作家——成为作家

  • func Decode(dst,src []byte) (int,error)-src 十六进制字符串到 dst 字节

  • 函数解码(s 字符串)([]字节,错误)–src 十六进制字符串到 dst 字节

  • func decodelen(x int)int–总是 x / 2

  • func Dump(data []byte)字符串–格式化十六进制转储

  • 功能转储器(带 io。作家)木卫一。write closer–格式化十六进制转储

  • func Encode(dst,src[]byte)int–src 字节到 dst 十六进制字符串

  • func EncodeToString(src []byte)字符串–src 字节到十六进制字符串

  • func 编码器(n int)-始终 n * 2

json包具有以下类型和功能:

  • func Compact(dst *字节。Buffer,src []byte)错误–删除无关紧要的空白

  • func html scape(dst *字节。Buffer,src[]byte)–使嵌入 HTML 变得安全

  • func 缩进(dst *字节。Buffer,src []byte,prefix,indent string)错误–缩进 JSON

  • func Marshal(v 接口{ })([]字节,错误)-基于传递的类型生成 JSON

  • func MarshalIndent(v interface{},prefix,indent string) ([]byte,error)-根据传递的类型生成缩进的 JSON

  • func 解组(data []byte,v interface{})错误–将 JSON 解析为传递的类型

  • func Valid(data[]byte)bool–测试 JSON 字符串的有效性

解码器解码/解析 JSON 字符串。它提供以下功能:

  • func NewDecoder(r io。阅读器)*解码器–制作解码器

  • func(dec * Decoder)Decode(v interface { })error–解码下一个 JSON 值

  • func(dec * Decoder)DisallowUnknownFields()–导致未知键出错

  • func(dec * Decoder)input offset()int 64–输入文本中的位置

  • func(dec * Decoder)More()bool–测试是否有更多数据要解析

  • func (dec *Decoder) Token() (Token,error)-获取下一个令牌

编码器编码/构建 JSON 字符串。它提供以下功能:

  • func NewEncoder(w io。writer)* Encoder–制作编码器

  • func(enc * Encoder)Encode(v interface { })错误–将值格式化为 JSON

  • func (enc *Encoder) SetEscapeHTML(在 bool 上)–转义 HTML 控制字符

  • func (enc *Encoder) SetIndent(前缀,缩进字符串)-设置缩进空格

有关使用 JSON 和 XML 编码器的示例,请参见 capstone 程序。

Unicode 编码包

unicode包提供了检查和操作流行编码中的 Unicode 字符(即,符文)的功能。这个包有常量和变量,它们定义了主要的 Unicode 字符类别,比如LetterDigitPunctSpace等等。

它具有以下功能。许多人通过 Unicode 分类来测试符文的类型。这就像 Java 的Character.isXxx(...)方法。

  • 函数在(r rune,ranges...* range table)bool–测试符文的成员资格

  • func Is(rangeTab *RangeTable,r rune)bool–测试成员的 Rune

  • func isco control(r rune)bool

  • func IsDigit(r rune) bool

  • func IsGraphic(r rune) bool

  • func IsLetter(s rune) bool

  • func ispower(r rune)bool

  • func ismak(r rune)bool

  • func IsNumber(r rune) bool

  • func is of(ranges[]* range table,r rune) bool

  • func IsPrint(r rune) bool

  • func IsPunct(r rune) bool

  • func IsSpace(r rune) bool

  • func 此符号(r rune) bool

  • func isttle(r rune)bool

  • func isper(r rune)bool

  • func SimpleFold(r rune) rune

  • Func ToLower(s rune) 符文

  • func ToTitle(r rune) rune

  • func ToUpper(r rune)符文

unicode包有子包:

  • 为 UTF-8 提供编码和解码符文。

  • utf16为 UTF-16 提供编码和解码符文。

utf8包提供了这些功能:

  • func DecodeLastRune(p[]byte)(r rune,size int)-获取最后一个符文和长度

  • func DecodeLastRuneInString(s string)(r rune,size int)-获取最后一个符文和长度

  • func DecodeRune(p []byte) (r rune,size int)-获取第一个符文和长度

  • 获得第一个符文和长度

  • func EncodeRune(p []byte,r rune)int–在 UTF-8 中制作一个符文

  • func full rune(p[]byte)bool–测试 p 是否以有效的 UTF-8 符文开始

  • 测试 s 是否以有效的 UTF-8 符文开始

  • func rune Count(p[]byte)int–计算 p 中的字符数

  • func rune Count instring(s string)(n int)-以 s 为单位计算符文数

  • func rune len(r rune)int–rune UTF-8 中有多少字节

  • func RuneStart(b 字节)bool–b 是有效的 RuneStart 吗

  • func Valid(p[]byte)bool–测试 p 是一个值 rune 序列

  • 一个符文可以用 UTF8 表示吗

  • func valid string(s string)bool–Test s 是一个有效的符文序列

utf16包提供了这些功能:

  • func Decode(s[]uint 16)[]符文–转换为符文

  • 符文——将一对符文转换成一个符文

  • func Encode(s[]rune)[]uint 16–从 rune 转换而来

  • func EncodeRune(r rune) (r1,R2 rune)-转换成一对

  • 测试一个符文是否需要一对

十五、SQL 数据库访问

Java 在java.sqljava.sqlx包中提供了多级基于 SQL 的数据库访问,称为 JDBC (也称为 Java 数据库连接)。Go 与sqldriver软件包做了几乎相同的事情,但是功能更少。sql包提供了一个通用的功能框架,就像java.sql为 SQL 数据库提供的一样,而驱动程序包是一个系统编程接口 (SPI),允许使用可插拔的驱动程序。大多数 SQL 操作都是由驱动程序完成的。与 Java 不同,大多数 Go 驱动程序来自社区资源,而不是数据库供应商。

Java 有社区支持(例如,Hibernate1Java 持久性架构 2 (JPA)实现)用于对象关系映射器 (ORMs) 3 使保存持久对象(或实体;Java 中的类,Go 中的结构)要容易得多。许多 Java 开发人员使用这些 ORM,而不是 JDBC 提供的更基本的 CRUD4SQL5access。

以下软件包提供类似 JDBC 的低级访问。有一些社区包,比如 GORM,增加了对 Go 的 ORM 支持。

sql包很大,有几种类型和功能。

主要类型有

  • ColType–定义表格列的类型

  • conn–表示单个数据库连接

  • DB——代表一个连接池

  • row–表示单个返回的表格行(行的子情况)

  • rows–表示多个返回的表格行

  • 扫描仪–访问行中各列的界面

  • stmt–表示一条准备好的 SQL 语句

  • tx–表示一个数据库事务

sql包具有以下功能:

  • func Drivers()[]string–获取注册的驱动程序名称

  • func 寄存器(名称字符串,驱动程序驱动程序。驱动程序)–注册驱动程序

  • ColumnType 提供了这些函数。它们是不言自明的:

  • func(ci * column type)database typename()string

  • func(ci * ColumnType)decimal size()(precision,scale int64,ok bool)

  • func(ci * ColumnType)Length()(Length int 64,ok bool)

  • func(ci * column type)name()string

  • func(ci * column type)null()(null,ok bool)

  • func(ci * ColumnType)ScanType()reflect。类型

Conn 提供连接级访问功能:

  • func (c *Conn) BeginTx(ctx 上下文。Context,opts TxOptions) (Tx,error)-启动事务

  • func (c *Conn) Close()错误–关闭连接;把它放回水池

  • func(c * Conn)exec context(CTX context。上下文,查询字符串,参数...接口{})(结果,错误)-执行非查询 SQL

  • func(c * Conn)ping context(CTX context。上下文)错误–查看连接是否可以工作

  • func(c * Conn)prepare context(CTX context。上下文,查询字符串)(*Stmt,error)-准备一条 SQL 语句

  • func (c Conn) QueryContext(ctx 上下文。上下文,查询字符串,参数...interface{}) (Rows,error)–执行可以返回许多行的 SQL 查询

  • func(c * Conn)QueryRowContext(CTX context。上下文,查询字符串,参数...interface { })* Row–执行将返回< = 1 行的 SQL 查询

DB 提供数据库级访问功能:

  • func Open(driverName,dataSourceName string) (*DB,error)–按名称打开数据库

  • c 驱动程序。连接器)*数据库–打开数据库

  • func (db DB) Begin() (Tx,error)-启动一个事务

  • func (db *DB) BeginTx(ctx 上下文。Context,opts TxOptions) (Tx,error)–用选项启动事务

  • func (db *DB) Close()错误–关闭数据库

  • func (db DB) Conn(ctx 上下文。上下文)(连接,错误)–获取到数据库的连接

  • func (db *DB) Driver()驱动程序。驱动程序–为数据库获取一个驱动程序

  • func (db *DB) Exec(查询字符串,参数...接口{})(结果,错误)–执行通用 SQL

  • func(DB * DB)exec context(CTX context。上下文,查询字符串,参数...接口{})(结果,错误)–执行通用 SQL

  • func (db *DB) Ping()错误–测试数据库可用

  • func(DB * DB)ping context(CTX context。上下文)错误–测试数据库可用

  • func (db DB)准备(查询字符串)(Stmt,error)-准备 SQL

  • func (db DB) PrepareContext(ctx 上下文。上下文,查询字符串)(Stmt,error)-准备 SQL

  • func (db DB)查询(查询字符串,参数...接口{ })(行,错误)–执行一般选择

  • func (db DB) QueryContext(ctx 上下文。上下文,查询字符串,参数...接口{ })(行,错误)–执行一般选择

  • func (db DB) QueryRow(查询字符串,参数...interface { }) Row–执行查询

  • func(DB * DB)query rowcontext(CTX context。上下文,查询字符串,参数...interface { })* Row–执行查询

  • func(DB * DB)Stats()DBStats–获取各种数据库访问统计信息

行是单一选择结果行:

  • func(r * Row)Err()error–获取任何执行错误

  • 功能(右*行)扫描(目的地...接口{})错误–将返回的数据复制到变量中

Rows 是一组选择结果行:

  • func (rs *Rows) Close()错误–表示现在已完成对行的处理

  • func(RS * Rows)column types()([]* ColumnType,error)-获取列元数据

  • func(RS * Rows)Columns()([]string,error)-获取列名

  • func(RS * Rows)Err()error–获取任何执行错误

  • func(RS * Rows)Next()bool–前进到下一行

  • func(RS * Rows)next resultset()bool–前进到下一个结果集

  • 函数(rs *行)扫描(目标...接口{})错误–将返回的数据复制到变量中

Stmt 提供 SQL 语句级访问函数:

  • func (s *Stmt)关闭()错误

  • func (s *Stmt) Exec(args...接口{})(结果,错误)

  • func(s * Stmt)exec context(CTX context。上下文,参数...接口{})(结果,错误)

  • func (s Stmt)查询(参数...接口{ })(行,错误)

  • func (s Stmt) QueryContext(ctx 上下文。上下文,参数...接口{ })(行,错误)

  • func (s Stmt) QueryRow(args...接口{ })

  • func(s * Stmt)QueryRowContext(CTX 上下文。上下文,参数...接口{ })*行

Tx 提供事务级访问功能。见前面类似的描述。

  • func (tx *Tx) Commit()错误–提交任何更改

  • func (tx *Tx) Exec(查询字符串,参数...接口{})(结果,错误)

  • func(Tx * Tx)exec context(CTX context。上下文,查询字符串,参数...接口{})(结果,错误)

  • func (tx Tx)准备(查询字符串)(Stmt,错误)

  • func (tx Tx) PrepareContext(ctx 上下文。上下文,查询字符串)(Stmt,错误)

  • func (tx Tx)查询(查询字符串,参数...接口{ })(行,错误)

  • func (tx Tx) QueryContext(ctx 上下文。上下文,查询字符串,参数...接口{ })(行,错误)

  • func (tx Tx) QueryRow(查询字符串,参数...接口{ })

  • func (tx Tx) QueryRowContext(ctx 上下文。上下文,查询字符串,参数...接口{ })

  • func (tx *Tx) Rollback()错误–回滚(取消)任何更改

  • func(Tx * Tx)Stmt(Stmt * Stmt)* Stmt–获取此事务中的一条语句

  • func (tx *Tx) StmtContext(ctx 上下文。上下文,stmt *Stmt) *Stmt

作为使用sql包的一个例子,清单 15-1 展示了一个对简单表格进行 CRUDs 的程序。

// Table row entity
type DBEntity struct {
     name  string
     value string
}

// Do in a DB context.
func DoInDB(driverName, datasourceParams string, f func(db *sql.DB) error) (err error) {
     db, err := sql.Open(driverName, datasourceParams)
     if err != nil {
          return
     }
     defer db.Close()
     err = f(db)
     return
}

// Do in a connection.
func DoInConn(db *sql.DB, ctx context.Context, f func(db *sql.DB, conn *sql.Conn, ctx context.Context) error) (err error) {
     conn, err := db.Conn(ctx)
     if err != nil {
          return
     }
     defer conn.Close()
     err = f(db, conn, ctx)
     return
}

// Do in a transaction.
func DoInTx(db *sql.DB, conn *sql.Conn, ctx context.Context, txOptions *sql.TxOptions, f func(tx *sql.Tx) error) (err error) {
     if txOptions == nil {
          txOptions = &sql.TxOptions{Isolation: sql.LevelSerializable}
     }
     tx, err := db.BeginTx(ctx, txOptions)
     if err != nil {
          return
     }

     err = f(tx)
     if err != nil {
          _ = tx.Rollback()
          return
     }
     err = tx.Commit()
     if err != nil {
          return
     }
     return
}

var ErrBadOperation = errors.New("bad operation")

// Execute a SQL statement.
func ExecuteSQL(tx *sql.Tx, ctx context.Context, sql string, params ...interface{}) (count int64, values []*DBEntity, err error) {
     lsql := strings.ToLower(sql)
     switch {

     // process query
     case strings.HasPrefix(lsql, "select "):
          rows, xerr := tx.QueryContext(ctx, sql, params...)
          if xerr != nil {
               err = xerr
               return
          }
          defer rows.Close()
          for rows.Next() {
               var name string
               var value string
               if err = rows.Scan(&name, &value); err != nil {
                    return
               }
               data := &DBEntity{name, value}
               values = append(values, data)
          }
          if xerr := rows.Err(); xerr != nil {
               err = xerr
               return
          }

     // process an update
     case strings.HasPrefix(lsql, "update "), strings.HasPrefix(lsql, "delete "), strings.HasPrefix(lsql, "insert "):
          result, xerr := tx.ExecContext(ctx, sql, params...)
          if xerr != nil {
               err = xerr
               return
          }
          count, xerr = result.RowsAffected()
          if xerr != nil {
               err = xerr
               return
          }

     default:
          err = ErrBadOperation  // INSERT and DELETE not demo’ed here
          return
     }
     return
}

Listing 15-1Sample DB Access (Part 1)

该库由该测试函数驱动(清单 15-2 )。

func testDB() {
     values := make([]*DBEntity, 0, 10)
     values = append(values, &DBEntity{"Barry", "author"},
          &DBEntity{"Barry, Jr.", "reviewer"})

     err := DoInDB("postgres", "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
          func(db *sql.DB) (err error) {
               err = DoInConn(db, context.Background(), func(db *sql.DB, conn *sql.Conn,
                       ctx context.Context) (err error) {
                    err = createRows(db, conn, ctx, values)
                    if err != nil {
                         return
                    }

                    // must be done in separate transaction to see the change
                    err = queryRows(db, conn, ctx)
                    return
               })
               return
          })
     if err != nil {
          fmt.Printf("DB access failed: %v\n", err)
     }
}

// Create data rows.
func createRows(db *sql.DB, conn *sql.Conn, ctx context.Context, values []*DBEntity) (err error) {
     err = DoInTx(db, conn, ctx, nil, func(tx *sql.Tx) (err error) {
          // first remove any old rows
          count, _, err := ExecuteSQL(tx, ctx, `delete from xvalues`)
          if err != nil {
               return
          }
          fmt.Printf("deleted %d\n", count)
          // insert new rows
          for _, v := range values {
               count1, _, xerr := ExecuteSQL(tx, ctx, fmt.Sprintf(`insert into xvalues(name, value) values('%s', '%s')`, v.name, v.value))
               if xerr != nil || count1 != 1 {
                    err = xerr
                    return
               }
               fmt.Printf("inserted %q = %q\n", v.name, v.value)
          }
          // update a row
          v := &DBEntity{"Barry", "father"}
          _, _, xerr := ExecuteSQL(tx, ctx, fmt.Sprintf(`update xvalues set value='%s' where name='%s'`, v.value, v.name))
          if xerr != nil {
               err = xerr
               return
          }

          fmt.Printf("updated %q = %q\n", v.name, v.value)
          return
     })
     return
}

// Query and print all rows.
func queryRows(db *sql.DB, conn *sql.Conn, ctx context.Context) (err error) {
     err = DoInTx(db, conn, ctx, nil, func(tx *sqB.Tx) (err error) {
          _, xvalues, err := ExecuteSQL(tx, ctx, `select name, value from xvalues`)
          if err != nil {
               return
          }
          for _, v := range xvalues {
               fmt.Printf("queried %q = %q\n", v.name, v.value)
          }
          return
     })
     return
}

Listing 15-2Sample DB Access (Part 2)

注意确保数据库资源关闭的嵌套方法。释放资源是很重要的,特别是对于长时间运行的程序,比如服务器。顶层是对数据库连接池的访问。然后从池中访问单个连接。最后,有一个要在其中执行 SQL 语句的事务。在这个例子中,多个事务在一个连接中完成,这是很典型的。此外,多条语句通常在一个事务中完成。

该程序输出以下内容:

deleted 2
inserted "Barry" = "author"
inserted "Barry, Jr." = "reviewer"
updated "Barry" = "father"queried "Barry, Jr." = "reviewer"
queried "Barry" = "father"

注意删除了两条记录,因为该输出来自程序的第二次执行。

当程序结束时,数据库中的数据如图 15-1 所示。

img/516433_1_En_15_Fig1_HTML.jpg

图 15-1

数据库示例结果(在 PostgreSQL 中)

十六、客户端和服务器支持

html包提供了处理 HTML 文本的函数。Java 标准版(JSE)几乎没有类似的支持;Java 社区提供了大部分这种功能。Go template包提供了生成带有值插入的文本输出的模板。它支持纯文本和防黑客 HTML 文本的扩展。

MIME 包

mime包提供了对 MIME 类型的支持。它包含子包:

  • Multipart提供 MIME 多部分解析。

  • Quotedprintable通过阅读器和编写器提供可打印的引用编码。

网络包

net包提供了使用 TCP/IP 和套接字级访问和 UDP 的接口。它有几个子包,但本书只介绍了http子包。http包提供了创建 HTTP 客户端和服务器的能力。它有几个常用的子程序包:

  • Cgi为每个请求服务器的通用网关接口(CGI)进程提供支持。

  • Fcgi提供对“快速”通用网关接口(CGI)服务器的支持。

  • Cookiejar提供对 HTTP cookies 的支持。

  • Httputil提供 HTTP helper 实用程序功能。

  • Textproto为带有文本头和文本段的协议(如 HTTP 和 SMTP)提供帮助。

网络包

net包裹很大。它为使用套接字和数据报访问 TCP/IP 网络以及选择协议(如 HTTP)提供基本服务。net包有多种类型和功能。它们不会在本书中列出,但可以在网上找到。下面的例子展示了如何使用 API 的一个子集(DialListenAcceptReadWrite)。

在我们进入 go 的 HTTP 包之前,我们应该简单讨论一下 TCP/IP 和 HTTP 与 REST 和 RPC。

终端控制协议(TCP)1结合互联网协议(IP)2是互联网的首要基础。它们一起允许在网络上的主机之间进行低级和不可靠的数据报传输或可靠的套接字/会话交换。

超文本传输协议 (HTTP ) 3 是一种流行的协议,通过 TCP 套接字传递,与超文本标记语言 (HTML ) 4 (和级联样式表 (CSS ) 5 和 JavaScript 及其他 MIME 类型)相结合,创建了万维网 (WWW

HTTP 允许多种格式的数据在服务器和客户机(通常是浏览器)之间交换。它支持许多动词,但主要是允许 GET(读取)、PUT(创建或替换)、POST(创建或附加)和 DELETE,也就是 CRUD, 7 资源。

具象状态转移 (REST,有时 ReST) 8 构建于 HTTP 之上,但对其进行了限制,以提高易用性和可伸缩性。REST 不是一个实现,而是一套设计指南。它反映了 WWW 支持的最可取的品质。它将服务器操作仅限于应用于由 URL 标识的资源的 CRUD。休息做得最大限度地实现了 HATEOAS, 9 这体现了 WWW 组织。大多数 RESTful APIs 都达不到这个水平。RESTful 服务是基于 RPC 的服务的替代品。Go 对这样的服务有很多支持。

远程过程调用 (RPC) 10 是使用 HTTP(或其他协议)的另一种方式,与 REST 相比,它限制更少(通常性能更好),允许用户创建服务器提供给其客户端(几乎总是程序,而不是人)的任意操作(过程)。

Web 服务(WS)11RPC 的一种形式,通常通过 SOAP12 一种基于 XML 的协议实现,使用 HTTP。相对于 RESTful 服务,这种服务已经失宠了。Go 对基于 SOAP 的 WS 几乎没有标准库支持。

Go 在net/rpc/jsonrpc包中有有限的标准 RPC 支持。一个更强大更受欢迎的社区选项是谷歌的 gRPC 13

net包的 Go 的httprpc子包允许一个人开发这些级别的程序。这本书主要讨论 REST 风格的访问。

这一切都是建立在基于底层 TCP/IP14Socket15–的通信上。Go 在net包中也支持在这一层工作。

下面包括一个简单的 TCP 套接字客户机/服务器对。他们演示了通过套接字进行的基本通信。请注意,与进行 HTTP 通信(如本书中的其他示例所示)相比,使用套接字方法会更加复杂(需要更多代码)。下面的代码效率很低,因为每个连接只允许一个请求(这类似于 HTTP 版本 1)。它也是低效的,因为它一次读取/写入一个字节的数据。

作为演示,我们将运行一个启动服务器并发出多个请求的配置(所有请求通常在不同的机器上启动,但在这里通过多个 goroutines 进行模拟)。这个演示开始在后台以随机的时间间隔发送 10 个请求,其中有一些是重叠的,然后快速地(在任何请求丢失之前)启动一个服务器来处理它们。该演示被硬编码为对服务器使用localhost:8080。注意服务器处理并发请求(nest值有时大于 1;在示例输出中观察到三个)。

服务器分两部分工作:

  1. 接受连接的长时间运行的循环。

  2. 在接受的连接上处理请求的 goroutine 可以有任意数量的这样的处理器同时运行。

前面的模式是服务器请求处理的典型模式。在其他语言中,比如 Java,一个 OS 线程(通常来自一个有限的线程池)通常用于每个请求;在 Go 中,用 goroutine 代替。这可以使用 Go 实现更好的请求规模。

如清单 16-1 所示,客户端发送命令,服务器发送响应。命令是带有消息结束标记的字符串(“~”不能是消息文本的一部分)。

关于getGID()的定义,参见log封装部分。

var xlog = log.New(os.Stderr, "", log.Ltime+log.Lmicroseconds)

func main(){
     var wg sync.WaitGroup
     wg.Add(1)
     go SocketClientGo(&wg)
     ss := NewSocketServer()
     go func() {
          gid := getGID()
          err := ss.AcceptConnections(8080)
          if err != nil {
              xlog.Printf("%5d testSocketServer accept failed: %v\n", gid, err)
               return
          }
     }()
     wg.Wait()
     ss.Accepting = false
}

func SocketClientGo(wg *sync.WaitGroup) {
     defer wg.Done()
     gid := getGID()
     cmds := []string{TODCommand, SayingCommand}
     max := 10

     var xwg sync.WaitGroup
     for i := 0; i < max; i++ {
          xwg.Add(1)
          go func(index, max int) {
               defer xwg.Done()
               time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
               sc := newSocketClient("127.0.0.1", 8080)
               xlog.Printf("%5d SocketClientGo request %d of %d\n", gid, index, max)
               resp, err := sc.GetCmd(cmds[rand.Intn(len(cmds))])
               if err != nil {
                    xlog.Printf("%5d SocketClientGo failed: %v\n", gid, err)
                    return
               }
               xlog.Printf("%5d SocketClientGo response: %s\n", gid, resp)
          }(i+1, max)
     }
     xwg.Wait()
}

// allowed commands
const (
     TODCommand    = "TOD"
     SayingCommand = "Saying"
)

var delim = byte('~')

// some saying to return
var sayings = make([]string, 0, 100)

func init(){
    sayings = append(sayings,
     `Now is the time...`,
     `I'm busy.`,
     `I pity the fool that tries to stop me!`,
     `Out wit; Out play; Out last!`,
     `It's beginning to look like TBD!`,
     )
}

// a Server
type SocketServer struct {
     Accepting bool
}

func NewSocketServer() (ss *SocketServer) {
     ss = &SocketServer{}
     ss.Accepting = true
     return
}

// Accept connection until told to stop.
func (ss *SocketServer) AcceptConnections(port int) (err error) {
     gid := getGID()
     xlog.Printf("%5d accept listening on port: %d\n", gid, port)
     listen, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
     if err != nil {
          return
     }
     for ss.Accepting {
          conn, err := listen.Accept()
          if err != nil {
               xlog.Printf("%5d accept failed: %v\n", gid,err)
               continue
          }
          xlog.Printf("%5d accepted connection: %#v\n", gid, conn)
          go ss.handleConnectionGo(conn)
     }
     return
}

var nesting int32

// Process each connection.
// Only one command per connection.
func (ss *SocketServer) handleConnectionGo(c net.Conn) {
     defer c.Close()
     nest := atomic.AddInt32(&nesting, 1)
     defer func(){
          atomic.AddInt32(&nesting, -1)
     }()
     gid := getGID()
     data := make([]byte, 0, 1000)
     err := readData(c, &data, delim, cap(data))
     if err != nil {
          xlog.Printf("%5d handleConnection failed: %v\n", gid, err)
          return
     }

     cmd := string(data)
     xlog.Printf("%5d handleConnection request: %s, nest: %d, conn: %#v\n", gid, cmd, nest, c)
     if strings.HasSuffix(cmd, string(delim)) {
          cmd = cmd[0 : len(cmd)-1]
     }
     xlog.Printf("%5d received command: %s\n", gid, cmd)
     time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)  // make request take a while
     var out string
     switch cmd {
     case SayingCommand:
          out = sayings[rand.Intn(len(sayings))]
     case TODCommand:
          out = fmt.Sprintf("%s", time.Now())
     default:
          xlog.Printf("%5d handleConnection unknown request: %s\n", gid, cmd)
          out = "bad command: " + cmd
     }
     _, err = writeData(c, []byte(out+string(delim)))
     if err != nil {
          xlog.Printf("%5d %s failed: %v\n", gid, cmd, err)
     }

}

// a Client
type SocketClient struct {
     Address    string
     Port       int
     Connection net.Conn
}

func newSocketClient(address string, port int) (sc *SocketClient) {
     sc = &SocketClient{}
     sc.Address = address
     sc.Port = port
     return
}

func (sc *SocketClient) Connect() (err error) {
     gid := getGID()
     xlog.Printf("%5d attempting connection: %s:%d\n", gid, sc.Address, sc.Port)
     sc.Connection, err = net.Dial("tcp", fmt.Sprintf("%s:%d", sc.Address, sc.Port))
     if err != nil {
          return
     }
     xlog.Printf("%5d made connection: %#v\n", gid, sc.Connection)
     return
}
func (sc *SocketClient) SendCommand(cmd string) (err error) {
     gid := getGID()
     c, err := sc.Connection.Write([]byte(cmd + string(delim)))
     if err != nil {
          return
     }
     xlog.Printf("%5d sent command: %s, count=%d\n", gid, cmd, c)
     return
}
func (sc *SocketClient) ReadResponse(data *[]byte, max int) (err error) {
     err = readData(sc.Connection, data, delim, 1000)
     return
}

// send command and get response.
func (sc *SocketClient) GetCmd(cmd string) (tod string, err error) {
     err = sc.Connect()
     if err != nil {
          return
     }
       defer sc.Connection.Close()
     err = sc.SendCommand(cmd)
     data := make([]byte, 0, 1000)
     err = readData(sc.Connection, &data, delim, cap(data))
     if err != nil {
          return
     }
     tod = string(data)
     return
}

func readData(c net.Conn, data *[]byte, delim byte, max int) (err error) {
     for {
          xb := make([]byte, 1, 1)
          c, xerr := c.Read(xb)
          if xerr != nil {
               err = xerr
               return
          }
          if c > 0 {
               if len(*data) > max {
                    break
               }
               b := xb[0]
               *data = append(*data, b)
               if b == delim {
                    break
               }
          }
     }
     return
}
func writeData(c net.Conn, data []byte) (count int, err error) {
     count, err = c.Write(data)
     return
}

Listing 16-1Sample TCP/IP Socket Usage

这会产生以下输出(每次运行可能会有所不同)。每一行都有一个时间戳和一个 goroutine id。这使你能够相对地分辨出哪个消息来自哪个路由器,以及何时来自哪个路由器。goroutine ids 并不重要,但是它们在某个 goroutine 的每一行中都是相同的。新的 go routine 可以重用已完成的 go routine 以前的 id。请注意,请求是以明显随机的顺序发送的。

定制的记录器用于删除正常输出的日期:

09:32:57.910516    20 accept listening on port: 8080
09:32:57.911512    19 SocketClientGo request 7 of 10
09:32:57.911512    19 SocketClientGo request 9 of 10
09:32:57.911512    19 SocketClientGo request 8 of 10
09:32:57.912507    13 attempting connection: 127.0.0.1:8080
09:32:57.912507    11 attempting connection: 127.0.0.1:8080
09:32:57.912507    12 attempting connection: 127.0.0.1:8080
09:32:57.914499    11 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298000)}}
09:32:57.914499    12 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021a000)}}
09:32:57.914499    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc0000cc000)}}
09:32:57.914499    13 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000198000)}}
09:32:57.914499    12 sent command: TOD, count=4
09:32:57.914499    11 sent command: TOD, count=4
09:32:57.914499    13 sent command: Saying, count=7
09:32:57.914499    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298280)}}
09:32:57.914499    15 handleConnection request: TOD~, nest: 1, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc0000cc000)}}
09:32:57.914499    15 received command: TOD
09:32:57.914499    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000316000)}}
09:32:57.914499    51 handleConnection request: TOD~, nest: 2, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298280)}}
09:32:57.914499    51 received command: TOD
09:32:57.914499    82 handleConnection request: Saying~, nest: 3, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000316000)}}
09:32:57.914499    82 received command: Saying
09:32:58.004647    19 SocketClientGo response: 2020-12-29 09:32:58.0046474 -0800 PST m=+0.097117101~
09:32:58.150718    19 SocketClientGo response: 2020-12-29 09:32:58.150718 -0800 PST m=+0.243187101~
09:32:58.190435    19 SocketClientGo response: I'm busy.~
09:32:58.925744    19 SocketClientGo request 1 of 10
09:32:58.925744    19 SocketClientGo request 2 of 10
09:32:58.925744    19 SocketClientGo request 5 of 10
09:32:58.925744     6 attempting connection: 127.0.0.1:8080
09:32:58.925744     5 attempting connection: 127.0.0.1:8080
09:32:58.925744     9 attempting connection: 127.0.0.1:8080
09:32:58.925744     5 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00014cc80)}}
09:32:58.925744    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298780)}}
09:32:58.925744     6 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000316280)}}
09:32:58.925744     9 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298500)}}
09:32:58.925744     5 sent command: Saying, count=7
09:32:58.925744     6 sent command: Saying, count=7
09:32:58.925744     9 sent command: TOD, count=4
09:32:58.925744    20 accepted connection

: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298a00)}}
09:32:58.925744    53 handleConnection request: TOD~, nest: 1, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298780)}}
09:32:58.925744    53 received command: TOD
09:32:58.925744    54 handleConnection request: Saying~, nest: 2, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298a00)}}
09:32:58.925744    54 received command: Saying
09:32:58.925744    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021a280)}}
09:32:58.925744    35 handleConnection request: Saying~, nest: 3, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021a280)}}
09:32:58.925744    35 received command: Saying
09:32:58.954615    19 SocketClientGo response: Out wit; Out play; Out last!~
09:32:59.393099    19 SocketClientGo response: I pity the fool that tries to stop me!~
09:32:59.420974    19 SocketClientGo response: 2020-12-29 09:32:59.4209749 -0800 PST m=+1.513438801~
09:32:59.921948    19 SocketClientGo request 10 of 10
09:32:59.921948    19 SocketClientGo request 3 of 10
09:32:59.921948    14 attempting connection: 127.0.0.1:8080
09:32:59.921948     7 attempting connection: 127.0.0.1:8080
09:32:59.921948    14 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc0000cc280)}}
09:32:59.921948     7 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000298c80)}}
09:32:59.921948    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021a500)}}
09:32:59.921948    14 sent command: Saying, count=7
09:32:59.921948     7 sent command: Saying, count=7
09:32:59.921948    56 handleConnection request: Saying~, nest: 1, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021a500)}}
09:32:59.921948    56 received command: Saying
09:32:59.921948    20 accepted connection

: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021a780)}}
09:32:59.921948    36 handleConnection request: Saying~, nest: 2, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021a780)}}
09:32:59.921948    36 received command: Saying
09:33:00.219828    19 SocketClientGo response: Now is the time...~
09:33:00.314614    19 SocketClientGo response: I'm busy.~
09:33:00.924919    19 SocketClientGo request 6 of 10
09:33:00.924919    10 attempting connection: 127.0.0.1:8080
09:33:00.924919    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021ac80)}}
09:33:00.924919    10 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021aa00)}}
09:33:00.924919    10 sent command: TOD, count=4
09:33:00.924919    38 handleConnection request: TOD~, nest: 1, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc00021ac80)}}
09:33:00.924919    38 received command: TOD
09:33:01.316527    19 SocketClientGo response: 2020-12-29 09:33:01.3165274 -0800 PST m=+3.408983501~
09:33:01.911216    19 SocketClientGo request 4 of 10
09:33:01.911216     8 attempting connection: 127.0.0.1:8080
09:33:01.911216    20 accepted connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000316780)}}
09:33:01.911216     8 made connection: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000316500)}}
09:33:01.911216     8 sent command: Saying, count=7
09:33:01.911216    85 handleConnection request: Saying~, nest: 1, conn: &net.TCPConn{conn:net.conn{fd:(*net.netFD)(0xc000316780)}}
09:33:01.911216    85 received command: Saying

09:33:02.349666    19 SocketClientGo response: I'm busy.~

注意输出的数量和复杂性,即使对于这个简单的场景也是如此。来自日志的信息量可能会很快失控。这是来自处理许多请求的服务器程序的典型日志,尤其是在云规模上。人们经常使用专门的搜索程序,如 ElasticsearchKibana16 经常结合工具如 Fluentd17 来查看这些日志。

HTTP 模板包

template包提供了安全生成 HTML 文本(根据需要添加 HTML 转义)和包含动态值的服务。text包中的template子包可以用于更简单的文本,比如电子邮件。Go 有一个默认的模板引擎。Java 没有标准的模板引擎,但是 Java 社区提供了几个。 18

模板是一个字符串,其中嵌入了一些替换内容。模板中的所有其他文本按原样输出。替换指令(又名指令)具有以下形式:{{ ... }},其中……被替换为若干选项之一,包括条件输出或重复输出以及调用 Go 代码来生成输出。人们甚至可以格式化嵌入的模板。可能性是丰富的。全面解释模板功能超出了本书的范围。更深入的解释见 Go 包文档。

模板在某些上下文中求值,通常由结构或映射实例提供。一个例子可以很好地说明这一点:

var tformat = time.RFC850

type Purchase struct {
     FirstName, LastName string
     Address string
     Phone string
     Age float32
     Male bool
     Item string
     ShipDate string
}

var purchases = []Purchase{
     {LastName:"Feigenbaum", ShipDate: time.Now().Format(tformat),
          Male:true, Item:"Go for Java Programmers"},
       //...
}

const purchaseTemplate = `
Dear {{ if .Male}}Mr.{{ else }}Ms.{{ end }} {{.LastName}},
Your purchase request for "{{ .Item }}" has been received.
It will be shipped on {{ .ShipDate }}.
Thank you for your purchase.
`

func runTemplate(p *Purchase, f *os.File, t *template.Template) (err error) {
     err = t.Execute(f, *p)
     return
}

func genTemplate(prefix string)  {
     t := template.Must(template.New("purchases").Parse(purchaseTemplate))
     for i, p := range purchases {
          f, err := os.Create(fmt.Sprintf("%s-%d.tmpl",prefix, i) )
          if err != nil {
               log.Fatalf("failed to create file, cause:", err)
          }
          err = runTemplate(&p, f, t)
          if err != nil {
               log.Fatalf("failed to run template, cause:", err)
          }
     }
}

这里,输入购买值提供了在各种替换中要替换的值。使用路径前缀调用 genTemplate 会生成一个文件(/tmp/example-0.tmpl),其内容为:

Dear Mr. Feigenbaum,
Your purchase request for "Go for Java Programmers" has been received.
It will be shipped on Friday, 07-May-21 06:19:24 PDT.
Thank you for your purchase.

需要注意的是,模板可以在内存中,也可以存储在外部,比如文件中。这使得它们的行为很像 Jakarta 企业版中定义的 Java 服务器页面 19 (JSP)模板。JSP 被动态转换成 Javaservlet20然后运行。Go 模板总是被解释。这使得 Java (servlet)方式对于大量工作更有效,但是对于少量工作成本更高。

作为一个例子,这里有一个完整的 HTTP 服务器,它可以呈现一个模板。它可以很容易地扩展为呈现多个模板,比如每个请求处理程序一个模板。

假设有一个文件“tod.tmpl ”,其内容是以下类似 HTML 的文本:

<!DOCTYPE HTML>
<html>
<head><title>Date and Time</title></head>
<body>
<p>Date and Time:</p>
<br><br>
<sl>
<li>The date is <big>{{ .TOD | formatDate }}</big>
<li>The time is <big>{{ .TOD | formatTime }}</big>
</sl>
</body>

</html>

清单 16-2 显示了使用它的完整代码。

package main

import (
     "fmt"
     "io/ioutil"
     "log"
     "net/http"
     "text/template"
     "time"
)

var functionMap = template.FuncMap{
     "formatDate": formatDate,
     "formatTime": formatTime,
}
var parsedTodTemplate *template.Template

func loadTemplate(path string) {
     parsedTodTemplate = template.New("tod")
     parsedTodTemplate.Funcs(functionMap)
     data, err := ioutil.ReadFile(path)
     if err != nil {
          log.Panicf("failed reading template %s: %v", path, err)
     }
     if _, err := parsedTodTemplate.Parse(string(data)); err != nil {
          log.Panicf("failed parsing template %s: %v", path, err)
     }
}
func formatDate(dt time.Time) string {
     return dt.Format("Mon Jan _2 2006")
}
func formatTime(dt time.Time) string {
     return dt.Format("15:04:05 MST")
}

type TODData struct {
     TOD time.Time
}

func processTODRequest(w http.ResponseWriter, req *http.Request) {
     var data = &TODData{time.Now()}
     parsedTodTemplate.Execute(w, data) // assume cannot fail
}

var serverPort = 8085

func timeServer() {
     loadTemplate( `C:\Temp\tod.tmpl`)
     http.HandleFunc("/tod", processTODRequest)
     spec := fmt.Sprintf(":%d", serverPort)
     if err := http.ListenAndServe(spec, nil); err != nil {
          log.Fatalf("failed to start server on port %s: %v", spec, err)
     }
     log.Println("server exited")
}

func main() {
     timeServer()
}

Listing 16-2Time Server Implementation

注意错误处理没有完成。

类似http://localhost:8085/tod的请求产生如图 16-1 所示的输出。

img/516433_1_En_16_Fig1_HTML.jpg

图 16-1

时间服务器输出

网。HTTP 包

net.http包裹很大。它为 TCP/IP 上的 HTTP 访问提供基本服务。它使得创建 HTTP 客户机和服务器,尤其是类似 REST 的服务器,变得相对容易。如前所述,结合 Go 模板,它可以使提供可变的 HTML 内容(很像 Java 中的 servlets)变得容易。静态 HTML,CSS,JS 等。文件中的内容也很容易提供。Go 1.16 增加了访问捆绑在可执行文件中的静态内容的能力;这允许将全功能的 web 服务器构建到单个可分发文件中。有关更多详细信息,请参见 Go 1.16 发行说明。

http包为各种 HTTP 方法和状态代码提供了常量。它有以下几个关键变量:

var DefaultClient–由内置的 Head、Get 和 Post 方法使用

var DefaultServeMux–Serve 方法在未提供覆盖时使用的 ServeMux

空空的身体

http包提供了这些功能。有些是不言自明的:

  • func canonical header key(s string)string–返回一个规范的头名称

  • func detect content type(data[]byte)字符串-对内容类型的有根据的猜测

  • 函数错误(w ResponseWriter,错误字符串,代码 int)-返回一个 HTTP 错误

  • func Handle(模式字符串,处理程序 Handler)-注册一个请求处理程序

  • func HandleFunc(模式字符串,处理程序 func(ResponseWriter,* Request))–注册一个处理程序函数

  • func ListenAndServe(addr string,handler Handler)错误–开始接受 HTTP 请求

  • func ListenAndServeTLS(addr,certFile,keyFile string,handler Handler)错误–开始接受 HTTP 和 HTTPS 请求

  • func max bytes reader(w response writer,r io。ReadCloser,n int64) io。read closer——做一个有限的读者

  • func NotFound(w ResponseWriter,r * Request)-返回 HTTP 404

  • func ParseHTTPVersion(到字符串)(major、minor int、ok bool)

  • func ParseTime(文本字符串)(t time。时间,误差误差)

  • func proxy from environment(req * Request)(* URL。URL,错误)–返回代理 URL(如果有)

  • func Redirect(w ResponseWriter,r *Request,url string,code int)-返回重定向响应

  • 功能服务(l 网络。Listener,handler Handler)错误-接受 HTTP 请求。在新的 goroutine 中处理每个

  • func serve content(w response writer,req *Request,名称字符串,modtime 时间。时间,内容 io。read seeker)–返回提供的内容(通常是文件的内容)

  • func ServeFile(w ResponseWriter,r *Request,name string)-读取并提供文件/目录

  • func ServeTLS(l net。侦听器、处理程序处理程序、证书文件、密钥文件字符串)错误–服务 HTTP 和 HTTPS

  • func SetCookie(w ResponseWriter,Cookie * Cookie)-设置一个响应 Cookie

  • func status text(code int)string–获取 HTTP 状态代码的文本

客户端支持 HTTP 客户端。它提供了这些功能。有些是不言自明的:

  • func(c * Client)CloseIdleConnections()

  • func(c * Client)Do(req * Request)(* Response,error)-发送/接收请求

  • func (c *Client) Get(url 字符串)(resp *Response,err error)-执行 Get

  • func (c *Client) Head(url 字符串)(resp *Response,err error)-Do Head

  • func (c *Client) Post(url,contentType 字符串,body io。Reader) (resp *Response,err error)-用正文发布帖子

  • func (c *Client) PostForm(url 字符串,数据 url。Values) (resp *Response,err error)-用 0+表单值执行 POST

CloseNotifier 允许在连接关闭时发出通知。

ConnState 允许观察连接。

Cookie 代表一个 HTTP cookie。

CookieJar 表示一组 cookies。

Dir 允许访问目录。

  • func (d Dir) Open(名称字符串)(文件,错误)

文件允许访问文件。

文件系统允许访问多个静态文件。它使得实现一个静态内容(比如图片、HTML、CSS 等。)服务器简单。

处理程序响应 HTTP 请求。它们提供以下功能:

  • func 文件服务器(根文件系统)处理程序

  • func NotFoundHandler()处理程序

  • func 重定向处理程序(url 字符串、int 代码)处理程序

  • func StripPrefix(前缀字符串,h 处理程序)处理程序

  • func TimeoutHandler(h Handler,dt time。持续时间,消息字符串)处理程序

HandlerFuncs 响应 HTTP 请求。它们提供以下功能:

  • func(f handler func)ServeHTTP(w response writer,r *Request)

Header 表示 HTTP 请求/响应头:

  • func (h Header) Add(键,值字符串)

  • func (h 头)Clone()头

  • func (h Header) Del(密钥字符串)

  • func (h Header) Get(key string)字符串

  • func (h Header)集合(键,值字符串)

  • func (h Header)值(密钥字符串)[]字符串

  • func (h 头)写(w io。Writer)错误

  • func (h Header) WriteSubset(w io。Writer,exclude map[string]bool)错误–写入选定的标头

Request 表示客户端请求。它提供了这些功能。许多人使用请求的字段:

  • func NewRequest(方法,url 字符串,正文 io。读者)(*请求,错误)-提出请求

  • func NewRequestWithContext(ctx 上下文。上下文、方法、url 字符串、正文 io。Reader) (*Request,error)–提出带有上下文的请求

  • func ReadRequest(b bufio。读取器)(请求,错误)–解析请求

  • func(r * Request)Add Cookie(c * Cookie)-添加一个 Cookie

  • func (r *Request) BasicAuth()(用户名,密码字符串,ok bool)-使用基本身份验证从请求中获取凭据

  • func (r Request)克隆(ctx 上下文。上下文)请求–在新的上下文中进行克隆

  • func (r *Request)上下文()上下文。语境

  • func (r Request) Cookie(名称字符串)(Cookie,错误)

  • func(r * Request)Cookie()[]* Cookie

  • func (r Request)表单文件(密钥字符串)(多部分。文件,多部分。文件头,错误)

  • func (r *Request) FormValue(密钥字符串)字符串

  • func(r * Request)multipart reader()(* multipart。阅读器,错误)

  • func (r *Request) ParseForm()错误–设置字段表单数据

  • func(r * Request)ParseMultipartForm(maxMemory int 64)错误–设置表单数据的字段

  • func(r * Request)PostFormValue(key string)string–设置字段表单数据

  • func(r * Request)proto 至少(major,minor int)bool–至少测试一个协议版本

  • func (r *Request) Referer()字符串

  • func (r *Request) SetBasicAuth(用户名,密码字符串)

  • func (r *Request) UserAgent()字符串

  • func(r * Request)with context(CTX context。上下文)*请求

  • func(r *请求)写(w io。Writer)错误

  • func (r *Request) WriteProxy(w io。Writer)错误

Response 表示服务器响应和 HTTP 操作:

  • func Get(url 字符串)(resp *Response,err error)-执行 Get 请求

  • func Head(url 字符串)(resp *Response,err error)-执行 Head 请求

  • func Post(url,contentType 字符串,body io。Reader) (resp *Response,err error)-用正文执行 POST 请求

  • func PostForm(url 字符串,数据 url。Values) (resp *Response,err error)-用表单数据执行 POST 请求

  • func ReadResponse(r *bufio。Reader,req Request) (Response,error)——做一个 HTTP 请求;做出回应

  • func(r * Response)Cookie()[]* Cookie

  • func(r * Response)Location()(* URL。URL,错误)

  • func(r * Response)proto minimum(major,minor int) bool

  • func (r *Response)写(w io。Writer)错误–发送响应

RoundTripper 基于不同的协议封装发送/接收事务:

  • func NewFileTransport(fs 文件系统)往返

ServeMux 解码传入的请求:

  • func NewServeMux()* ServeMux–制作解码器

  • func (mux *ServeMux) Handle(模式字符串,处理程序 Handler)-解码请求

  • func (mux ServeMux) HandleFunc(模式字符串,处理程序 func(ResponseWriter, Request))–解码请求

  • func (mux *ServeMux)处理程序(r *Request) (h 处理程序,模式字符串)-接收请求

  • func(mux * serve mux)serve HTTP(w response writer,r * Request)-启动 HTTP 服务器

服务器提供基本的 HTTP 服务器行为:

  • func (srv *Server) Close()错误–停止请求处理

  • func (srv *Server) ListenAndServe()错误–开始提供 HTTP

  • func(SRV * Server)ListenAndServeTLS(certFile,keyFile string)错误–开始提供 HTTP 和 HTTPS 服务

  • func(SRV * Server)registersonshutdown(f func())–注册关闭回调

  • func (srv *Server) Serve(l net。Listener)错误–开始提供 HTTP 服务

  • func (srv *Server) ServeTLS(l net。监听器,证书文件,密钥文件字符串)错误–开始提供 HTTP 和 HTTPS 服务

  • func(SRV *服务器)关闭(ctx 上下文。上下文)错误–关闭请求处理

传输提供了往返数据移动。它管理客户端和服务器之间的连接状态。它配置 TCP 连接:

  • func (t *Transport)取消请求

  • func(t 传输)Clone()传输

  • func(t * Transport)CloseIdleConnections()

  • func(t * Transport)Register protocol(scheme string,rt round tripler)-注册一个协议处理程序

  • func (t Transport)往返(req * Request)(响应,错误)

清单 16-3 显示了一个完整但功能较低的类似 HTTP REST 的服务器的例子。

package main

import (
     "fmt"
     "io"
     "log"
     "net/http"
     "strings"
     "time"
)

func greet(w http.ResponseWriter, req *http.Request) {
     if req.Method != "GET" {
          http.Error(w, fmt.Sprintf("Method %s not supported", req.Method), 405)
          return
     }
     var name string
     if err := req.ParseForm(); err == nil {
          name = strings.TrimSpace(req.FormValue("name"))
     }
     if len(name) == 0 {
          name = "World"
     }
     w.Header().Add(http.CanonicalHeaderKey("content-type"),
          "text/plain")
     io.WriteString(w, fmt.Sprintf("Hello %s!\n", name))
}

func now(w http.ResponseWriter, req *http.Request) {
     // request checks like in greet
     w.Header().Add(http.CanonicalHeaderKey("content-type"),
          "text/plain")
     io.WriteString(w, fmt.Sprintf("%s", time.Now()))
}

func main() {
     fs := http.FileServer(http.Dir(`/temp`))

     http.HandleFunc("/greet", greet)
     http.HandleFunc("/now", now)
     http.Handle( "/static/", http.StripPrefix( "/static", fs ) )
     log.Fatal(http.ListenAndServe(":8088", nil))
}

Listing 16-3Basic Hello World, Time and File HTTP Server

这提供了两条路径:

  1. greet–返回所提供名称的问候语

  2. now–返回服务器的当前时间

它在端口 8080 上为他们服务。

一些样本结果如图 16-2 、 16-3 和 16-4 所示。

img/516433_1_En_16_Fig4_HTML.jpg

图 16-4

文件服务器响应 1

img/516433_1_En_16_Fig3_HTML.jpg

图 16-3

时间服务器响应

img/516433_1_En_16_Fig2_HTML.jpg

图 16-2

Hello 服务器响应

或者对于未知文件,参见图 16-5 。

img/516433_1_En_16_Fig5_HTML.jpg

图 16-5

文件服务器响应 2

但是如果使用了无效的 HTTP 方法,请参见图 16-6 。

img/516433_1_En_16_Fig6_HTML.jpg

图 16-6

错误的请求响应

处理程序(默认情况下)返回带有text/plain内容类型的 HTTP 200 响应。处理程序由HandleFunc保存在一个全局列表中,由传递给ListenAndServer (LnS)的nil值使用。如果服务器不能启动(比如说端口 8080 已经被使用了),服务器程序就会出错结束;否则,LnS 函数不会返回。

服务器将处理传入的请求,直到被操作系统终止。每个请求都在它自己的线程中运行,所以服务器是高性能的,并且服务器可以处理许多并发请求。

http包提供了一个简单易用的文件服务器。其中一个已经包含在前面带有static路由的示例服务器代码中。如图 16-7 所示使用。

img/516433_1_En_16_Fig7_HTML.jpg

图 16-7

文件服务器示例输出;服务文本文件的内容

该特性可用于提供静态内容,如 HTTP、CSS、图像、JavaScript 等。必须注意不要共享服务器的私有数据。

作为 Go to Node.js 相似性的一个例子,考虑这个相似的(只有一个函数并且没有错误检查)等价物,它由 Node.js 引擎运行。以下是用 JavaScript 写的:

const http = require('http');
const os = require('os');
var todHandler = function(req, resp) {
   resp.writeHead(200);
   resp.end(new Date().toString());
}
var server = http.createServer(todHandler);
server.listen(8080);

包 URL

url包是一个net子包,提供 URL 解析和处理。

它具有以下功能和类型:

  • func path escape(s string)string–URL 转义路径

  • func path unescape(s string)(string,error)-反转转义

  • func query scape(s string)string–URL 转义查询字符串

  • func query escape(s string)(string,error)-反转转义

URL 在 URL 上提供函数。大多数函数获取/测试部分解析的 URL:

  • func Parse(rawurl 字符串)(*url,错误)-解析任何 URL

  • func ParseRequestURI(rawurl 字符串)(*URL,错误)-解析 URL;不允许有碎片

  • func (u *URL) EscapedFragment()字符串

  • func (u *URL) EscapedPath()字符串

  • func (u *URL) Hostname()字符串

  • func (u *URL) IsAbs() bool

  • func(u * URL)Parse(ref string)(* URL,error)-在此 URL 的上下文中解析

  • func (u *URL) Port()字符串

  • func (u *URL)查询()值

  • func (u *URL)密文()字符串

  • func (u *URL) RequestURI() string

  • func(u * URL)Resolve reference(ref * URL)* URL–在此 URL 的上下文中解析

Userinfo 提供用户凭据:

  • func user(username string)* user info

  • func UserPassword(用户名,密码字符串)*Userinfo

  • func(u * userid info)password()(string,bool)

  • func(u * userid info)username()字符串

Values 提供了获取/使用路径和查询参数的函数:

  • func ParseQuery(查询字符串)(值,错误)-将查询参数解析为值

  • func (v Values) Add(key,value string)–将值添加到键值中

  • func (v 值)Del(密钥字符串)–删除密钥

  • func (v 值)Encode()字符串–URL 编码值

  • func(v Values)Get(key string)string–获取键的第一个值

  • func (v Values) Set(key,value string)–重置键值

十七、运行时

本章介绍了几个与 Go 运行时相关的 Go 包。

错误包

errors包提供了帮助创建和选择错误的功能。存在许多社区扩展,一些插入式替代。

有一个内置的error类型。所有错误都实现了这个预定义的接口:

type error interface {
      Error() string
}

这意味着任何符合这个接口的类型(例如,有Error方法)都可以被用作错误。这意味着可以创建许多自定义错误。许多 Go 包提供自定义错误,通常包括关于失败的补充信息。例如,考虑os.PathError类型:

type PathError struct {
      Op string    // failed operation
      Path string  // failed on path
      Err error    // root cause, if any
}

所有的自定义错误都需要实现error接口,比如前面的错误:

func (pe *PathError) Error() string {
      if pe.Err == nil {
            return fmt.Sprintf("PathError %s:%s", pe.Op, pe.Path)
      }
      return fmt.Sprintf("PathError %s:%s:%v", pe.Op, pe.Path, pe.Err.Error())
}

自定义错误类型可以是任何基类型,而不仅仅是结构,如字符串。例如:

type MyError string

func (me MyError) Error() string {
      return string(me)
}

errors包具有以下功能:

  • func As(err error,target interface { })bool–如果是目标类型,则转换为目标类型;目标是指向接收转换错误的位置的指针。

  • func Is(err,target error)bool–测试目标类型是否为 err。

  • func New(文本字符串)错误–出错。

  • func Unwrap(err error)错误–如果可用,获取包装的错误原因;err 必须有一个Unwrap()方法。

通常,错误被声明为顶级值。这样可以测试它们是否相等,或者与前面的函数一起使用,防止返回错误。例如:

var (
      ErrSystem = errors.New("System Error")
      ErrIO = errors.New("I/O Error")
      ErrOther = errors.New("Other Error")
     :
)

标签封装

flag包实现了一种简单但标准化的方法来解析命令行。通常,参数是二进制开关(又名标志,因此是包名)。存在许多社区扩展。

flag 包主要作为一个全局库工作,因为它假设只有一个命令行要解析。定义了各种可能的命令行值,然后解析命令行来查找这些值,并设置任何找到的值。还提供了描述参数的帮助功能。定义值的方式有几种选择。一般情况下,不要混用XxxXxxVar(其中 Xxx 是类型名)样式;坚持使用它们。

一些示例标志定义:

var iflag int
var fflag float64
var sflag string
func init() {
      flag.IntVar(&iflag, "IntFlag", 1, "IntFlag sets ...")
      flag.Float64Var(&fflag, "FloatFlag", 1.0, "FloatFlag sets ...")
      flag.StringVar(&sflag, "StringFlag", "", "StringFlag sets ...")
}

This is indeed a long distance.

标志看起来像命令行中的以下选项之一:

  • -flag

  • -flag=x

  • -flag x -标志不能是布尔类型

flag包提供了这个关键变量:

var CommandLine–默认访问os.Args

flag包提供了这些类型:

  • error handling–控制如何处理错误的枚举

  • 标志–标志的状态,包括当前值

  • 标志集–标志的集合,通常每个可能的标志一个

flag包提供了这些功能。他们访问全局标志集:

  • func Arg(I int)string–返回第 I 个非标志参数

  • func Args()[]string–返回所有非标志参数

  • func Bool(名称字符串、值 Bool、用法字符串)* bool 使布尔标志

  • func BoolVar(p *bool,名称字符串,值 bool,用法字符串)-将 p 包装为标志

  • func Duration(名称字符串,值时间。持续时间,使用字符串)*时间。持续时间–制作持续时间标志

  • func DurationVar(p *time。持续时间、名称字符串、值时间。持续时间,用法字符串)–将 p 包装为标志

  • func Float64(名称字符串,值 Float64,用法字符串)* float 64–生成一个浮点标志

  • func Float64Var(p *float64,名称字符串,值 float64,用法字符串)-将 p 包装为标志

  • func int(名称字符串,值 Int,用法字符串)* Int–创建 Int 标志

  • func Int64(名称字符串,值 Int64,用法字符串)* int64–创建 int 64 标志

  • func Int64Var(p *int64,名称字符串,值 int64,用法字符串)–将 p 包装为标志

  • func IntVar(p *int,name string,value int,usage string)–将 p 包装为一个标志

  • func NArg()int–非标志参数的数量

  • func NFlag()int–标志参数的数量

  • func Parse()-解析命令行,并在定义所有标志后设置参数和标志

  • func Parsed()bool–测试是否已解析

  • func print defaults()–向用户描述默认值

  • 函数集(名称,值字符串)错误–设置标志的值

  • 函数字符串(名称字符串、值字符串、用法字符串)*字符串–创建字符串标志

  • func StringVar(p *string,名称字符串,值字符串,用法字符串)–将 p 包装为标志

  • func uint(名称字符串、值 Uint、用法字符串)* Uint–创建 Uint 标志

  • func Uint64(名称字符串,值 Uint64,用法字符串)* uint64–制作 uint 64 标志

  • func Uint64Var(p *uint64,名称字符串,值 uint64,用法字符串)-将 p 包装为标志

  • func UintVar(p *uint,名称字符串,值 uint,用法字符串)-将 p 包装为标志

  • func UnquoteUsage(flag *Flag)(名称字符串,用法字符串)-获取标志的描述

  • func Var(值 value,名称字符串,用法字符串)-制作一个通用标志

  • func Visit(fn func(* Flag))–将 f 应用于所有设置的标志

  • func visit all(fn func(* Flag))–将 f 应用于所有标志

旗帜有这样的功能:

  • func Lookup(名称字符串)* Flag–按名称获取定义的标志

一个标志集有这些功能。许多与前面描述的相同,不再重复描述:

  • func NewFlagSet(名称字符串,错误处理错误处理)*FlagSet

  • func (f *FlagSet) Arg(i int)字符串

  • func(f * flag set)args()[]字符串

  • func (f FlagSet) Bool(名称字符串、值布尔、用法字符串)bool

  • func(f * flag set)boor(p * bool,名称字符串,值 bool,用法字符串)

  • func (f FlagSet) Duration(名称字符串,值时间。持续时间,使用字符串)时间。持续时间

  • func(f * FlagSet)duration var(p * time。持续时间、名称字符串、值时间。持续时间、使用字符串)

  • func(f * flag set)error handling()error handling

  • func (f FlagSet) Float64(名称字符串,值 Float64,用法字符串)float64

  • func(f * FlagSet)float64 var(p * float 64,名称字符串,值 float 64,用法字符串)

  • func (f *FlagSet) Init(名称字符串,错误处理错误处理)

  • func (f FlagSet) Int(名称字符串,值 Int,用法字符串)int

  • func (f FlagSet) Int64(名称字符串,值 Int64,用法字符串)int64

  • func(f * FlagSet)int64 var(p * int 64,名称字符串,值 int 64,用法字符串)

  • func (f *FlagSet) IntVar(p *int,名称字符串,值 int,用法字符串)

  • func (f FlagSet)查找(名称字符串)标志

  • func (f *FlagSet) NArg() int

  • func (f *FlagSet) NFlag() int

  • func (f *FlagSet) Name()字符串

  • func (f *FlagSet)输出()io。作者

  • func (f *FlagSet)解析(arguments []string)错误

  • func (f *FlagSet) Parsed() bool

  • func (f *FlagSet) PrintDefaults()

  • func (f *FlagSet)集合(名称,值字符串)错误

  • func (f *FlagSet) SetOutput(输出 io。作家)

  • func (f FlagSet)字符串(名称字符串、值字符串、用法字符串)字符串

  • func(f * FlagSet)string var(p * string,名称字符串,值字符串,用法字符串)

  • func (f FlagSet) Uint(名称字符串,值 Uint,用法字符串)uint

  • func (f FlagSet) Uint64(名称字符串,值 Uint64,用法字符串)uint64

  • func(f * FlagSet)uint64 var(p * uint 64,名称字符串,值 uint 64,用法字符串)

  • func (f *FlagSet) UintVar(p *uint,name string,value uint,usage string)

  • func (f *FlagSet) Var(值值,名称字符串,用法字符串)

  • 访问(fn func(*Flag))

  • func(f * flag set)visitall(fn func(* flag))

Go flags包固执己见。它支持受限样式的标志。不同的操作系统(OS)类型可能有不同的标志样式。尤其是通常使用正斜杠(“/”)字符而不是破折号(“-”)来引入标志的 Windows。

为了使您的程序与操作系统风格相匹配,您可能需要编写代码来以不同的方式解析命令行。runtime.GOOS值可用于确定操作系统类型。你可以在这里找到有帮助的社区包。

日志包

log包提供了一个简单的日志功能。存在许多社区扩展。有些是直接替换,有些则使用不同的风格。许多社区产品都使用(或表面上)这种日志功能。这个包类似于 JavaLog4J1或者类似的日志框架。

这个包提供了包级日志功能。它还有一个记录器接口,具有任何代码都可以实现的类似功能。它为指向某个io.Writer的任何消息提供格式化的日志前缀,比如 STDOUT。详细信息,如日期和时间格式、源文件引用等。可以配置前缀字符串的。某些日志操作可能会导致死机或退出程序。

在使用 Log4J 的 Java 中,可以这样做:

import ...Log4J.Logger;
static Logger log = Logger.getLogger(<myclass>.class);
:
log.trace("Program running...")
:
log.trace("Program done")

一个基本的 Go 日志序列是

import "log"
:
log.Print("Program running...")
:
log.Print("Program done")

请注意,这个预定义的记录器会记录到 STDERR。

同样,如果您有一个 Logger 实例,您可以配置它:

import "log"
var logger = log.New(<someWriter>, "<someLinePrefix>", <flags>)
:
logger.Print("Program running...")
:
logger.Print("Program done")

根据配置的不同,可能会输出如下所示的行:

2021/01/01 00:00:00.123456 /x/y/x.go:100: Program running...

请注意,没有提供严重性。许多第三方日志记录产品添加了这一统计数据和其他统计数据。请参见下面的示例。

由于日志记录程序的创建需要一个编写器作为参数,日志记录可以针对许多目标,包括像文件这样的持久性目标。记录器的客户端(创建者)需要打开和关闭这样的目标。例如,要记录完整程序的输出:

var DefaultLogger log.Logger
var DefaultLoggerTarget os.File
var DefaultLoggerTargetPath = "main.log"
:
func main() {
      var f *os.File
      if f, err := os.Create(DefaultLoggerTargetPath); err != nil {
            log.Fatalf("Cannot create log file: %s\n",
                  DefaultLoggerTargetPath)
      }
      defer f.Close()
      DefaultLoggerTarget = f
      DefaultLogger = log.New(f, "main ", log.LstdFlags)
      defer DefaultLogger.Flush()
      DefaultLogger.Println("main starting...")
      :
      DefaultLogger.Flush()
      :
      DefaultLogger.Println("main done")
}

注意,记录器是一个公共的顶级值,因此可以从程序中的所有函数访问它。这比将 logger 实例作为函数参数传递给程序要容易得多。此示例在程序每次运行时重新创建日志。可以使用Open函数(vs. Create)来(比方说)附加到现有的日志中。

在程序退出之前,可能不会写入日志输出。如果将创建的文件公开为公共顶级值(如示例中所做的那样),编码人员可以对其使用Flush函数来强制在其他时间写入数据。

作为使用 Go 日志的一个例子,清单 17-1 展示了 Go 社区可能提供的一个简单的扩展/包装器。它提供了一个日志记录器接口,输出任何日志引擎都需要实现的分级日志消息。这个记录器不是等同于标准记录器的 API,因此不是一个现成的替代品。

该示例提供了一个名为 DefaultLoggerImpl 的默认引擎实现。有访问当前状态的 helper 函数,包括获取调用 goroutine 的 id;这是 Go 运行时不提供直接访问的函数。

type Logger interface {
      Error(format string, args ...interface{})
      Warn(format string, args ...interface{})
      Info(format string, args ...interface{})
      Debug(format string, args ...interface{})
      Trace(format string, args ...interface{})
}

type DefaultLoggerImpl struct{
      logger log.Logger
}
func (l *DefaultLoggerImpl) output(level, format string, args ...interface{}) {
      l.logger.Printf(fmt.Sprintf("%s %s %s\n",getCallerDetails(2, "-"),level, fmt.Sprintf(format, args...)))
}
func (l *DefaultLoggerImpl) Error(format string, args ...interface{}) {
      l.output("ERROR", format, args...)
}
func (l *DefaultLoggerImpl) Warn(format string, args ...interface{}) {
      l.output("WARN ", format, args...)
}
func (l *DefaultLoggerImpl) Info(format string, args ...interface{}) {
      l.output("INFO ", format, args...)
}
func (l *DefaultLoggerImpl) Debug(format string, args ...interface{}) {
      l.output("DEBUG", format, args...)
}
func (l *DefaultLoggerImpl) Trace(format string, args ...interface{}) {
      l.output("TRACE", format, args...)
}

var DefaultLogger  *DefaultLoggerImpl

func init(){
      DefaultLogger = &DefaultLoggerImpl{}
      DefaultLogger.logger = log.New(os.Stdout, "GoBook ", log.LstdFlags|log.Lmicroseconds|log.LUTC)
}

// get details about the caller.
func getCallerDetails(level int, lead string) string {
      level++
      if pc, file, line, ok := runtime.Caller(level); ok {
            file = getName(file)
            goId := getGID()
            xlineCount := atomic.AddUint64(&lineCount, 1)
            lead = fmt.Sprintf("%7d go%-5d %08X %-40v@%4v", xlineCount, goId, pc, file, line)
      }

      return lead
}

var lineCount uint64

// Get the current goroutine id.
func getGID() (n uint64) {
      b := make([]byte, 64)
      b = b[:runtime.Stack(b, false)]
      b = bytes.TrimPrefix(b, []byte("goroutine "))
      b = b[:bytes.IndexByte(b, ' ')]
      n, _ = strconv.ParseUint(string(b), 10, 64)
      return
}

//  Get the file name part.
func getName(file string) string {
      posn := strings.Index(file, src)
      if posn >= 0 {
            file = file[posn+len(src):]
            if strings.HasSuffix(file, goExtension) {
                  file = file[0 : len(file)-len(goExtension)]
            }
      }
      return file
}

const src = "/src/"
const goExtension = ".go"

Listing 17-1Sample Logger Implementation

在不同的 goroutines 中进行日志记录时,可以像这样使用它,如清单 17-2 所示。

DefaultLogger.Trace("Hello %s!", "World")
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
      wg.Add(1)
      go func(id int) {
            defer wg.Done()
            DefaultLogger.Info("Hello from goroutine %d!", id)
            time.Sleep( time.Duration(rand.Intn(2000)) * time.Millisecond)
            DefaultLogger.Info("Goodbye from goroutine %d!", id)
      }(i)
}
wg.Wait()
DefaultLogger.Trace("Goodbye %s!", "World")

Listing 17-2Sample Logger Implementation Client

此示例产生如下输出:

GoBook 2020/12/18 15:21:57.365337       1 go1     004D6AE7 main/main                               @ 122 TRACE Hello World!
GoBook 2020/12/18 15:21:57.366333       3 go15    004D6D97 main/main                               @ 128 INFO  Hello from goroutine 9!
GoBook 2020/12/18 15:21:57.366333       5 go9     004D6D97 main/main                               @ 128 INFO  Hello from goroutine 3!
GoBook 2020/12/18 15:21:57.366333       4 go6     004D6D97 main/main                               @ 128 INFO  Hello from goroutine 0!
GoBook 2020/12/18 15:21:57.366333       2 go7     004D6D97 main/main                               @ 128 INFO  Hello from goroutine 1!
GoBook 2020/12/18 15:21:57.366333       7 go10    004D6D97 main/main                               @ 128 INFO  Hello from goroutine 4!
GoBook 2020/12/18 15:21:57.366333       9 go14    004D6D97 main/main                               @ 128 INFO  Hello from goroutine 8!
GoBook 2020/12/18 15:21:57.366333       8 go11    004D6D97 main/main                               @ 128 INFO  Hello from goroutine 5!
GoBook 2020/12/18 15:21:57.366333      10 go12    004D6D97 main/main                               @ 128 INFO  Hello from goroutine 6!
GoBook 2020/12/18 15:21:57.366333       6 go8     004D6D97 main/main                               @ 128 INFO  Hello from goroutine 2!
GoBook 2020/12/18 15:21:57.366333      11 go13    004D6D97 main/main                               @ 128 INFO  Hello from goroutine 7!
GoBook 2020/12/18 15:21:57.426070      12 go7     004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 1!
GoBook 2020/12/18 15:21:57.447973      13 go15    004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 9!
GoBook 2020/12/18 15:21:57.447973      14 go10    004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 4!
GoBook 2020/12/18 15:21:57.792721      15 go11    004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 5!
GoBook 2020/12/18 15:21:57.822589      16 go8     004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 2!
GoBook 2020/12/18 15:21:57.917368      17 go12    004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 6!

GoBook 2020/12/18 15:21:58.674824      18 go13    004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 7!
GoBook 2020/12/18 15:21:58.684779      19 go14    004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 8!
GoBook 2020/12/18 15:21:59.228337      20 go6     004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 0!
GoBook 2020/12/18 15:21:59.254222      21 go9     004D6E84 main/main                               @ 130 INFO  Goodbye from goroutine 3!
GoBook 2020/12/18 15:21:59.254222      22 go1     004D6C2E main/main                               @ 134 TRACE Goodbye World!

注意不同的 go routine id(go##)。当查看使用多个 goroutine 的代码的跟踪时,在日志记录中显示 goroutine id 非常有帮助。否则,日志记录会显得非常混乱。

记录在创建时就被分配了行号,而不是在由 Go logger 写入控制台时被分配;因此,行号并不总是按顺序出现,因为 goroutines 并不按任何可预测的顺序运行。在单个 goroutine 中,行号通常是连续的。

可执行文件中的实际代码位置显示在(十六进制)日志调用者中。这在撞车时会有帮助。如果日志调用来自不同的包,就会显示出来。在这种情况下,所有被调用的日志都是从同一个包(main)和函数(main)中完成的。包括呼叫线路号码。

runtime.Stack(b, false)的结果开头像下面这段文字。这就是getGID()如何访问 id:

goroutine 1 [running]:
:

注意不能保证Stack方法的输出在将来不会改变,因此这段代码会被废弃。

数学包

math包提供类似于java.math包的功能。结合 Go 的复数类型,这使得 Go 成为相对于 Java 更强大的数字处理语言。它提供了有用的常数和数学函数。常量有 E,Pi,Phi,Sqrt2,SqrtE,SqrtPi,SqrtPhi,Ln2,Log2E (1/Ln2),Ln10,Log10E (1/Ln10)。大多数至少有 60 位数的精度。

math包有几个子包:

  • big提供了大整数(Int,很像java.math.BigInteger)、大浮点(Float,类似但不等同于java.math.BigDecimal),以及有理(Rat,没有 Java 等价的)数类型。

  • bits提供计数、访问和改变无符号整数类型的位的函数。

  • cmplx(注意奇怪的名字,这是因为complex是保留字)为complex类型提供了有用的常数和数学函数。

  • rand提供随机数生成。

math包提供了这些(不言自明的)功能:

  • 函数伽玛(x 浮点 64)浮点 64

  • 功能海波(p,q 浮动 64)浮动 64

  • func ilgb(x float 64)int

  • func Inf(sign int)浮点 64

  • func IsInf(f float64,sign int) bool

  • func IsNaN(f float64) (is bool)

  • func J0(x float64) float64

  • 功能 J1(x 浮动 64)浮动 64

  • 函数 Jn(n int,x float64) float64

  • func ldxp(frac float 64,exp int) float64

  • func l gamma(x float 64)(l gamma float 64,sign int)

  • 函数日志(x 浮点 64)浮点 64

  • func Log10(x float64) float64

  • func Log1p(x float64) float64

  • 函数对数 2(x 浮点 64)浮点 64

  • 函数日志 b(x 浮点 64)浮点 64

  • 功能最大值(x,y 浮动 64)浮动 64

  • func Min(x,y 浮点 64)浮点 64

  • func Mod(x,y 浮点 64)浮点 64

  • func Modf(f 浮点 64) (int 浮点 64,frac 浮点 64)

  • func NaN() float64

  • func Nextafter(x,y float64) (r float64)

  • func Nextafter32(x,y float32) (r float32)

  • 功能功率(x,y 浮点 64)浮点 64

  • func Pow10(n int) float64

  • 函数余数(x,y 浮点 64)浮点 64

  • 功能圆(x 浮点 64)浮点 64

  • func 往返偶数(x 浮点 64)浮点 64

  • func 信号位(x float64) bool

  • func Sin(x float64) float64

  • 函数正弦余弦(x 浮点 64)(正弦余弦浮点 64)

  • func Sinh(x float64) float64

  • 函数 Sqrt(x float64) float64

  • func Tan(x float64) float64

  • func Tanh(x 浮动 64)浮动 64

  • func Trunc(x float64) float64

  • func Y0(x float64) float64

  • 功能 Y1(x 浮动 64)浮动 64

  • func Yn(n 整数,x 浮点)浮点 64

作为使用math包中函数的一个例子,清单 17-3 显示了一个简单的函数绘图示例:

var ErrBadRange = errors.New("bad range")

type PlotFunc func(in float64) (out float64)

// Print (to STDOUT) the plots of one or more functions.
func PlotPrinter(xsteps, ysteps int, xmin, xmax, ymin, ymax float64,
      fs ...PlotFunc) (err error) {
      xdiff, ydiff := xmax-xmin, ymax-ymin
      if xdiff <= 0 || ydiff <= 0 {
            err = ErrBadRange
            return
      }

      xstep, ystep := xdiff/float64(xsteps), ydiff/float64(ysteps)
      plots := make([][]float64, len(fs))
      for index, xf := range fs {
            plot := make([]float64, xsteps)
            plots[index] = plot
            err = DoPlot(plot, xf, xsteps, ysteps, xmin, xmax, ymin, ymax, xstep)
            if err != nil {
                  return
            }
      }
      PrintPlot(xsteps, ysteps, ymin, ymax, ystep, plots)
      return
}

// Plot the values of the supplied function.
func DoPlot(plot []float64, f PlotFunc, xsteps, ysteps int,
          xmin, xmax, ymin, ymax, xstep float64) (err error) {
      xvalue := xmin
      for i := 0; i < xsteps; i++ {
            v := f(xvalue)
            if v < ymin || v > ymax {
                  err = ErrBadRange
                  return
            }
            xvalue += xstep
            plot[i] = v
      }
      return
}

// Print the plots of the supplied data.
func PrintPlot(xsteps, ysteps int, ymin float64, ymax float64, ystep float64,
          plots [][]float64) {
      if xsteps <= 0 || ysteps <= 0 {
            return
      }
      middle := ysteps / 2
      for yIndex := 0; yIndex < ysteps; yIndex++ {
            fmt.Printf("%8.2f: ", math.Round((ymax-float64(yIndex)*ystep)*100)/100)
            ytop, ybottom := ymax-float64(yIndex)*ystep, ymax-float64(yIndex+1)*ystep
            for xIndex := 0; xIndex < xsteps; xIndex++ {
                  pv := " "
                  if yIndex == middle {
                        pv = "-"
                  }
                  for plotIndex := 0; plotIndex < len(plots); plotIndex++ {
                        v := plots[plotIndex][xIndex]
                        if v <= ytop && v >= ybottom {
                              pv = string(markers[plotIndex%len(markers)])
                        }

                  }
                  fmt.Print(pv)
            }
            fmt.Println()
      }
      fmt.Printf("%8.2f: ", math.Round((ymax-float64(ysteps+1)*ystep)*100)/100)
}

const markers = "*.^~-=+"

Listing 17-3Sample Plotting of a Math Function Client

它由这个测试函数驱动:

func testPlotPrint() {
      err := PlotPrinter(100, 20, 0, 4*math.Pi, -1.5, 4,
            func(in float64) float64 {
                  return math.Sin(in)
            }, func(in float64) float64 {
                  return math.Cos(in)
            }, func(in float64) float64 {
                  if in == 0 {
                        return 0
                  }
                  return math.Sqrt(in) / in
            })
      if err != nil {
            fmt.Printf("plotting failed: %v", err)
      }

}

注意,对于不同的示例方程,传递了三个不同的函数文字,它们符合PlotFunc类型。

这产生了如图 17-1 所示的输出。

img/516433_1_En_17_Fig1_HTML.jpg

图 17-1

数学函数输出的示例绘图

注意,三个情节,用的是“^”,“、和“*”作为标记,叠加在图形上。图表的中间(不是零点)用一条虚线标出。

big包提供了这些类型:

  • float–扩展精度浮点值

  • int–扩展(大)精度整数值

  • rat–由int64分子和分母组成的有理数数值

浮点类型有以下功能(大多数是不言自明的):

  • 函数 NewFloat(x float64) *Float

  • func ParseFloat(s string,base int,prec uint,RoundingMode) (f *Float,b int,err error)

  • 函数(z *浮点)绝对值(x 浮点)浮点

  • func (z *Float) Add(x,y *Float) *Float

  • func(x * Float)Append(buf[]字节,fmt 字节,prec int)[]字节

  • func(x * Float)Cmp(y * Float)int–比较

  • func(z * Float)Copy(x * Float)* Float

  • func (x *Float) Float32() (float32,精度)

  • func (x *Float) Float64() (float64,精度)

  • func (x *Float)格式(s fmt。状态,格式符文)

  • func (x *Float) Int(z Int) (Int,Accuracy)

  • func (x *Float) Int64() (int64,精度)

  • func (x *Float) IsInf() bool

  • func (x *Float) IsInt() bool

  • func(x * float)manexp(mant * float)(exp int)

  • func (x *Float) MinPrec() uint

  • func (x *Float)模式()舍入模式

  • func(z *浮点)Mul(x,y 浮点)浮点

  • 函数(z *浮点)负数(x 浮点)浮点

  • func (z *Float) Parse(s string,base int) (f *Float,b int,err error)

  • func(x * float)pre()uint

  • func (z *Float) Quo(x,y *Float) *Float

  • func (x *Float) Rat(z Rat) (Rat,精度)

  • 函数(z *Float)扫描(s fmt。ScanState,ch rune)错误

  • func (z *Float)集合(x *Float) *Float

  • func(z * Float)set Float 64(x Float 64)* Float

  • func(z Float)setnf(bool 符号) float

  • func(z * float)SETI(x * int)* float

  • func(z * float)setnt 64(x int 64)* float

  • func(z * float)setmanexp(mant * float,exp int) *Float

  • func (z Float) SetMode(模式舍入模式)Float

  • func(z * float)set rec(pre uint)* float

  • func(z * float)set rat(x * rat)* float

  • func(z * float)setstring(s string)(* float,bool)

  • func(z * float)set uint 64(x uint 64)* float

  • func (x *Float)符号()int

  • func (x *Float) Signbit() bool

  • 函数(z *浮点)平方(x 浮点)浮点

  • func (x *Float) String()字符串

  • func (z *Float) Sub(x,y *Float) *Float

  • func (x *Float)文本(格式字节,精度整数)字符串

  • func (x *Float) Uint64() (uint64,精度)

Int 类型具有以下功能(大多数是不言自明的):

  • func NewInt(x int64) *Int

  • func (z *Int) Abs(x *Int) *Int

  • func (z *Int) Add(x,y *Int) *Int

  • func (z *Int)和(x,y *Int) *Int

  • func (z *Int) AndNot(x,y *Int) *Int

  • func(x * int)append(buf[]字节,base int[]字节)

  • func (z *Int)二项式(n,k int64) *Int

  • func (x*Int) Bit(i int) uint

  • func (x *Int) BitLen() int

  • func(x * Int)Bits()[]字

  • func (x *Int)字节()[]字节

  • func(x * int)CMP(y * int)(r int)-比较

  • func(x * Int)CmpAbs(y * Int)Int–比较绝对值

  • func (z *Int) Div(x,y *Int) *Int

  • func (z Int) DivMod(x,y,m Int) (Int,Int)

  • func (z *Int) Exp(x,y,m *Int) *Int

  • func (x *Int)文件字节(buf[]字节)[]字节

  • func (x *Int)格式。状态,ch rune)

  • func (z *Int) GCD(x,y,a,b *Int) *Int

  • func (x *Int) Int64() int64

  • func (x *Int) IsInt64() bool

  • func (x *Int) IsUint64() bool

  • func (z *Int) Lsh(x *Int,n uint) *Int

  • func (z *Int) Mod(x,y *Int) *Int

  • func (z *Int)修订版本(g,n *Int) *Int

  • func (z *Int) ModSqrt(x,p *Int) *Int

  • func (z *Int) Mul(x,y *Int) *Int

  • func (z *Int) MulRange(a,b int64) *Int

  • func (z *Int) Neg(x *Int) *Int

  • func (z *Int) Not(x *Int) *Int

  • func (z *Int)或(x,y *Int) *Int

  • func (x *Int)可能是布林值(n int)

  • func (z *Int)现状(x,y *Int) *Int

  • func(z Int)qurem(x,y,r Int) (Int, int)

  • func (z *Int) Rand(rnd *rand)。边界,n * int * int

  • func (z *Int) Rem(x,y *Int) *Int

  • func (z *Int) Rsh(x *Int,n uint) *Int

  • 函数(z *Int)扫描(s fmtScanState,ch rune)错误

  • func (z *Int)集合(x *Int) *Int

  • func (z *Int) SetBit(x *Int,i int,b uint) *Int

  • func(z * int)set bits(ABS[]word)* int

  • func(z * int)set bytes(buf[]byte)* int

  • func(z * int)setnt 64(x int 64)* int

  • func (z Int) SetString(s string,base int) (Int,bool)

  • func(z * int)set uint 64(x uint 64)* int

  • func (x *Int)符号()Int

  • func (z *Int) Sqrt(x *Int) *Int

  • func (x *Int) String()字符串

  • func (z *Int) Sub(x,y *Int) *Int

  • func (x *Int)文本(base int)字符串

  • func(x * Int)trailing zero bits()uint

  • func (x *Int) Uint64() uint64

  • func (z *Int) Xor(x,y *Int) *Int

作为使用Int类型的一个例子,考虑一下N! (N 阶乘)函数。n!定义为:

  • N < 0:未定义

  • N == 0: 1

  • N > 0: N * (N-1)!

注意 N!如清单 17-4 中所实现的,随着 N 的增加而迅速变大。即使使用小 N ( < < 100),该值也超过了一个uint64(最大的机器整数)类型所能容纳的。

var ErrBadArgument = errors.New("invalid argument")

func factorial(n int) (res *big.Int, err error) {
      if n < 0 || n >= 1_000 {  // limit result and time
            err = ErrBadArgument
            return   // or raise panic
      }
      res = big.NewInt(1)
      for i := 2; i <= n; i++ {
            res = res.Mul(res, big.NewInt(int64(i)))
      }
      return
}

Listing 17-4N! Function

顺序:

fact, _ := factorial(100)
fmt.Println("Factorial(100):", fact)

产生以下输出:

Factorial(100): 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

Rat 类型具有以下功能(大多数不言自明):

  • func NewRat(a,b int 64)*大鼠

  • func (z *Rat) Abs(x *Rat) *Rat

  • func (z *Rat) Add(x,y *Rat) *Rat

  • 函数(x * Rat)Cmp(y * Rat)int–比较

  • func (x *Rat) Denom() *Int

  • func (x *Rat) Float32() (f float32,精确布尔值)

  • func (x *Rat) Float64() (f float64,精确布尔值)

  • func (x *Rat) FloatString(prec int)字符串

  • func (z *Rat) GobDecode(buf []byte)错误

  • func(x * Rat)gob encode()([]字节,错误)

  • func (z *Rat) Inv(x *Rat) *Rat

  • func (x *Rat) IsInt() bool

  • func (z *Rat) Mul(x,y *Rat) *Rat

  • 功能(z *Rat)阴性(x *Rat) *Rat

  • func (x *Rat) Num() *Int

  • func (z *Rat) Quo(x,y *Rat) *Rat

  • func (x *Rat) RatString()字符串

  • 功能(z *Rat)扫描(s fmt。ScanState,ch rune)错误

  • func (z *Rat)集合(x *Rat) *Rat

  • func(z * Rat)set float 64(f float 64)* Rat

  • func (z *Rat) SetFrac(a,b *Int) *Rat

  • func (z *Rat) SetFrac64(a,b int64) *Rat

  • func(z * rat)SETI(x * int)* rat

  • func(z * rat)SETI 64(x int 64)* rat

  • func (z Rat) SetString(s 字符串)(Rat,bool)

  • S7-1200 可编程控制器

  • func (x *Rat)符号()int

  • func (x *Rat) String()字符串

  • func (z *Rat) Sub(x,y *Rat) *Rat

cmplx包提供了这些(不言自明的)功能:

  • 功能 Abs(x complex128)浮动 64

  • 函数 Acos(x complex128) complex128

  • func Acosh(x 复杂 128)复杂 128

  • 函数 Asin(x complex128) complex128

  • func Asinh(x 复数 128)复数 128

  • 函数 Atan(x complex128) complex128

  • 函数 Atanh(x complex128) complex128

  • func Conj(x complex 128)complex 128

  • 函数 Cos(x 复数 128)复数 128

  • func Cosh(x 复杂 128)复杂 128

  • 函数成本(x 复合 128)复合 128

  • 函数表达式(x 复杂 128)复杂 128

  • func Inf() complex128

  • func IsInf(x complex128) bool

  • func IsNaN(x complex128) bool

  • 函数日志(x complex128) complex128

  • func log 10(x complex 128)complex 128

  • func NaN() complex128

  • 功能相位(x 复杂 128)浮动 64

  • 函数极坐标(x 复数 128) (r,θ float64)

  • 函数幂(x,y 复数 128)复数 128

  • func Rect(r,θ float64)复数 128

  • 函数 Sin(x 复数 128)复数 128

  • func Sinh(x 复数 128)复数 128

  • func Sqrt(x complex 128)complex 128

  • 函数 Tan(x complex128) complex128

  • func Tanh(x 复杂 128)复杂 128

rand包提供了这些类型:

  • rand–一个随机数生成器。

  • source–随机数的 63 位种子源;在所有执行中默认为相同的值,导致重复的“随机”序列;这不同于 Java 行为。

rand包提供了这些功能:

  • func exppfloat 64()float 64–获取指数分布值

  • func float 32()float 32–Get[0.0,1.0]

  • func float 64()float 64–Get[0.0,1.0]

  • func Int() int

  • func Int31() int32

  • func int 31n(n int 32)int 32–Get[0,n]

  • func Int63() int64

  • func int 63n(n int 64)int 64–Get[0,n]

  • func intn(n int)int–获取[0,n]

  • func normfloat 64()float 64–获取正态分布值

  • func Perm(n int)[]int–获取[0.0,1.0]的排列

  • func Read(p []byte) (n int,err error)–将 n 个字节读入 p

  • 函数种子(种子 int64)

  • func Shuffle(n int,swap func(i,j int))–混洗 n 个项目。通过交换关闭访问项目

  • func Uint32() uint32

  • func Uint64() uint64

请注意,不要依赖前面的生成器函数来随机生成生成器(比如通过流程开始时间)。如果需要,你必须自己做这件事。如果没有种子,Go 程序的每次执行都将重复相同的随机值序列。

Rand 类型提供了这些功能。有关解释,请参见前面的列表:

  • func New(src 源)*Rand

  • func(r * Rand)expfloat 64()float 64

  • func (r *Rand) Float32() float32

  • func (r *Rand) Float64() float64

  • func (r *Rand) Int()

  • func (r *Rand) Int31() int32

  • func(r * rand)int 31n(n int 32)int 32

  • func (r *Rand) Int63() int64

  • func(r * rand)int 63n(n int 64)int 64

  • func (r *Rand) Intn(n int) int

  • func(r * rand)standard float 64()float 64

  • func (r *Rand) Perm(n int) []int

  • func (r *Rand) Read(p []byte) (n int,err error)

  • func (r *Rand)种子(种子 int64)

  • func (r *Rand) Shuffle(n int,swap func(i,j int))

  • func (r *Rand) Uint32() uint32

  • func (r *Rand) Uint64() uint64

源类型提供以下功能:

  • func NewSource(种子 int64)源

操作系统支持包

os包以与操作系统无关的方式提供对操作系统(OS)功能的访问。这是访问这些功能的首选方式。此包包含子包:

  • exec提供以命令行方式启动外部流程的能力。

  • signal提供监视和捕获操作系统产生的信号(也称为中断)的能力。

  • user提供对操作系统用户和组帐户的访问。

path包提供了处理操作系统文件系统路径的实用函数。它有一个子包:

  • filepath提供实用函数来解析和处理操作系统文件路径。

syscall包提供了对其他包没有提供的低级操作系统功能的访问。它在某种程度上依赖于操作系统类型,因此它的功能可能不会在所有操作系统类型上工作相同。

os包有以下几种类型:

  • 文件–表示对文件的访问

  • FileInfo–表示关于文件的元数据

  • FileMode–文件访问模式(作为位标志)

  • 流程–代表外部流程

  • ProcessState–表示流程退出状态

os包有这些有用的常量:

  • PathSeparator–特定于操作系统的文件路径分隔符

  • PathListSeparator–特定于操作系统的外壳路径列表分隔符

os包具有以下有用的价值:

  • Stdin–用于/dev/stdin 的文件。

  • Stdout–标准输出文件。

  • Stderr–标准错误文件。

  • 命令行参数的Args[]string;与 Java 不同,参数 0 是启动程序的命令。

os包有这些功能。这些都是基于同名的 Unix 函数。有些函数可能无法在所有操作系统类型上工作(比如返回有用的值),尤其是 Microsoft Windows:

  • func Chdir(目录字符串)错误–更改当前目录

  • func Chmod(名称字符串,模式文件模式)错误–更改文件模式

  • func Chown(名称字符串,uid,gid int)错误–更改文件所有者

  • func Chtimes(名称字符串,一次时间。时间,时间。时间)错误–更改文件时间

  • func Clearenv()–清除流程环境

  • func Environ()[]string–获取流程环境

  • func Executable() (string,error)-获取活动程序路径

  • func Exit(代码 int)–强制退出该进程

  • func Expand(s string,mapping func(string)string)string–替换字符串中的\({var},\)vaR

  • func ExpandEnv(s string)string–使用环境替换字符串中的\({var},\)vaR

  • func Get egid()int–获取有效的组 id

  • func Getenv(密钥字符串)字符串–通过密钥获取环境值

  • func Geteuid()int–获取有效的用户 id

  • func Getgid()int–获取用户的组 id

  • func Getgroups() ([]int,error)-获取用户所属的组 id

  • func Get pagesize()int–获取虚拟内存页面大小

  • func Getpid()int–获取当前进程 id

  • func Getppid()int–获取当前进程的父进程 id

  • func Getuid()int–获取用户 id

  • func Getwd()(目录字符串,err 错误)-获取工作目录

  • func Hostname()(名称字符串,err 错误)-获取系统的主机名

  • func is exist(err error)bool–测试“exists”的错误

  • func is not exist(err error)bool–测试“不存在”的错误

  • func Is path separator(c uint 8)bool–是路径分隔符

  • func is permission(err error)bool–测试“权限问题”的错误

  • func IsTimeout(err error)bool–测试“超时”错误

  • func Lchown(名称字符串,uid,gid int)错误–更改文件/链接的所有者

  • func Link(旧名称,新名称字符串)错误–在文件之间创建硬链接

  • func lookup env(key string)(string,bool)-通过 key (name)获取环境值

  • func Mkdir(名称字符串,perm FileMode)错误–创建目录

  • func MkdirAll(路径字符串,perm FileMode)错误–创建所有需要的目录

  • func Pipe() (r *File,w *File,err error)-在文件之间创建管道

  • func Readlink(名称字符串)(字符串,错误)-读取链接

  • func Remove(名称字符串)错误–移除(删除)文件或空目录

  • func RemoveAll(路径字符串)错误–删除目录树

  • func Rename(oldpath,newpath string)错误–更改文件/目录名

  • func SameFile(fi1,fi2 FileInfo)bool–测试同一文件

  • func Setenv(键,值字符串)错误–设置环境值

  • func Symlink(旧名称,新名称字符串)错误–在名称之间创建符号链接

  • func TempDir()string–获取当前的临时目录

  • func Truncate(name string,size int64)错误–扩展/缩短文件

  • func Unsetenv(密钥字符串)错误–删除环境密钥

  • func UserCacheDir() (string,error)-获取用户缓存目录

  • func UserConfigDir()(字符串,错误)-获取用户配置目录

  • func UserHomeDir() (string,error)-获取用户主目录

文件提供以下功能:

  • func Create(名称字符串)(*File,error)-创建/截断文件

  • func Open(名称字符串)(*File,error)–以默认访问权限打开文件

  • func OpenFile(name string,flag int,perm FileMode) (*File,error)-打开一个文件

  • func (f *File) Chdir()错误–使目录 f 成为当前目录

  • func (f *File) Chmod(mode FileMode)错误–更改文件模式

  • func (f *File) Chown(uid,gid int)错误–更改文件所有者

  • func (f *File) Close()错误–关闭打开的文件

  • func(f * File)Name()string–获取文件名

  • func (f *File) Read(b []byte) (n int,err error)-从文件中的当前位置读取

  • func (f *File) ReadAt(b []byte,off int64) (n int,err error)-从文件中的某个位置读取

  • func(f * File)Readdir(n int)([]FileInfo,error)-读取目录条目

  • func(f * File)Read dirnames(n int)(names[]string,err error)-读取目录名

  • func (f *File) Seek(offset int64,where int)(ret int 64,err error)-设置当前位置

  • func (f *File) Stat() (FileInfo,error)-获取文件信息

  • func (f *File) Sync()错误–刷新挂起的更改

  • func (f *File) Truncate(size int64)错误–设置文件长度

  • func(f * File)Write(b[]byte)(n int,err error)-在当前位置写入字节

  • func (f *File) WriteAt(b []byte,off int64) (n int,err error)–在该位置写入字节

  • func(f * File)WriteString(s string)(n int,err error)-写入一个字符串

FileInfo 提供了以下功能:

  • func Lstat(名称字符串)(FileInfo,error)-获取链接/文件信息

  • func Stat(名称字符串)(FileInfo,error)-获取文件信息

FileMode 提供了以下功能:

  • func(m FileMode)IsDir()bool–Test m 代表一个目录

  • func(m FileMode)is regular()bool–Test m 表示常规文件

  • func(m FileMode)Perm()FileMode–获取文件模式

流程提供以下功能:

  • func Find Process(PID int)(* Process,error)-按进程 id 查找

  • func StartProcess(name string,argv []string,attr ProcAttr) (Process,error)-创建并启动

  • func (p *Process) Kill()错误–终止正在运行的进程

  • func (p *Process) Release()错误–如果不使用等待,则释放资源

  • func (p *Process)信号(sig 信号)错误–向进程发送信号(中断)

  • func(p * Process)Wait()(* ProcessState,error)-等待进程结束

ProcessState 提供这些函数来访问状态:

  • func (p *ProcessState)退出代码()int

  • func (p *ProcessState)已退出()bool

  • func (p *ProcessState) Pid() int

  • func (p *ProcessState)成功()bool

  • func(p * ProcessState)system time()time。持续时间

  • func(p * ProcessState)user time()time。持续时间

exec包具有以下类型和功能:

  • func LookPath(文件字符串)(字符串,错误)-在 OS 路径中查找可执行文件;返回路径

  • func 命令(名称字符串,参数...string)* Cmd–发出命令

  • func CommandContext(ctx 上下文。上下文,名称字符串,参数...string)* Cmd–根据上下文制作

Cmd 类型具有以下功能:

  • func(c * Cmd)combined output()([]byte,error)-运行并获取 stdout 和 stderr

  • func (c *Cmd) Output() ([]byte,error)-运行并获取 stdout

  • func (c *Cmd) Run()错误–运行

  • func (c *Cmd) Start()错误–开始

  • func (c *Cmd) StderrPipe() (io。ReadCloser,error)–将管道连接到 stderr

  • func (c *Cmd) StdinPipe() (io。WriteCloser,error)–将管道连接到 stdin

  • func (c *Cmd) StdoutPipe() (io。ReadCloser,error)–将管道连接到 stdout

  • func (c *Cmd) Wait()错误–等待启动的命令结束

当用户或程序要求时,操作系统可以发送“信号”(异步事件通知)。一些程序可能想要检测/拦截这些信号。Go 通过通道支持这一点,只要有信号出现,通道就会发送一条消息。

这些信号总是受支持的。其他也可能是:

var Interrupt Signal = syscall.SIGINT
var Kill      Signal = syscall.SIGKILL

signal包具有以下功能:

  • 函数忽略(符号...os。信号)–忽略信号

  • func 被忽略(签名操作系统。信号)布尔值–如果忽略,则进行测试

  • func Notify(c chan

  • 功能复位(信号...os。信号)–撤销通知操作

  • 功能停止(更改

user包允许访问用户和用户组。并非所有操作系统类型都支持这些功能。

该组提供以下功能:

  • func LookupGroup(名称字符串)(*Group,error)-按组名查找

  • func LookupGroupId(gId 字符串)(*Group,error)-按组 id 查找

用户提供这些功能:

  • func Current() (*User,error)-获取当前用户

  • 函数查找(用户名字符串)(*用户,错误)-按用户名查找

  • func LookupId(uId 字符串)(*用户,错误)-按用户 id 查找

  • func(u * User)groupid()([]string,error)-获取用户所属的组

作为使用os包的一个例子,下面是一个读取文件内容并以字符串形式返回的函数:

func ReadFile(filePath string) (text string, err error) {
      var f *os.File
      if f, err = os.Open(filePath); err != nil {
            return
      }
      defer f.Close()  // ensure closed
      var xtext []byte // accumulate result
      buffer := make([]byte, 16*1024)
      for { // read file in chunks
            n, xerr := f.Read(buffer)
            if xerr != nil {
                  if xerr == io.EOF {
                        break // EOF is OK error
                  }
                  err = xerr
                  return
            }
            if n == 0 {
                  continue
            }

            xtext = append(xtext, buffer[0:n]...)
      }
      text = string(xtext) // want as string
      return
}

由...调用

text, err := ReadFile(`.../words.txt`)
if err!=nil {
      fmt.Printf("got: %v" , err)
      return
}

fmt.Printf("testFile: %q" , text)

produces:testFile: "Now is the time to come to the aid of our countrymen!\r\n"

反射包

reflect包提供了反射(运行时类型和数据自省和/或创建)功能,允许处理任意类型的数据。它在概念上类似于java.lang.reflect包。

反射是一个复杂的主题(它可以有自己的书),详细的用法超出了本文的范围。

在 Go 中,每个离散值(不一定是每个数组、切片或 map 元素)都有一个与之相关联的运行时类型。Go 提供了在运行时查询类型的函数。它还允许在运行时动态创建和/或更改值。在大多数情况下,被查询的值被声明为一个interface{}类型,因此在运行时可以是许多不同的类型:

var x interface{}
:
fmt.Printf("%v is of type %T\n", x, x)

这会打印 x 中值的当前值和运行时类型。

通过反射通常要做的事情是测试值的类型。像fmt.Sprintf()这样的函数就是这样做的。考虑到

var values = []interface{}{0, 0.0, 0i, "", []int{}, map[int]int{},
   func() {}, }

可以按如下方式测试这些类型:

for _, v := range values {
      switch v := reflect.ValueOf(v); v.Kind() {
      case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
            fmt.Println(v.Int())
      case reflect.Float32, reflect.Float64:
            fmt.Println(v.Float())
      case reflect.String:
            fmt.Println("-", v.String() , "-")
      default:
            fmt.Printf("other type %v: %v\n", v.Kind(), v)
      }
}

这会产生以下结果:

0
0
other type complex128: (0+0i)
-  -
other type slice: []
other type map: map[]
other type func: 0x81baf0

ValueOf()方法用于将潜在的*T类型解引用为T类型(在测试类型时*TT被认为是不同的)。一个种类是值的类型的整数(枚举)形式。

reflect包有两种主要类型:

  1. type–表示运行时的类型;由reflect.TypeOf(interface{})函数返回

  2. value–表示运行时接口类型的值;由reflect.ValueOf(interface{})函数返回

要获得一个值的值,必须根据值的种类调用一个值方法。请求错误类型的值会导致恐慌。

正则表达式包

regexp包提供了一个正则表达式(重)解析器和匹配器的实现。假设读者理解 RE 的概念。Go 包文档中解释了 RE 语法和函数的细节。注意许多语言都支持 REs,但是大多数语言在工作方式上有细微的差别。Java vs. Go 就是这样。

使用这种(类重)模式,有几种重匹配的变体:

Xxxx(All)?(String)?(Submatch)?(Index)?

何时何地出现

  • 全部-匹配所有不重叠的线段

  • string–匹配一个字符串(相对于[]字节)

  • 子匹配–返回模式中每个(…)的捕获组

  • index–用输入中匹配的位置增加子匹配

它提供了以下功能和类型:

  • func Match(模式字符串,b[]字节)(匹配布尔值,err 错误)

  • func MatchReader(模式字符串,r io。RuneReader)(匹配 bool,err 错误)

  • func MatchString(模式字符串,s 字符串)(匹配布尔值,err 错误)

  • func QuoteMeta(s string)字符串–引用元字符(例如*。?s 中的+)

Regexp 类型提供了正则表达式引擎:

  • func Compile(expr string)(* Regexp,error)-Compile Go regex

  • func CompilePOSIX(表达式字符串)(*Regexp,错误)-编译 POSIX regex

  • func must Compile(str string)* Regexp–编译或死机

  • func MustCompilePOSIX(str 字符串)* Regexp–编译或死机

  • func (re *Regexp) Copy() *Regexp

  • func(re * Regexp)Expand(dst[]字节,template[]字节,src[]字节,match[]int)[]字节

  • func(re * Regexp)expand string(dst[]字节,模板字符串,src 字符串,match[]int)[]字节

  • func(re * Regexp)Find(b[]字节)[]字节

  • func(re * regexp)findall(b[]字节,n int[])字节

  • func(re * regexp)findallindex(b[]字节,n int)[]int

  • func(re * Regexp)FindAllString(s string,n int) []string

  • func(re * regexp)findall tringinindex(s string,n int)[]int

  • func(re * regexp)findalltringsubmatch(s string,n int)[]string

  • func(re * regexp)findalltringsubmatch index(s string,n int)[]int

  • func(re * regexp)findall ubmatch(b[]字节,n int[]字节)

  • func(re * regexp)findallubmatch index(b[]字节,n int)[]int

  • func(re * regexp)findindex(b[]字节)(loc []int)

  • func(re * regexp)findreaderindex(r io)。run ader(loc[]int)

  • func(re * regexp)findreaders ubmatch index(r io)。run ader[]int

  • func(re * Regexp)find string(s string)字符串

  • func(re * regexp)findstring index(s string)(loc[]int)

  • func(re *Regexp) FindStringSubmatch(s string) []string

  • func(re * regexp)findstringsubmatchindex(s string)[]int

  • func(re * Regexp)FindSubmatch(b[]byte)[][]byte

  • func(re * Regexp)FindSubmatchIndex(b[]byte)[]int

  • func (re *Regexp) LiteralPrefix()(前缀字符串,完整布尔值)-是 re 的所有前缀

  • func(re * Regexp)Longest()–修改 RE 以匹配最长的

  • func(re * regexp)match(b[]字节)bool

  • func (re *Regexp) MatchReader(r io)。-布尔

  • func(re * regexp)match string(s string)bool

  • func (re *Regexp) NumSubexp() int

  • func (re *Regexp) ReplaceAll(src,repl[]byte)[]字节

  • func(re * regexp)replace all func(src[]字节,repl func[]字节[]字节[]字节)

  • func(re * Regexp)ReplaceAllLiteral(src,repl []byte) []byte

  • func(re * Regexp)ReplaceAllLiteralString(src,repl string)字符串

  • func(re * Regexp)ReplaceAllString(src,repl string)字符串

  • func(re * regexp)replacealltringfunc(src string,repl func(string) string)字符串

  • func (re *Regexp) Split(s string,n int)[]字符串

  • func(re * regexp)subexpindex(name string)int

  • func(re * Regexp)sub expnames()[]string

运行时包

runtime包包含公开 Go 运行时系统的函数。它在角色上类似于java.lang.Systemjava.lang.Runtime类型。runtime包有几个不在本文中介绍的子包。

运行时包具有以下功能:

  • func Caller(skip int) (pc uintptr,file string,line int,ok bool)-获取呼叫者信息

  • func GC()–运行垃圾收集

  • func GOMAXPROCS(n int)int–设置 goroutine 处理器的数量

  • func GOROOT()字符串–获取 Go 安装根目录

  • func go Exit()–退出调用的 goroutine

  • func go sched()–运行另一个就绪的 goroutine

  • func num CPU()int–获取 CPU 内核的数量

  • func NumGoroutine()int–获取活动 Goroutine 计数

  • func Set finalizer(obj interface { },finalizer interface { })–为对象设置终结器 func

  • func Version()字符串–获取 Go 版本

与 Java 中每个对象都有一个finalize方法不同,大多数 Go 数据没有关联的终结器。如果您有一个值需要终结(例如,垃圾收集时的资源清理),您应该对它使用SetFinalizer函数,可能是在数据类型的构造函数中。

字符串处理包

strconv提供了与string类型相互转换(即解析和格式化)的转换函数。fmt包也经常被用来格式化值。

strconv包有这个关键常量:

const IntSizeint(以及【?? 和指针】)类型的位大小;这可能因硬件架构(32/64 位)而异。

strconv具有以下功能:

  • func append bool(dst[]字节,b bool[]-将布尔值附加到 dst

  • func AppendFloat(dst []byte,f float64,fmt byte,prec,bitSize int)[]byte–向 dst 追加一个浮点数

  • func AppendInt(dst []byte,i int64,base int)[]byte–向 dst 追加一个有符号整数

  • func AppendQuote(dst []byte,s string)[]byte–将带引号的 s 追加到 dst

  • func AppendQuoteRune(dst []byte,r rune)[]byte–将带引号的 r 追加到 dst

  • func append uint(dst[]字节、uint64 字节、base int[]-将未签名的整数附加到 dst

  • 函数 Atoi(s string) (int,error)-将字符串解析为整数

  • func FormatBool(b bool)字符串 boolean to string

  • func FormatComplex(c complex128,fmt byte,prec,bitSize int)string–复杂到字符串

  • func FormatFloat(f float64,fmt byte,prec,bitSize int)string–浮点到字符串

  • func FormatInt(i int64,base int)string–有符号整数到 base 中的字符串

  • func format int(I uint 64,base int)string–无符号整数到 base 中的字符串

  • func is graphic(r rune)bool–如果是 Unicode 图形字符,则为 True

  • func is print(r rune)bool–如果是可打印字符,则为 True

  • function Itoa(I int)string–以 10 为基数的整数到字符串

  • func ParseBool(str string) (bool,error)-将字符串解析为布尔值

  • func ParseComplex(s string,bitSize int) (complex128,error)-将字符串解析为复杂的

  • func ParseFloat(s string,bitSize int) (float64,error)-将字符串解析为浮点型

  • func ParseInt(s string,base int,bitSize int) (i int64,err error)-将字符串解析为有符号整数

  • func ParseUint(s string,base int,bitSize int) (uint64,error)-将字符串解析为无符号整数

  • 函数引号(s 字符串)-如果需要,用转义符将字符串括在引号中

  • 字符串——如果需要的话,用引号将符文括起来

  • func Unquote(s string) (string,error)-删除引号和转义符

strings包提供了函数和类型来简化字符串的处理。注意,在 Go 中,像在 Java 中一样,字符串是不可变的,所以所有这些函数都返回新的,而不是修改过的字符串。在 Java 中,大多数函数都是StringStringBuilder/Buffer类型的方法:

  • func Compare(a,b string)int–比较 a 和 b,返回-1,0,1。替代,> =

  • func Contains(s,substr string)bool–如果 s 中的字符串

  • func ContainsAny(s,chars string)bool–如果 chars 中的任何字符在 s 中,则为 True

  • func ContainsRune(s string,r rune)bool–如果 r 在 s 中,则为 True

  • func Count(s,substr string)int–s 中 substr 的计数

  • func EqualFold(s,t string)bool–Unicode 折叠后,如果 s == t,则为 True

  • 函数字段(字符串)[]字符串–在空白处分割

  • func FieldsFunc(s string,f func(rune)bool)[]string–s 在字符处拆分,其中 f 返回 true

  • func HasPrefix(s,前缀字符串)bool–如果 s 以前缀开头,则为 True

  • func HasSuffix(s,后缀字符串)bool–如果 s 以后缀结尾,则为 True

  • func Index(s,substr string)int –> = 0(如果 s 中有 substr)

  • func IndexAny(s,chars string)int –> = 0,如果 s 中的 chars 中有任何 char

  • func IndexByte(s string,c byte)int –> = 0(如果 c 在 s 中)

  • func 索引 unc(s 字符串,f func(rune)bool)int-> 0 if f true on any char

  • func IndexRune(s string,r rune)int –> = 0(如果 r 在 s 中)

  • func Join(elems []string,sep string)string–用 sep between 连接 elems 中的项目

  • func LastIndex(s,substr string)int –> = 0(如果 substr 在 s 中,从末尾算起)

  • func LastIndexAny(s,chars string)int –> = 0,如果 s 中的 chars 从末尾开始

  • func LastIndexByte(s string,c byte)int –> 0(如果 c 在 s 中,从末尾算起)

  • func LastIndexFunc(s string,f func(rune)bool)int –> = 0 如果 f 对 s 中的任何字符为 true,则从 end 开始

  • func Map(mapping func(rune) rune,s string)string–s 的字符被映射结果替换,或者如果 rune < 0,则被删除

  • func Repeat(s string,count int)string–的重复计数次数

  • func Replace(s,old,new string,n int)string–s 中出现的旧字符串最多被新字符串替换 n 次

  • func ReplaceAll(s,old,new string)string–中所有出现的旧字符串都被替换为新字符串

  • func Split(s,sep string)[]string–出现 sep 时的 Split

  • func SplitAfter(s,sep string)[]string–sep 出现后的 s split

  • func SplitAfterN(s,sep string,n int)[]string–在 sep 出现 n 次后进行拆分

  • func SplitN(s,sep string,n int)[]string–在 sep 出现 n 次时进行拆分

  • func Title(s string)string–将每个单词的首字母转换为标题大小写

  • func to lower(s string)string–转换为全部小写

  • func ToTitle(s 字符串)字符串–转换为所有标题大小写

  • func ToUpper(s string)字符串–转换为全大写

  • func Trim(s,割集字符串)字符串–删除割集中的前导/尾随字符

  • func TrimFunc(s string,f func(rune)bool)string–在 f 为真的情况下删除 s 前导/尾随字符

  • func TrimLeft(s,割集字符串)字符串–删除割集中的前导字符

  • func TrimLeftFunc(s string,f func(rune)bool)string–删除 f 为真时的 s 前导字符

  • func TrimPrefix(s,前缀字符串)字符串–从 s 中删除任何前缀

  • func TrimRight(s,割集字符串)字符串–删除割集中的尾随字符

  • func TrimRightFunc(s string,f func(rune)bool)string–删除 f 为真的 s 尾随字符

  • func TrimSpace(s string)字符串–删除 s 前导/尾随空格

  • func TrimSuffix(s,后缀字符串)字符串–从 s 中删除任何后缀

类型构建器——用于构建字符串(像 Java 的 StringBuilder

type Reader–用于从作为源的字符串中读取文本

构建器类型有以下方法:

  • func(b * Builder)Cap()int–当前的 Builder 容量

  • func(b * Builder)Grow(n int)–添加到构建器容量

  • func(b * Builder)Len()int–当前内容长度

  • func(b * Builder)Reset()–将长度设置为 0

  • func(b * Builder)Write(p[]byte)(int,error)-Add bytes

  • func (b *Builder) WriteByte(c byte)错误–添加一个字节

  • func(b * Builder)write Rune(r rune)(int,error)-添加一个 rune

  • func(b * Builder)WriteString(s string)(int,error)-添加一个字符串

读取器类型有以下方法:

  • func new Reader(s string)* Reader–在字符串上创建一个读取器

  • func(r * Reader)Len()int–获取未读计数

  • func(r * Reader)Read(b[]byte)(n int,err error)–将最多 n 个字节读入 b

  • func (r *Reader) ReadAt(b []byte,off int64) (n int,err error)-在位置读取

  • func (r *Reader) ReadByte() (byte,error)-读取一个字节

  • func(r * Reader)Read Rune()(ch rune,size int,err error)-读取一个 rune

  • func (r *Reader)复位(s 字符串)–设置为开始

  • func (r *Reader) Seek(offset int64,where int)(int 64,error)-设置为 position

  • func(r * Reader)Size()int 64–获取原始(总)长度

  • func (r *Reader) UnreadByte()错误–反向读取

  • func(r * Reader)un readune()错误–反向读取

  • func (r *Reader) WriteTo(w io。Writer) (n int64,err error)-复制到 Writer

并发和 Goroutines

sync包提供了互斥函数等 goroutine 同步支持。这通常用来替代 Java 中的synchronized语句和 select 方法。它的功能类似于java.util.concurrent.locks包。atomic子包提供了对某些数据类型的原子访问。它类似于java.util.concurrent.atomic包。Go 社区提供了更多的并发类型和序列化函数。

sync包提供了这些类型:

  • cond–提供一个条件变量;像 Java 的Object.wait/notify{All}对。

  • map——提供类似 Java ConcurrentHashMap的行为。

  • 互斥体-提供对共享值的访问控制;参见java.util.concurrent包装。

  • rw Mutex——具有多个并发阅读器的互斥体;参见java.util.concurrent包装。

  • once–代码只阻塞一次;对于创建单例非常有用。

  • pool——类似于相同类型值的缓存;成员可以被自动删除。

  • wait group–用于等待多个 goroutines 退出(就像 Java 中的Thread.join())。

注意,synchronized块可以被同一个线程重入,但不能被其他线程重入。Go 没有提供这种行为的预定义库;Go 锁将阻塞拥有它们的同一个 goroutine(死锁)。

前面的许多类型在 Java 中没有直接的相似之处,但是它们通常是近似的。通常,反过来也是正确的;许多 Java 并发函数可以在 Go 中轻松模拟。例如, Once 类型可以在 Java 中模拟为

@FunctionalInterface
public interface Onceable {
   void doOnce(Runnable r);
}
public class Oncer implements Onceable {
   private AtomicBoolean once = new AtomicBoolean(false);
   public void doOnce(Runnable r) {
     if(!once.getAndSet(true)) {
       r.run();
     }
   }
}

这被用作

Onceable oncer = new Oncer();
  for(var i = 0; i < N; i++) {
    oncer.doOnce(()->System.out.println("Hello World!"));
 }
}

在 Go 中,这是这样完成的:

var once sync.Once
for i := 0; i < N; i++ {
      once.Do(func(){
            fmt.Println("Hello World!");
      })
}

给定:

type Locker interface {
      Lock()
      Unlock()
}

Cond 类型有以下方法:

  • func newcomb(l 宽松)* cond 使伯爵

  • func(c * Cond)Broadcast()–LikeObject.notifyAll

  • func (c *Cond)信号()–如Object.notify

  • func(c * Cond)Wait()–就像Object.wait

(并发)映射类型有这些方法(通常是不言自明的——load = > get;store => put):

  • func (m *Map)删除(关键接口{})

  • func (m *Map)加载(键接口{})(值接口{},ok bool)

  • func (m *Map) LoadAndDelete(键接口{})(值接口{},loaded bool)

  • func (m *Map) LoadOrStore(key,value interface{})(实际接口{},加载的 bool)

  • func (m *Map) Range(f func(key,value interface { })bool)-用于键范围

  • func (m *Map)存储(键,值接口{})

互斥类型有这些方法(通常不言自明),因此是一个Locker:

  • func (m *Mutex)锁()

  • func (m *Mutex)解锁()

RWMutex 类型有这些方法(通常不言自明),因此是一个Locker:

  • func (rw *RWMutex)锁()

  • func (rw *RWMutex) RLock()

  • func (rw *RWMutex) RLocker()锁定程序

  • func(rw * rmutex)runlock()程式码

  • func (rw *RWMutex) Unlock()

Once 类型有这些方法(通常不言自明):

  • func(o * Once)Do(f func())–f 只调用一次

池类型有这些方法(通常不言自明):

  • func(p * Pool)Get()interface { }–获取和移除任意实例(所有实例都应该是从New返回的类型)

  • func(p * Pool)Put(x interface { })–(重新)添加一个实例

  • Pool 有一个成员函数值New,用于在找不到条目时创建条目。

WaitGroup 类型有这些方法(通常不言自明):

  • func (wg *WaitGroup) Add(delta int)

  • func(WG * wait group)Done()–与 Add(-1)相同

  • func(WG * Wait group)Wait()–等待计数变为 0

测试包

本节是第十七章“Go 单元测试和基准测试”的浓缩。

Java 本身没有内置的测试框架,但是有很好的社区测试框架。许多作者使用类的main方法作为测试用例。因为在 Go 中创建一个main函数开销更大(不像在 Java 中那样每个类都有),所以这种创建测试的方法在 Go 中并不常用。类似地,由于一个包可以有许多init()函数,它们可以用来方便地包含测试用例,但是它们必须在代码中手动启用/禁用。

Go testing包提供了对 Go 代码进行类似于 JUnit 2 的可重复测试的支持和框架。这个包是反射驱动的,有许多类型用于运行测试套件和基准,通常不被测试人员直接使用。

在 Go 中,测试套件是任何 Go 源文件,其名称的形式为 xxxx _test.go(其中 xxxx 是套件名称),包含一个或多个测试函数(也称为测试用例)。测试代码通常与被测试的代码 (CUT)放在同一个包中,所以它可以访问非公共的名字。这些类型的测试被称为“白盒” 3 测试。通过将测试代码放在与 CUT 不同的包中,可以进行“黑盒” 4 测试。

测试函数具有以下形式:

func TestXxx(*testing.T) {
      :
}

其中 Xxx 是测试用例的名称。在一个测试套件中可以有任意数量的这样的测试用例。“go test”命令将运行它们和 CUT 并报告结果。注意,测试套件没有main功能。测试用例通过T参数与测试运行程序交互。

测试的典型结构如下

func TestSomething(t *testing.T) {
      got := <run some test>
            want := <expected value>
      if got != want {
            t.Errorf("unexpected result %v (vs. %v)", got, want)
      }
}

像测试用例一样,基准具有一致的形式:

func BenchmarkXxx(*testing.B) {
      :
}

其中 Xxx 是基准的名称。在一个测试套件中可以有任意数量的这样的基准。带有基准选项的“go test”命令(默认情况下是关闭的,因为基准通常需要很长的运行时间)将运行它们并报告结果。基准通过B参数与基准运行程序交互。

基准的典型结构如下

func BenchmarkSomething(b *testing.B) {
      : do some setup which may take time
      b.ResetTimer()
      for i := 0; i < b.N; i++ {
            : some code to time
      }
}

基准跑步者将确定一个好的N值(通常相当大)来使用。因此,运行基准测试会花费很多时间,可能不应该在每次构建时都这样做。

关键类型是 B 和 T:

  • b–基准函数的上下文/助手

  • benchmark result–将基准结果作为字段的结构

  • PB–支持并行运行基准测试

  • 测试用例功能的上下文/助手

  • TB–T 型和 B 型方法

测试包具有以下功能:

  • func AllocsPerRun(runs int,f func())(avg float 64)-获取 f 的每次调用的平均分配

  • func Short()bool–报告短选项

  • func Verbose()bool–报告详细选项

B(基准)类型具有以下功能:

  • func(c * B)clean up(f func())–在基准测试之后调用 f 进行清理

  • func (c *B)错误(参数...接口{ })–记录然后失败

  • func (c *B) Errorf(格式字符串,参数...接口{ })–格式化日志,然后失败

  • func(c * B)Fail()–标记失败

  • func(c * B)Fail now()–失败并退出

  • func(c * B)Failed()bool–测试失败

  • func (c *B)致命(参数...接口{ })–现在登录然后失败

  • func (c *B) Fatalf(格式字符串,参数...接口{ })–格式化日志,然后立即失败

  • func(c * B)Helper()–将调用者标记为帮助者(不跟踪)

  • 函数(c *B)日志(参数...接口{ })–日志值

  • func (c *B) Logf(格式字符串,参数...接口{ })–格式化日志

  • func(c * B)Name()string–获取基准名称

  • func(B * B)ReportAllocs()–启用分配跟踪

  • func (b *B) ReportMetric(n float64,单位字符串)–设置报告比例

  • func(B * B)Reset timer()–重置基准计时器和计数

  • func (b *B) Run(name string,f func(B * B))bool–按顺序运行基准

  • func(B * B)Run parallel(body func(* PB))–并发运行基准

  • func (c *B)跳过(参数...接口{ })–跳过基准和日志

  • func(c * B)Skip now()–立即跳过并停止

  • func (c *B) Skipf(格式字符串,参数...接口{ })–跳过基准和格式化日志

  • func (c *B)跳过()bool–跳过测试

  • func (b *B)启动计时器()-启动计时

  • func(B * B)Stop timer()–停止计时

  • func (c *B) TempDir()字符串–获取临时目录

键入基准结果

  • func 基准(f func(b *B))基准结果–基准 f

  • func (r 基准结果)allocdistop()int 64–获取信息

  • func(r benchmark result)AllocsPerOp()int 64–获取信息

  • func(r benchmark result)MemString()string–获取信息

  • func (r 基准测试结果)NSP()int 64–获取信息

T(测试)型具有这些功能。许多与 B 型相同,不再赘述:

  • 函数(c *T)清理(f func())

  • func (t *T) Deadline()(截止时间。时间,ok bool)-获取测试截止日期

  • func (c *T)错误(参数...界面{})

  • func (c *T) Errorf(字符串格式,args-我...。接口{})

  • func (c *T)失败()

  • func(c * t)fain low()

  • func (c *T)失败()布尔值

  • func (c *T)致命(参数...界面{})

  • func (c *T) Fatalf(格式字符串,参数...界面{})

  • func(c * T)Helper()–将调用方标记为帮助方;它不包含在报告中

  • 函数(c *T)日志(参数...界面{})

  • func (c *T) Logf(格式字符串,参数...界面{})

  • func (c *T) Name()字符串

  • func(T * T)Parallel()–设置为与其他测试并行运行一个测试

  • func (t *T) Run(名称字符串,f func(t *T)) bool

  • func (c *T)跳过(参数...界面{})

  • func (c *T) SkipNow()

  • func (c *T) Skipf(格式字符串,参数...界面{})

  • func (c *T)跳过()布尔值

  • func (c *T) TempDir()字符串

时间和日期包

time包提供了显示和操作日期、时间和持续时间的功能。它有一个子包:

  • tzdata 提供对时区的支持,不依赖于操作系统的支持。

time包内置了这些时间格式(实际上是模板——实际值的格式看起来像模板):

  • ANSIC = "Mon Jan _2 15:04:05 2006"

  • UnixDate = "Mon Jan _2 15:04:05 MST 2006"

  • RubyDate = "Mon Jan 02 15:04:05 -0700 2006"

  • RFC822 = "02 Jan 06 15:04 MST"

  • RFC822Z = "02 Jan 06 15:04 -0700"

  • RFC850 = "Monday, 02-Jan-06 15:04:05 MST"

  • RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"

  • RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700"

  • RFC3339 = "2006-01-02T15:04:05Z07:00"

  • RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"

  • Kitchen = "3:04PM"

  • Stamp = "Jan _2 15:04:05"

  • StampMilli = "Jan _2 15:04:05.000"

  • StampMicro = "Jan _2 15:04:05.000000"

  • StampNano = "Jan _2 15:04:05.000000000"

注意,前面的模式不是任意的。这些值(如月份名称/缩写和日期)是关键字。例如,不能用五月代替一月,也不能用 03 代替 02。

时间以纳秒为单位进行测量,但计算机可能无法以此分辨率测量时间流逝,因此时间步长可能以多个纳秒为单位。time包内置了这些持续时间:

  • Nanosecond Duration = 1

  • Microsecond = 1000 * Nanosecond

  • Millisecond = 1000 * Microsecond

  • Second = 1000 * Millisecond

  • Minute = 60 * Second

  • Hour = 60 * Minute

time包具有以下功能和类型:

  • func After(d Duration)

  • func Sleep(d Duration)–在一段时间内睡眠/暂停调用程序

  • func Tick(d Duration)

  • 类型持续时间–代表一段时间

  • 类型位置–代表一个时区

  • 键入 Month–枚举月份

  • 类型 Ticker——包装获取 ticks 的通道;用来重复做动作

  • type Time–表示以纳秒为分辨率的时间瞬间

  • 类型 Timer–定期触发一个通道

  • 键入 Weekday–Enum 表示工作日

持续时间类型有以下方法(通常不言自明):

  • func ParseDuration(s 字符串)(持续时间,错误)

  • func 自(t 时间)持续时间

  • func Until(t 时间)持续时间

  • 功能(持续时间)小时()浮动 64

  • func (d 持续时间)微秒()int64

  • func (d Duration)毫秒()int64

  • func (d Duration)分钟()float64

  • func (d Duration)纳秒()int64

  • func (d 持续时间)Round(m 持续时间)持续时间

  • func (d 持续时间)秒()浮点 64

  • func (d 持续时间)Truncate(m 持续时间)持续时间

类型位置

  • func FixedZone(名称字符串,偏移量整数)*位置

  • func LoadLocation(名称字符串)(*位置,错误)

键入 Ticker

  • func new Ticker(d Duration)* Ticker–制作并启动一个 Ticker

  • func (t *Ticker)重置(d 持续时间)-更改间隔

  • 函数(t *Ticker)停止()

键入时间

  • 函数日期(年整数、月整数、日、小时、分钟、秒、秒整数、位置*位置)时间

  • func Now()时间

  • func Parse(布局,值字符串)(时间,错误)

  • func ParseInLocation(布局,值字符串,位置*位置)(时间,错误)

  • func Unix(sec int64,nsec int64)时间–从纪元开始的时间

  • 功能(时间)相加(持续时间)时间

  • func (t Time) AddDate(年整数,月整数,日整数)时间

  • (u Time) bool 后的 func (t Time)

  • func (t Time) Before(u Time) bool

  • func (t Time) Clock()(小时,分钟,秒整)

  • func (t Time) Date()(年整数,月整数,日整数)

  • func (t Time) Day() int

  • func (t 时间)等于(u 时间)bool

  • func (t Time)格式(布局字符串)字符串

  • func (t Time) Hour() int

  • func (t Time) ISOWeek() (year,week int)

  • 功能(时间)在(位置*位置)时间内

  • func(t time)ice ero()bool

  • func (t 时间)Local()时间

  • func (t Time) Location() *Location

  • func (t Time) Minute() int

  • 函数(时间)月()月

  • func (t Time)纳秒()整数

  • func (t 时间)Round(d 持续时间)时间

  • func (t Time) Second() int

  • 功能(时间)子(时间)持续时间

  • func (t 时间)Truncate(d 持续时间)时间

  • func (t 时间)UTC()时间

  • func (t Time) Unix() int64

  • func(t Time)UnixNano()int 64–自纪元以来的时间

  • func (t Time) Weekday()

  • func (t Time) Year() int

  • func (t Time) YearDay() int

  • func (t Time) Zone() (name string,offset int)

类型计时器

  • func AfterFunc(d Duration,f func())* Timer–在持续时间之后调用 goroutine 中的 func

  • func new Timer(d Duration)* Timer–制作计时器

  • func(t * timer)reset(d duration)bool

  • func (t *Timer) Stop() bool

在 Ticker vs. Timer 上,Ticker 定期提供多个事件,而 Timer 只提供一个事件。两者都与持续时间有关。考虑这个简单版本的时钟,它每分钟输出一天中的时间:

func startClock(minutes int) {
      minuteTicker := time.NewTicker(time.Minute)
      defer minuteTicker.Stop() // cleanup ticker when done
      fmt.Println("Clock running...")
      complete := make(chan bool) // notifies end
      go func() { // trigger the clock to stop eventually
            time.Sleep(time.Duration(minutes) * time.Minute)
            complete <- true
      }()
      count := 0
loop:
      for {
            select { // blocks while waiting for an event
            // ticker has a channel that fires every minute
            case tod := <-minuteTicker.C:
                  fmt.Println(tod.Format(time.RFC850))
                  count++
            case <-complete:
                  break loop
            }
      }
      fmt.Println("Clock stopped; final count:", count)
}

在某些main中使用 startClock(5)运行,结果如下:

Clock running...
Friday, 07-May-21 14:16:07 PDT
Friday, 07-May-21 14:17:07 PDT
Friday, 07-May-21 14:18:07 PDT
Friday, 07-May-21 14:19:07 PDT
Friday, 07-May-21 14:20:07 PDT
Clock stopped; final count: 5

相比之下,计时器会在完成后触发一个功能。定时器由Tick()功能创建。timer()函数在超时前返回:

func timer(seconds int, f func(t time.Time)) {
      ticks := time.Tick(time.Duration(seconds) * time.Second)
      go func() {
            // only iterates once as only one value sent before closure
            for t := range ticks {
                  f(t)
            }
      }()
}

这由main中的以下代码驱动:

var wg sync.WaitGroup
wg.Add(1)
start := time.Now()
fmt.Println("Running...")
timer(5, func(t time.Time) {
      defer wg.Done()
      trigger := time.Now()
      fmt.Println("Trigger difference:", trigger.Sub(start))
})
wg.Wait()
fmt.Println("Done")

产生以下输出:

Running...
Trigger difference: 5.0041177s
Done

第一部分:看一看 Go

First Look at Go

欢迎,未来的地鼠!为发现和丰富的旅程做好准备。

这本书帮助你学习 Go (又名 Golang 1 )编程语言和许多 Go 标准库,以便成为成功的地鼠。本文假设读者没有 Go 知识,面向有经验的 Java 程序员使用。这本书通常使用 Java 特性之间的比较以及这些相似的特性是否/如何存在于 Go 中来组织。

本书假设读者已经了解 Java 它不是 Java 教程。本书还假设读者在基本编程概念和过程编程技术方面有基本的能力。

因为这是任何编程语言的普遍情况,所以几乎不可能以严格的线性顺序介绍语言主题/特性(在使用任何主题之前,对所有主题进行充分描述)。大多数语言都依赖于它们的特性,以至于不可能做到这一点。Go 也不例外;事实上,特性之间的这种相互依赖是 Go 设计的一个关键方面。

这本书没有做到如此完美的主题顺序。主题有时会在完全描述之前被提及。我们将在参考点提供一些背景信息,但有时可能需要跳到后面来浏览更详细的演示。这种排序会导致整本书内容的有限重复。有限的重复也在整个文本中使用,以帮助加强关键概念。

通过比较和举例来学习是强大而有效的技术。在本书中,我们将比较 Go 和它的一些标准库与 Java 和它的一些标准库来教授基本的 Go 编程。这本书经常用例子来说明相似和不同之处。这是本书的主要目的。

这本书不会涵盖 Go 语言的每一个细节或选项,但是它的大部分特性都有描述,或者至少在例子中有展示。当然,Go 将会被描述的非常详细。Go 语言规范可以用来获得完整的描述。许多标准的 Go 库也是如此。

大部分参考和比较都会针对 Java。但是,由于 Go 语言和运行时主要针对 C 语言和 C 标准库的用例,所以这本书有时也会将 Go 与 C 进行比较。由于 C++是 C 的派生物和超集,有时这本书也可能将 Go 与 C++相比较。在任何情况下,C 或 C++知识都不是有效使用这本书的先决条件。偶尔,这本书会比较其他语言的 Go。

虽然术语“Go”通常意味着,Go 不仅仅是一种语言、一组运行时库和一组开发人员工具。“Go”也代表了一个用户和贡献者的社区。与 Java 一样,这个社区是标准 Go 功能之外的丰富功能来源,也是 Go 开发人员培训和支持的巨大来源。通过 Go 工具链和像 GitHub 这样的库,可以很容易地访问这些扩展。

Go 于 2009 年 11 月首次公布。1.0 版本于 2012 年 9 月发布。在 1.0 版本之前,语言和运行时经常变化,有时甚至不兼容。1.0 以后,稳定多了。

1.0 以后的每个版本都有一个完全向后兼容的目标(所有较旧的源程序在重新编译后继续编译和运行),但也有一些例外。Go 1.16 中 Go 1.16 模块环境选项的缺省值从auto变为on就是一个例子。随着时间的推移,这种版本与版本之间的不兼容性正在迅速减少。

截至本书出版之日,Go 已有十几个专业(1 的 XX。XX)发布和众多点(yy 的 1。XX.yy)版本。每个主要版本都引入了新的工具、语言和/或库特性、性能改进,通常还会修复一些错误。在一本书里详细描述它们是一种有计划的废弃形式,因此不会被采用。详细的总结可以在 https://golang.org/doc/devel/release.html 找到。

在开始学习 Go 之前,我们先来看看 Go 没有提供的 Java 语言的一些关键特性。从表面上看,这可能会使 Go 与 Java 相比显得逊色,但当你进一步深入文本时,作者相信你会发现事实并非如此。

本文的第一部分有一些 Go 代码示例;大部分是背景资料。这将在本文的后续部分有所改变。

注意在正文中,特别是在顶点部分,提到了源文件名。通常,这些名称不是操作系统文件名,而是以不同的形式显示。一些操作系统要求输入的文件名大小写完全一致,而其他操作系统则不要求。

第二部分:Go 语言

The Go Language

在这一部分,将介绍 Go 语言的一些关键和基本特性。当我们完成这一部分时,您应该能够描述使 Go 不同于 Java 的关键特性。

下面的章节提到了一些还没有很好描述的 Go 主题。您可能需要继续阅读第五章“基本功能”,以获得全面掌握本部分所有内容所需的基础知识。

这一章和本文的其余章节有大量的代码示例。一般来说,可运行代码用列表标题来标记。这段代码可能不是完全独立的(如字面所示是可编译和可执行的),因为它通常需要一个 package 语句和一些添加的导入,以及在main()函数中的包装,如本文后面所述。

第三部分:Go 库调查

Go Library Survey

这一部分将简要介绍标准的 Go 库。它旨在让您了解和熟悉这些库以及它们能做什么,而不是详细介绍它们的用途。并非每个库中的所有类型和函数都会被提及,只有一些更普遍有用的(有些固执己见的选择)会被提及。许多函数都是不言自明的(它们的名字本质上暗示了它们的功能),因此可能不再进一步描述。对于其他功能,只做简要说明。一些类型和/或功能将包括示例。

我们将从 Go 标准包库的一个简短的调查开始。它旨在让读者了解并熟悉 Go 运行时函数。它并不打算成为每个库函数的完整教程或参考,但对于许多库来说,它提供了足够的细节来开始成功地使用它们。有关每个库的更多信息,请参见在线 Go 文档(位于 www.golang.org/pkg )。我们将只讨论标准的 Go 库。

像 Java 一样,Go 既有标准(包含在 Go install 中)库,也有第三方(也就是社区提供的)库。在作者看来,Java 标准库通常在本质上更全面,但是 Go 库足够丰富,可以编写许多有用的程序。此外,Go 版本通常更容易学习和使用,至少在最初是这样。

讨论所有社区提供的 Go 库,比如 Java 社区库,将是一项超人的任务,会产生成千上万的页面。在一本书里也不可能与这些库保持同步。因此,本文不打算这样做。

在某些情况下,Go 标准库具有 Java 标准库所没有的功能。例如,Go 内置了创建生产服务器的支持,尤其是 HTTP 服务器。在 Java 中,这通常需要额外的库,比如来自Apache?? 的库。org 或者 Spring 。org 。所以标准 Go 类似于一个简化功能 Java 扩展版(JEE——现在被称为 Jakarta 扩展版 1 )的环境。

与 Java 相比,Go 的一个非常薄弱的地方是本机 GUI。Java 有 AWTSwingJavaFX GUI 库,等等。Go 没有类似的东西,但是 Go 有一些库可以帮助 Java 在 JEE 中包含的 web GUIs 生成 HTML(或者 CSS 或者 JSON)。一些 Go 社区成员提供了本地 GUI 支持,但似乎没有一个是完全跨操作系统平台的。此外,许多不是纯粹的 Go,需要 CGo 访问本机操作系统库。

在逐个函数的基础上比较这些库可能需要数百页(如果不是数千页的话)。类似地,仅仅为程序员提供 Go 库中所有函数的参考就需要数百页。本书不会试图这样做。在线 Go 文档可用于此目的。

注意,Go 库被安排在包中,所以在这个上下文中,包和库通常是同义词(但是有些库可能有多个包)。

库调查将由 Go 中的可用内容驱动。因此,有些 Java 库可能根本不包含在内。一些 Go 库不会被调查,因为它们不经常使用,或者不打算用于高级或内部(比如 Go 构建系统或运行时)使用。

这本书将比较一些关键的库,并提供一些他们提供的大多数程序员会使用的函数的例子。这基本上是将java.langjava.util(及其子包)包中的一些类和函数与它们的 Go 等价物进行比较。还会提到其他 Java 包中的一些类和函数。

一些 Go 库是非常低级的(例如,不安全的内存访问或直接操作系统访问),通常由更高级别的库包装;将只讨论更高级别的库。一些 Go 库处理 Go 运行时和工具集的实现,这里就不讨论了。一些 Go 库处理低级调试和跟踪,它们将不被讨论。

一些 Go 库处理跨进程和网络的远程过程调用 (RPC ),这里就不讨论了。RPC 现在不如 HTTP 访问使用得频繁,HTTP 访问将被包括在内。同样,Go 的邮件支持就不讨论了。

这里没有列出所描述的包中的所有类型和功能。请参阅完整的联机文档。这个站点描述了标准包和一些补充包(其中一些是实验性的),并提供了一些到第三方包的链接。场地( https://golang.org/pkg/ )如图 P3-1 (标准库隐藏)。

img/516433_1_En_3_Fig1_HTML.jpg

图 P3-1

Go 扩展包摘要

  • Pkg.go.dev 多为搜索引擎;你需要提供关键词。

  • https://github.com/golang/go/wiki/Projects 是 Go 搜索引擎的目录,也是跨多个领域的项目列表。这是一个寻找社区库贡献的好来源。

标准库包含许多包;一些总结如下。标准库的许多类型和功能在社区产品中都有增强版(通常是替换版)。一些社区产品完全偏离了标准的库 API 模式,并且有不同的方法来提供特性。在承诺使用标准库 API 之前,建议对社区备选库进行一次调查(比如 web 搜索)。通常,它们更丰富,功能更强。

这种现象在 Java 中不太普遍,大多数开发人员使用标准库来实现他们提供的任何功能。一个明显的例外是标准的 HTTP 客户端,它可能很难使用并且功能有限。因此,它存在许多社区增强。注 Java 11 提供了一个替代的 HTTP 客户端,它有了很大的改进,可能会淘汰一些社区产品。

下面没有列出所有的类型和类型的函数/方法,只列出了最常用的。许多包都有常量和变量的定义。仅列出这些常用值。如果没有为下面的类型定义构造函数(NewXxx)方法,请使用零值声明。

包变量通常包含标准化的error类型,这些类型可以用来与包中方法返回的错误进行比较。这些值的使用类似于 Java 的标准异常类型(例如,IllegalArgumentExceptionArrayIndexOutOfBoundsException)。例如,zip包定义了这些错误:

var (
      ErrFormat    = errors.New("zip: not a valid zip file")
      ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
      ErrChecksum  = errors.New("zip: checksum error")
)

请注意,这为代码中任何类似的错误设置了一种命名模式,并为错误消息文本设置了一种可能的样式。

下面列出的许多类型,尤其是结构类型,都实现了Stringer接口。这在以下方法集中没有记录。

Go 标准包被安排在一个浅层次结构中。一些通用功能被分组在父包下,而更多的特定功能被分组在子包下。

虽然标准层次结构中的软件包会随着时间的推移而增加,但下面列出了一些具有代表性的软件包,并简要描述了它们的用途:

  • 归档–空,参见嵌套

  • 归档/TAR–TAR 读取权限

  • 归档/ZIP–ZIP 读/写访问

  • bufio–通过低级无缓冲 I/O 功能提供缓冲 I/O

  • 内置——描述 Go 内置类型和标识符

  • 字节–处理字节片的函数

  • 压缩–空,参见嵌套

  • 压缩/BZIP–BZIP 2 解压缩

  • 压缩/压缩–处理压缩格式数据

  • 压缩/gzip–处理 gzip 格式数据

  • 压缩/lzw–处理莱姆佩尔-齐夫-韦尔奇格式数据

  • compress/zlob–处理 zlib 格式的数据

  • 容器-空,参见嵌套

  • 容器/堆–处理堆(树,其中每个节点是任何子树的最小值,最小值在顶部)

  • 容器/列表–双向链表

  • 容器/环–循环列表

  • 上下文–报告超时和取消操作的方法

  • crypto–保存加密常数

  • 加密/aes–提供 AES 加密

  • 加密/密码–支持分组密码

  • 加密/des–提供 DES 和 3DES 加密

  • cypto/dsa–提供 DSA 支持

  • crypto/ECD sa–提供椭圆曲线 dsa

  • crypto/ed25519–提供 ed 25519 签名

  • 加密/椭圆–提供素数域上的椭圆曲线

  • crypto/hmac–提供 hmac 认证

  • crypto/md5–提供 MD5 哈希

  • crypto/rand–提供加密字符串随机数

  • crypto/rc4–提供 RC4 加密

  • 加密/rsa–提供 RSA 加密

  • crypto/sha1–提供 SHA-1 哈希

  • crypto/SHA 256–提供 SHA-224 和 SHA-256 哈希

  • crypto/sha 512–提供 SHA-384 和几个 SHA-512 哈希

  • 加密/微妙-提供加密助手功能

  • crypto/tls–提供 TLS(在 HTTPS 使用)支持

  • crypto/x509–提供 X.509 证书支持

  • crypto/pkix–为 x509 提供数据

  • 数据库-空,参见嵌套

  • 数据库/SQL–支持对数据库的 JDBC 式访问

  • 数据库/驱动程序–支持数据库驱动程序(SPI)

  • 调试–空,参见嵌套

  • 调试/dwarf-支持 DWARF 信息

  • 调试/elf–支持 ELF 目标文件

  • 调试/gosym–在运行时访问符号/行号

  • 调试/macho-支持 Mach-O 目标文件

  • 调试/PE–支持可移植的可执行文件

  • debug/Plan9 obj–支持 plan 9 目标文件

  • 编码–支持各种格式转换的接口

  • 编码/ascii85–支持 ascii 85 编码

  • 编码/ASN 1–支持 ASN.1 编码

  • 编码/base32–支持 base 32 编码

  • 编码/base64–支持 base64 编码

  • 编码/二进制–支持序列化基本类型

  • 编码/csv–支持读取/写入 CSV 格式数据

  • encoding/gob–支持序列化复杂(结构)类型

  • 编码/十六进制–二进制到十六进制字符串转换

  • encoding/json–二进制到 JSON 字符串转换

  • 编码/pem–提供 PEM 编码

  • 编码/XML–XML 解析器

  • 错误–支持 Go 错误类型

  • exp var——支持导出 Go 运行时状态(如 JMX)

  • flag–支持解析命令行标志(即开关)

  • fmt–支持扫描/格式化值

  • go–空,请参见嵌套

  • go/ast–支持访问基于 Go 的 ast

  • go build–支持处理 Go 包

  • go/constant–支持 Go 常量

  • go/doc–支持在 AST 中处理 Go 文档注释

  • go/format–支持格式化 Go 源代码

  • go/importer–支持处理import语句

  • go/parser–解析 Go 源代码

  • go/printer–支持格式化 AST

  • go/scanner–对 Go 源进行词法分析(标记化)

  • go/tokens–各种 Go 源令牌的枚举

  • go/types–支持 Go 源代码中的类型和类型检查

  • 哈希–定义各种哈希

  • hash/adler32–支持 Adler 32 校验和

  • hash/crc32–支持 32 位 crc 校验和

  • hash/crc 64–支持 64 位 CRC 校验和

  • 哈希/fnv–支持 fnv 哈希

  • hash/map hash–字节序列的散列

  • html–支持转义 HTML 值

  • HTML/模板–HTML 注入安全模板

  • 图像-帮助制作 2D 图像的功能

  • 图像/颜色–提供颜色库

  • 图像/调色板-介质颜色调色板

  • 图像/绘图–支持 2D 绘图

  • image/gif-gif 支援格式

  • 图像/jpeg–支持 JPEG 格式

  • 影像/png 支援 png 格式

  • 索引–空,参见嵌套

  • index/suffix array–使用后缀数组提供子字符串搜索

  • io–支持低级 I/O 操作

  • iou til–提供 I/O 助手

  • 日志–提供基本的日志记录

  • 日志/系统日志–提供对操作系统日志的访问

  • 数学–提供基本的数学函数

  • math/big——支持大整数、大浮点数和有理数

  • math/bits–支持整数的位级访问

  • cmplx–为复数提供帮助器/函数

  • math/rand–支持生成随机数

  • mime–提供对 MIME 类型的支持

  • mime/multipart–支持多部分数据

  • mime/quoted printable–支持引用的可打印编码

  • net——支持 TCP/IP 和 UDP 网络、DNS 解析和套接字

  • net/http–支持 HTTP 客户端和服务器

  • net/cgi–支持 CGI 服务器

  • net/cookiejar–支持 HTTP cookies

  • net/fgci——支持“快速”CGI 服务器

  • 支持 HTTP 交互的模拟测试

  • net/HTTP trace–支持 HTTP 请求跟踪

  • net/HTTP util–提供 HTTP 助手函数

  • net/pprof–支持 HTTP 服务器分析

  • net/mail–支持电子邮件处理

  • net/rpc–支持基本的 RPC 消息传递和序列化

  • net/rpc/jasonrpc——支持使用 JSON 主体的 RPC

  • net/SMTP–支持电子邮件发送/接收

  • net/text proto–支持基于文本头的(例如 HTTP)网络协议

  • 支持解析和处理 url

  • os–提供对操作系统(OS)提供的功能的访问

  • OS/exec–支持运行外部进程

  • 操作系统/信号–支持处理操作系统信号

  • 操作系统/用户–支持操作系统用户/组和凭据

  • os/path–支持处理 OS 文件系统路径

  • 操作系统/文件路径–路径帮助函数

  • 插件–为动态加载的插件提供(有限的)支持

  • reflect——支持在运行时自省和创建类型和实例

  • 正则表达式——支持计算正则表达式

  • regex/syntax–支持解析正则表达式

  • 运行时–支持管理正在运行的 Go 应用程序

  • 运行时/CGO——支持访问用 C 编写的函数

  • 运行时/调试–为运行时诊断提供支持

  • 运行时/pprof–生成分析数据

  • 运行时/跟踪–支持运行时跟踪;比日志记录更实用

  • 排序–支持排序或切片和集合

  • srtconv–转换成(格式化程序)/转换成(解析程序)字符串的各种转换器

  • 字符串——string类型的各种助手

  • sync–提供同步原语

  • 同步/原子-提供进行原子更新的功能

  • syscall 提供各种低级操作系统(OS)功能;可能因操作系统而异,并非在所有操作系统类型上都可用

  • 测试–提供类似 JUnit 的测试和代码计时

  • 测试/io test–测试 I/O 操作的助手

  • 测试/快速——提供测试用例中使用的助手

  • 文本-空,参见嵌套

  • 文本/扫描仪–提供字符串扫描/标记

  • 文本/tab writer–提供列对齐的文本输出

  • 文本/模板–支持在模板中可编程地插入文本

  • 文本/解析–支持解析模板

  • 时间-提供对日期、时间、时间戳、持续时间、瞬间的支持

  • time/tz data–无需操作系统帮助即可支持时区

  • unicode–空,参见嵌套

  • unicode/ut F16–支持 16 位 Unicode 字符

  • unicode/utf 32–提供对 32 位 Unicode 字符(又名符文)的支持

  • Unicode/utf8–支持 UTF 8 Unicode 字符

  • 不安全–为体系结构敏感的数据和指针提供支持

虽然这个列表很长,但是比 Java 标准版中所有包的类似列表要短得多。尽管如此,所提供的功能通常足以创建丰富的 web 客户端和服务器,它们是现代微服务的基础,是一个关键的 Go 用例。

举个例子(基于一个 Go 网站的例子),utf8包允许人们从(UTF-8)字符串中提取符文:

var text = "The 世界 is a crazy place!"  // world defined in UTF-8
var runes = make([]rune, 0, len(text))
for len(text) > 0 {
      rune, runeLen := utf8.DecodeRuneInString(text)
      fmt.Printf("%c(%d, %d)\n", rune, rune, runeLen)
      runes = append(runes, rune)
      text = text[runeLen:]
}

它输出一个包含输入的所有符文的切片;它的长度可能(在本例中将)比输入长度短。

作为另一个例子,考虑这个简单的方法来测量一些代码的运行时间:

func TimeIt(timeThis func() error) (dur time.Duration, err error) {
      start := time.Now()
      err = timeThis()
      dur = time.Now().Sub(start)
      return
}

随着

elapsed, _ := TimeIt(func() (err error) {
      time.Sleep(1 * time.Second)
      return
})

elapsed的结果大约是1e9

该示例可以用另一种方式重做:

func TimeIt(timeThis func() error) (dur time.Duration, err error) {
      start := time.Now()  // must declare before use
      defer func(){
            dur = time.Now().Sub(start)
      }()
      err = timeThis()
      return
}

接下来的章节讨论了几个 Go 软件包,每个都提供了一个更完整的软件包概要。在这些包的许多描述中,有一些用名称模式描述的功能:

Xxxx–使用默认参数函数提供一些行为

XxxxFunc–使用提供的参数函数提供相同的行为,以实现自定义行为变体

提供的函数通常是谓词。

posted @ 2024-10-05 17:13  绝不原创的飞龙  阅读(27)  评论(0编辑  收藏  举报