目录

Go语言入门(一)

摘要
Go语言入门(一)。

Day 01

01 知名编程语言或系统的发展简史

02 Go语言的前世今生

  • 缺少一个执行兼顾运行效率与开发效率的语言:
    • C++运行效率高,开发效率低。
    • Java开发效率高,运行效率低。

03 Go语言logo和版本

  • Go语言的最大优势是执行速度和开发效率都不错:
    • 执行速度:编译型静态语言。
    • 开发效率:语法与结构简明。

04 Go语言的核心特性

  1. 并发编程
  2. 内存回收(GC)
  3. 内存分配
  4. 编译
  5. 网络编程
  6. 函数多返回值
  7. 语言交互性
  8. 异常处理

05 Go语言和其他语言的对比

06 Go语言能做什么

07 Go语言环境搭建

08 第一个程序:HelloWorld

1
2
3
4
5
package main
import "fmt"
func main() {
	fmt.Println("Hello, World!")
}

09 Go的执行原理以及Go的命令

Go的源码文件

  1. 命令源码文件:声明自己属于 main 代码包、包含无参数声明和结果声明的 main 函数。
  2. 库源码文件。
  3. 测试源码文件。

Go的命令

  1. go run
    • 专门用来运行命令源码文件的命令,注意,这个命令不是用来运行所有 Go 的源码文件的!
    • go run 命令只能接受一个命令源码文件以及若干个库源码文件(必须同属于 main 包)作为文件参数,且不能接受测试源码文件。它在执行时会检查源码文件的类型。如果参数中有多个或者没有命令源码文件,那么 go run 命令就只会打印错误提示信息并退出,而不会继续执行。
  2. go build:go build 用于编译我们指定的源码文件或代码包以及它们的依赖包。但是注意如果用来编译非命令源码文件,即库源码文件,go build 执行完是不会产生任何结果的。这种情况下,go build 命令只是检查库源码文件的有效性,只会做检查性的编译,而不会输出任何结果文件。
    • 如果是普通包,当你执行go build命令后,不会产生任何文件。
    • 如果是main包,当只执行go build命令后,会在当前目录下生成一个可执行文件。如果需要在$GOPATH/bin目录下生成相应的exe文件,需要执行go install 或者使用 go build -o 路径/可执行文件。
    • 如果某个文件夹下有多个文件,而你只想编译其中某一个文件,可以在 go build 之后加上文件名,例如 go build a.go;go build 命令默认会编译当前目录下的所有go文件。
    • 你也可以指定编译输出的文件名。比如,我们可以指定go build -o 可执行文件名,默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。
    • go build 会忽略目录下以”_”或者”.”开头的go文件。
  3. go install:用来编译并安装代码包或者源码文件的。
    • go install 命令在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到$GOPATH/pkg或者$GOPATH/bin
    • 可执行文件: 一般是 go install 带main函数的go文件产生的,有函数入口,所有可以直接运行。
    • .a应用包: 一般是 go install 不包含main函数的go文件产生的,没有函数入口,只能被调用。
    • 安装代码包会在当前工作区的 pkg 的平台相关目录下生成归档文件(即 .a 文件)。
    • 安装命令源码文件会在当前工作区的 bin 目录(如果 GOPATH 下有多个工作区,就会放在 GOBIN 目录下)生成可执行文件。
  4. go get:用于从远程代码仓库(比如 Github )上下载并安装代码包。注意,go get 命令会把当前的代码包下载到 $GOPATH 中的第一个工作区的 src 目录中,并安装。
    • 调用 git clone 方法下载源码。
    • 编译。
    • 把库源码文件编译成归档文件安装到 pkg 对应的相关平台目录下。

10 安装Goland开发工具

11 编码规范

命名规范

  1. 以字母或下划线开头。
  2. 不允许在命名时中使用@、$和%等标点符号。
  3. 区分大小写。
  4. 当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public)。
  5. 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
包命名:package
  • 保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。

    1
    2
    3
    
    package demo
    
    package main
    
文件命名
  • 尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。

    1
    
    my_test.go
    
结构体命名
  • 采用驼峰命名法,首字母根据访问控制大写或者小写

  • struct 申明和初始化格式采用多行,例如下面:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // 多行申明
    type User struct{
        Username  string
        Email     string
    }
    
    // 多行初始化
    u := User{
        Username: "astaxie",
        Email:    "astaxie@gmail.com",
    }
    
接口命名
  • 命名规则基本和上面的结构体类似。
变量命名
  • 和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写。
  • 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头。
常量命名
  • 常量均需使用全部大写字母组成,并使用下划线分词

    1
    
    const APP_VER = "1.0"
    
  • 如果是枚举类型的常量,需要先创建相应类型:

    1
    2
    3
    4
    5
    6
    
    type Scheme string
    
    const (
        HTTP  Scheme = "http"
        HTTPS Scheme = "https"
    )
    
关键字

注释

  • Go提供C风格的/* */块注释和C ++风格的//行注释。
包注释
  • 每个包都应该有一个包注释,一个位于package子句之前的块注释或行注释。包如果有多个go文件,只需要出现在一个go文件中(一般是和包同名的文件)即可。 包注释应该包含下面基本信息(请严格按照这个顺序,简介,创建人,创建时间):

    • 包的基本简介(包名,简介)

    • 创建者,格式: 创建人: rtx 名

    • 创建时间,格式:创建时间: yyyyMMdd

例如 util 包的注释示例如下

1
2
3
// util 包, 该包包含了项目共用的一些常量,封装了项目中一些共用函数。
// 创建人: hanru
// 创建时间: 20190419
结构(接口)注释
  • 每个自定义的结构体或者接口都应该有注释说明,该注释对结构进行简要介绍,放在结构体定义的前一行,格式为: 结构体名, 结构体说明。同时结构体内的每个成员变量都要有说明,该说明放在成员变量的后面(注意对齐),实例如下:

    1
    2
    3
    4
    5
    
    // User , 用户对象,定义了用户的基础信息
    type User struct{
        Username  string // 用户名
        Email     string // 邮箱
    }
    
函数(方法)注释
  • 每个函数,或者方法(结构体或者接口下的函数称为方法)都应该有注释说明,函数的注释应该包括三个方面(严格按照此顺序撰写):

    • 简要说明,格式说明:以函数名开头,“,”分隔说明部分

    • 参数列表:每行一个参数,参数名开头,“,”分隔说明部分

    • 返回值: 每行一个返回值

示例如下:

1
2
3
4
5
6
7
// NewtAttrModel , 属性数据层操作类的工厂方法
// 参数:
//      ctx : 上下文信息
// 返回值:
//      属性操作类指针
func NewAttrModel(ctx *common.Context) *AttrModel {
}

代码风格

缩进和折行
语句的结尾
  • Go语言中是不需要类似于Java需要分号结尾,默认一行就是一条数据。
  • 如果你打算将多个语句写在同一行,它们则必须使用 ;
括号和空格
  • go 会强制左大括号不换行,换行会报语法错误。
import 规范
  • import在多行的情况下,goimports会自动帮你格式化,但是我们这里还是规范一下import的一些规范,如果你在一个文件里面引入了一个package,还是建议采用如下格式:

    1
    2
    3
    
    import (
        "fmt"
    )
    
  • 如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    import (
        "encoding/json"
        "strings"
    
        "myproject/models"
        "myproject/controller"
        "myproject/utils"
    
        "github.com/astaxie/beego"
        "github.com/go-sql-driver/mysql"
    )   
    
  • 有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是第三方包。

  • 在项目中不要使用相对路径引入包:

    1
    2
    3
    4
    5
    
    // 这是不好的导入
    import ../net
    
    // 这是正确的做法
    import github.com/repo/proj/src/net
    
  • 但是如果是引入本项目中的其他包,最好使用相对路径。

错误处理
  • 错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来。

  • 尽早return:一旦有错误发生,马上返回。

  • 尽量不要使用panic,除非你知道你在做什么。

  • 错误描述如果是英文必须为小写,不需要标点结尾。

  • 采用独立的错误流进行处理。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    // 错误写法
    if err != nil {
        // error handling
    } else {
        // normal code
    }
    
    // 正确写法
    if err != nil {
        // error handling
        return // or continue, etc.
    }
    // normal code
    

测试

  • 单元测试文件名命名规范为 example_test.go
  • 测试用例的函数名称必须以 Test 开头,例如:TestExample
  • 每个重要的函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。

Day 02

变量

变量的定义

变量的声明

  1. 指定变量类型,声明后若不赋值,使用默认值。

    1
    2
    
    var name type
    name = value
    
  2. 根据值自行判定变量类型(类型推断Type inference)

    • 如果一个变量有一个初始值,Go将自动能够使用初始值来推断该变量的类型。因此,如果变量具有初始值,则可以省略变量声明中的类型。

      1
      
      var name = value
      
  3. 简短声明

    • 省略var, 注意 :=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误。

    • 这种方式它只能被用在函数体内,而不可以用于全局变量的声明与赋值

      1
      2
      3
      4
      5
      6
      
      name := value
      
      // 例如
      var a int = 10
      var b = 10
      c : = 10
      

多变量声明

  1. 第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值

    1
    2
    
    var name1, name2, name3 type
    name1, name2, name3 = v1, v2, v3
    
  2. 第二种,直接赋值,下面的变量类型可以是不同的类型

    1
    
    var name1, name2, name3 = v1, v2, v3
    
  3. 第三种,集合类型

    1
    2
    3
    4
    
    var (
        name1 type1
        name2 type2
    )
    

注意事项

  1. 变量必须先定义才能使用
  2. go语言是静态语言,要求变量的类型和赋值的类型必须一致。
  3. 变量名不能冲突。(同一个作用于域内不能冲突)
  4. 简短定义方式,左边的变量名至少有一个是新的
  5. 简短定义方式,不能定义全局变量。
  6. 变量的零值。也叫默认值。
  7. 变量定义了就要使用,否则无法通过编译。
  8. 在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明。
  9. 在同一个作用域中,已存在同名的变量,则之后的声明初始化,则退化为赋值操作。

常量

常量声明

  • 常量是一个简单值的标识符,在程序运行时,不会被修改的量。

    1
    2
    3
    
    const identifier [type] = value
    显式类型定义: const b string = "abc"
    隐式类型定义: const b = "abc"
    
  • 常量可以作为枚举,常量组

    1
    2
    3
    4
    5
    
    const (
        Unknown = 0
        Female = 1
        Male = 2
    )
    

注意事项

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

  • 不曾使用的常量,在编译的时候,是不会报错的。

  • 显示指定类型的时候,必须确保常量左右值类型一致,需要时可做显示类型转换。这与变量就不一样了,变量是可以是不同的类型值。

iota

  • iota,特殊常量,可以认为是一个可以被编译器修改的常量。

  • iota 可以被用作枚举值:

    1
    2
    3
    4
    5
    
    const (
        a = iota
        b = iota
        c = iota
    )
    
  • 第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

    1
    2
    3
    4
    5
    
    const (
        a = iota
        b
        c
    )
    

Day 03

数据类型

基本数据类型

  1. 布尔类型

  2. 数值类型:

    1. 整数型:
      • int8:有符号 8 位整型 (-128 到 127);长度:8bit;byte

      • int16:有符号 16 位整型 (-32768 到 32767)

      • int32:有符号 32 位整型 (-2147483648 到 2147483647);rune

      • int64:有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

      • uint8:无符号 8 位整型 (0 到 255);8位都用于表示数值。

      • uint16:无符号 16 位整型 (0 到 65535)

      • uint32:无符号 32 位整型 (0 到 4294967295)

      • uint64:无符号 64 位整型 (0 到 18446744073709551615)

    2. 浮点型:
      • float32:IEEE-754 32位浮点型数

      • float64:IEEE-754 64位浮点型数

      • complex64:32 位实数和虚数

      • complex128:64 位实数和虚数

  3. 字符串型

    • 字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本

      1
      2
      
      	var str string
      	str = "Hello World"
      
  4. 数据类型转换

    • 语法格式:Type(Value)
    • 常数:在有需要的时候,会自动转型
    • 变量:需要手动转型 T(V)
    • 注意点:兼容类型可以转换

复合数据类型

运算符

算术运算符

1
+ - * / %(求余) ++ --
  • ++与–不能参与运算,不能放在变量之前。

关系运算符

1
== != > < >= <=

逻辑运算符

运算符 描述
&& 所谓逻辑与运算符。如果两个操作数都非零,则条件变为真
|| 所谓的逻辑或操作。如果任何两个操作数是非零,则条件变为真
! 所谓逻辑非运算符。使用反转操作数的逻辑状态。如果条件为真,那么逻辑非操后结果为假

位运算符

A B A&B A|B A^B
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1
运算 描述(假设A为60,B为13) 示例
& 二进制与操作副本位的结果,如果它存在于两个操作数 (A & B) = 12, 也就是 0000 1100
| 二进制或操作副本,如果它存在一个操作数 (A | B) = 61, 也就是 0011 1101
^ 二进制异或操作副本,如果它被设置在一个操作数就是按位取非 (A ^ B) = 49, 也就是 0011 0001
&^ 二进制位清空&^ (A&^B)=48,也就是110000
« 二进制左移位运算符。左边的操作数的值向左移动由右操作数指定的位数 A « 2 =240 也就是 1111 0000
» 二进制向右移位运算符。左边的操作数的值由右操作数指定的位数向右移动 A » 2 = 15 也就是 0000 1111

赋值运算符

运算符 描述 示例
= 简单的赋值操作符,分配值从右边的操作数左侧的操作数 C = A + B 将分配A + B的值到C
+= 相加并赋值运算符,它增加了右操作数左操作数和分配结果左操作数 C += A 相当于 C = C + A
-= 减和赋值运算符,它减去右操作数从左侧的操作数和分配结果左操作数 C -= A 相当于 C = C - A
*= 乘法和赋值运算符,它乘以右边的操作数与左操作数和分配结果左操作数 C *= A 相当于 C = C * A
/= 除法赋值运算符,它把左操作数与右操作数和分配结果左操作数 C /= A 相当于 C = C / A
%= 模量和赋值运算符,它需要使用两个操作数的模量和分配结果左操作数 C %= A 相当于 C = C % A
«= 左移位并赋值运算符 C «= 2 相同于 C = C « 2
»= 向右移位并赋值运算符 C »= 2 相同于 C = C » 2
&= 按位与赋值运算符 C &= 2 相同于 C = C & 2
^= 按位异或并赋值运算符 C ^= 2 相同于 C = C ^ 2
|= 按位或并赋值运算符 C |= 2 相同于 C = C | 2

运算符优先级

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

键盘输入和打印输出

常用打印函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
格式化打印占位符:
			%v,原样输出
			%T,打印类型
			%t,bool类型
			%s,字符串
			%f,浮点
			%d,10进制的整数
			%b,2进制的整数
			%o,8进制
			%x,%X,16进制
				%x:0-9,a-f
				%X:0-9,A-F
			%c,打印字符
			%p,打印地址
			。。。

键盘输入

  • 常用方法:

func Scan(a …interface{}) (n int, err error)

func Scanf(format string, a …interface{}) (n int, err error)

func Scanln(a …interface{}) (n int, err error)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
	"fmt"
)

func main() {
	var x int
	var y float64
	fmt.Println("请输入一个整数,一个浮点类型:")
	fmt.Scanln(&x,&y)//读取键盘的输入,通过操作地址,赋值给x和y   阻塞式
	fmt.Printf("x的数值:%d,y的数值:%f\n",x,y)

	fmt.Scanf("%d,%f",&x,&y)
	fmt.Printf("x:%d,y:%f\n",x,y)
}

Day 04

程序的流程结构

  1. 顺序结构:从上向下,逐行执行。
  2. 选择结构:条件满足,某些代码才会执行。0-1次
    • 分支语句:if,switch,select
  3. 循环结构:条件满足,某些代码会被反复的执行多次。0-N次
    • 循环语句:for

条件语句

if 语句

  • 语法格式:

    1
    2
    3
    
    if 布尔表达式 {
       /* 在布尔表达式为 true 时执行 */
    }
    
    1
    2
    3
    4
    5
    
    if 布尔表达式 {
       /* 在布尔表达式为 true 时执行 */
    } else {
      /* 在布尔表达式为 false 时执行 */
    }
    
    1
    2
    3
    4
    5
    6
    7
    
    if 布尔表达式1 {
       /* 在布尔表达式1为 true 时执行 */
    } else if 布尔表达式2{
       /* 在布尔表达式1为 false ,布尔表达式2为true时执行 */
    } else{
       /* 在上面两个布尔表达式都为false时,执行*/
    }
    

if 变体

  • 如果其中包含一个可选的语句组件(在评估条件之前执行),则还有一个变体。它的语法是

    1
    2
    3
    4
    5
    6
    7
    
    if statement; condition {  
    }
    
    if condition{
    
    
    }
    
  • 示例代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    package main
    
    import (  
        "fmt"
    )
    
    func main() {  
        if num := 10; num % 2 == 0 { //checks if number is even
            fmt.Println(num,"is even") 
        }  else {
            fmt.Println(num,"is odd")
        }
    }
    

需要注意的是,num的定义在if里,那么只能够在该if..else语句块中使用,否则编译器会报错的。

switch语句

  • switch是一个条件语句,它计算表达式并将其与可能匹配的列表进行比较,并根据匹配执行代码块。它可以被认为是一种惯用的方式来写多个if else子句。

  • switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。

  • switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加break

  • 如果switch没有表达式,它会匹配true

  • Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码

  • 变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。

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

  • 和 if 一样可以在swath关键词后初始化变量。

    1
    2
    3
    4
    5
    6
    7
    8
    
    switch var1 {
        case val1:
            ...
        case val2:
            ...
        default:
            ...
    }
    

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var grade string = "B"
   var marks int = 90

   switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"  //case 后可以由多个数值
      default: grade = "D"  
   }

   switch {
      case grade == "A" :
         fmt.Printf("优秀!\n" )     
      case grade == "B", grade == "C" :
         fmt.Printf("良好\n" )      
      case grade == "D" :
         fmt.Printf("及格\n" )      
      case grade == "F":
         fmt.Printf("不及格\n" )
      default:
         fmt.Printf("差\n" );
   }
   fmt.Printf("你的等级是 %s\n", grade );      
}

fallthrough

  • 如需贯通后续的case,就添加fallthrough。一个fallthrough只穿透后续一个case,必须位于case的最后一行。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    package main
    
    import (
    	"fmt"
    )
    
    type data [2]int
    
    func main() {
    	switch x := 5; x {
    	default:
    		fmt.Println(x)
    	case 5:
    		x += 10
    		fmt.Println(x)
    		fallthrough
    	case 6:
    		x += 20
    		fmt.Println(x)
    
    	}
    
    }
    

运行结果:

1
2
15
35

switch的注意事项

  1. case后的常量值不能重复
  2. case后可以有多个常量值
  3. fallthrough应该是某个case的最后一行。如果它出现在中间的某个地方,编译器就会抛出错误。

Type Switch

Day 05

循环语句

for循环(Go 没有while循环)

  • 语法结构:

    1
    
    for init; condition; post { }
    
  • 示例代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    package main
    
    import (  
        "fmt"
    )
    
    func main() {  
        for i := 1; i <= 10; i++ {
            fmt.Printf(" %d",i)
        }
    }
    

for循环变体

  • 所有的三个组成部分,即初始化、条件和post都是可选的。

  • 效果与while相似

    1
    
    for condition { }
    
  • 效果与for(;;) 一样

    1
    
    for { }
    
  • for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环

    1
    2
    3
    
    for key, value := range oldMap {
        newMap[key] = value
    }
    
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    package main
    
    import "fmt"
    
    func main() {
    
       var b int = 15
       var a int
    
       numbers := [6]int{1, 2, 3, 5} 
    
       /* for 循环 */
       for a := 0; a < 10; a++ {
          fmt.Printf("a 的值为: %d\n", a)
       }
    
       for a < b {
          a++
          fmt.Printf("a 的值为: %d\n", a)
          }
    
       for i,x:= range numbers {
          fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
       }   
    }
    

多层for循环

跳出循环的语句

break语句

continue语句

goto语句

  • goto:可以无条件地转移到过程中指定的行。

  • 语法结构:

    1
    2
    3
    4
    
    goto label;
    ..
    ..
    label: statement;
    
  • 可用于集中处理错误

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    err := firstCheckError()
        if err != nil {
            goto onExit
        }
        err = secondCheckError()
        if err != nil {
            goto onExit
        }
        fmt.Println("done")
        return
    onExit:
        fmt.Println(err)
        exitProcess()