目录

一、go原则

二、组织代码项目

三、数据类型

四、fmt 格式

五、package 包

5.1 可见性与作用域

5.2 包的导入

六、函数

6.1 main() 函数

6.2 init() 初始化函数

6.3 自定义函数

6.4 更改函数参数值

七、控制语句

7.1 if 语句

7.2 switch 语句

7.3 select 语句

7.4 break 语句

7.5 continue 语句

7.6 goto 语句

7.7 defer 函数

7.8 panic 函数与 recover 函数

7.9 示例:用户登录(流程控制)

7.10 示例:错误处理(控制流中断)

八、数组

8.1 声明与初始化

8.2 赋值与访问

8.3 range遍历

九、切片

9.1 切片内部结构

9.2 切片项

9.3 append() 和 copy() 函数

9.4 容量cap变化规律

9.5 示例:斐波纳契数列

十、映射

10.1 定义 Map

10.2 添加与删除

10.3 判断存在

十一、结构体

11.1 声明和初始化

11.2 结构嵌入

11.3 JSON 序列化

十二、方法

12.1 声明方法

12.2 方法的重载

12.3 方法的封装

12.4 函数与方法的区别

十三、接口

13.1 示例:interface声明与使用方法

13.2 示例:创建用于管理在线商店的程序包

13.3 示例:通过实现接口对结构体切片进行排序

十四、类型断言

十五、随机数

15.1 伪随机示例:math/rand

15.2 真随机示例:crypto/rand

十六、日期与时间

16.1 type Time

16.2 示例:日期与时间

16.3 type Duration

十七、cmd编译

17.1 两种编译方法

17.2 go build命令

17.3 go build示例

17.4 go install命令

17.5 go install示例

十八、错误处理策略

18.1 示例

18.2 处理策略:

18.3 用于错误处理的推荐做法

十九、日志记录

19.1 log包

19.2 记录到文件

二十、反射

二十一、nil


一、go原则

(1)Go 致力于使事情变得简单,用更少的代码行执行更多操作。

(2)并发是首选,函数可作为轻量线程运行。

(3)编译和执行速度快,目标是与 C 一样快。

(4)Go 要求强制转换是显式的,否则会引发编译错误。

(5)未使用的代码不是警告,而是错误,代码将不会编译。

(6)有一种官方格式设置,有助于保持项目之间的一致性。

(7)Go 并不适用于框架,因为它更倾向于使用标准库。

(8)Go 确保向后兼容性。

(9)Go 许可证是完全开放源代码。

二、组织代码项目

Go 在组织项目文件方面与其他编程语言不同。 首先,Go 在工作区的概念之下工作,其中,工作区就是应用程序源代码所在的位置。 在 Go 中,所有项目共享同一个工作区。 不过,从版本 1.11 开始,Go 开始更改此方法。  现在,Go 工作区位于 $HOME/go,但如果需要,可以为所有项目设置其他位置。

若要定义其他工作区位置,请将值设置为 $GOPATH 环境变量。 开始创建更复杂的项目时,需要为环境变量设置一个值,以避免将来出现问题。

在 macOS 或 Linux 中,可以:

// 通过将以下命令添加到 ~/.profile 来配置工作区:
export GOPATH=$HOME/go// 然后运行以下命令以更新环境变量:
source ~/.profile

在Windows 中,创建一个文件夹(例如 C:\Projects\Go),你将在其中创建所有 Go 项目。 打开 PowerShell 提示符,然后运行以下命令:

[Environment]::SetEnvironmentVariable("GOPATH", "C:\Projects\Go", "User")

在 Go 中,可以通过打印 $GOPATH 环境变量的值来获取工作区位置,以供将来参考。 或者,可以通过运行以下命令获取与 Go 相关的环境变量:

go env

在 Go 工作区中,可以找到以下文件夹:

  • bin:包含应用程序中的可执行文件。
  • src:包括位于工作站中的所有应用程序源代码。
  • pkg:包含可用库的已编译版本。 编译器可以链接这些库,而无需重新编译它们。

三、数据类型

Go 有四类数据类型:

  1. 基本类型:数字、字符串和布尔值
  2. 聚合类型:数组和结构
  3. 引用类型:指针、切片、映射、函数和通道
  4. 接口类型:接口

值类型与引用类型用法的区别

  1. 值类型包括基本类型聚合类型
  2. 值类型内存中变量存储的是具体的值,变量在内存中的地址可以通过 &num 来获取;引用类型变量直接存放的就是一个地址值,这个地址值指向的空间存的才是值。
  3. 值类型内存通常在栈中分配;引用类型变量存储的地址(也就是通过指针访问类型里面的数据),通常真正的值在堆上分配。
  4. 函数引用时,作为参数的值类型不修改原始数据;引用类型修改数据内容。

默认值:

  • int 类型的: 0(及其所有子类型,如 int64
  • float32 和 float64 类型的: +0.000000e+000
  • bool 类型的: false
  • string 类型的:空值

四、fmt 格式

通用:%v 值的默认格式表示%+v    类似%v,但输出结构体时会添加字段名%#v    值的Go语法表示%T  值的类型的Go语法表示%%   百分号布尔值:%t    单词true或false整数:%b    表示为二进制%c    该值对应的unicode码值%d    表示为十进制%o    表示为八进制%q    该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示%x    表示为十六进制,使用a-f%X  表示为十六进制,使用A-F%U  表示为Unicode格式:U+1234,等价于"U+%04X"浮点数与复数的两个组分:%b  无小数部分、二进制指数的科学计数法,如-123456p-78;参见strconv.FormatFloat%e    科学计数法,如-1234.456e+78%E  科学计数法,如-1234.456E+78%f  有小数部分但无指数部分,如123.456%F   等价于%f%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)%G   根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)字符串和[]byte%s 直接输出字符串或者[]byte%q   该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示%x   每个字节用两字符十六进制数表示(使用a-f)%X  每个字节用两字符十六进制数表示(使用A-F)    指针:%p    表示为十六进制,并加上前导的0x    没有%u。整数如果是无符号类型自然输出也是无符号的。类似的,也没有必要指定操作数的尺寸(int8,int64)。宽度与精度:%f:    默认宽度,默认精度%9f    宽度9,默认精度%.2f   默认宽度,精度2%9.2f  宽度9,精度2%9.f   宽度9,精度0  

五、package 包

(1)包(package)是多个Go源码的集合,go语言有很多内置包,比如fmt,os,io等;

(2)一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下;

(3)包名一般是小写的,使用一个简短且有意义的名称;

(4)main包是一个可执行的包,是应用程序的入口包,编译完会生成一个可执行文件;

(5)编译不包含 main 包的源码文件时不会得到可执行文件;

(6)包名一般要和所在的目录同名,也可以不同,包名中不能包含 “-”等特殊符号;

(7)包一般使用域名作为目录名称,这样能保证包名的唯一性;

(8)GitHub 项目的包一般会放到 GOPATH/src/github.com/userName/projectName 目录下;

(9)任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是 package 语句,通过该语句声明自己所在的包。

5.1 可见性与作用域

变量作用域的好处:

  1. 可以在多个位置使用相同的变量名而不引起任何冲突;
  2. 脱离作用域的变量将不再可见并且无法访问
  3. 帮助更好地阅读代码;
  4. go的作用域通常随着大括号{ }的出现而开启和结束;
  5. 虽然没有使用大括号,但关键字 case 和 default 也都引入了新的作用域;
  6. 在 for 语句、if 语句或 switch 语句所在行声明的变量,作用域持续至该语句结束为止;
  7. 声明变量的位置决定了变量所处的作用域;

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。

在Go语言中只需要将标识符的首字母大写就可以。

// 首字母小写,外部包不可见,只能在当前包内使用
var num = 10//首字母大写外部包可见,可在其他包中使用
const Name  = "ares"// 首字母小写,外部包不可见,只能在当前包内使用
type person struct {name string
}type Student struct {Name  string           //可在包外访问的方法class string           //仅限包内访问的字段
}type Payer interface {init()                 //仅限包内访问的方法Pay()                  //可在包外访问的方法
}// 首字母大写,外部包可见,可在其他包中使用
func Add(x, y int) int {return x + y
}func age() {               // 首字母小写,外部包不可见,只能在当前包内使用var Age = 18           // 函数局部变量,外部包不可见,只能在当前函数内使用fmt.Println(Age)
}

5.2 包的导入

(1)使用import关键字;

(2)import导入语句通常放在文件开头包声明语句的下面;

(3)导入的包名需要使用双引号包裹起来;

(4)当你导入多个包时,导入的顺序会按照字母排序;

(5)导入包即等同于包含了这个包的所有的代码对象;

(6)导入的包必须要被使用,否则程序编译时也会报错。

导入格式:

Go包的引入格式常见的有四种,下面以引用"fmt"包为例说明:

import "fmt"             // 标准引用
import fmt_go "fmt"      // 别名引用,fmt_go是引用fmt包的别名,此时使用fmt包的功能时需要用fmt_go.方法来使用
import . "fmt"           // 省略方式,这种引用相当于把包fmt的命名空间合并到当前程序的命名空间了,因此可以直接引用,不用在加上前缀fmt.
import _ "fmt"           // 匿名引用,仅执行包的初始化函数,不使用包内数据

导入路径:

import导入时,会从GO的安装目录(也就是GOROOT环境变量设置的目录)和GOPATH环境变量设置的目录中,检索 src/package 来导入包。如果不存在,则导入失败。

  • GOROOT,就是GO内置的包所在的位置;
  • GOPATH,就是我们自己定义的包的位置。

关于我们自己定义的包的导入路径有多种说法,本文进行了实际测试;

假设文件结构:

D:\golang\.|└──src├──add│   └───add.go└──main└───main.go└───sub└───sub.go

包的引用策略:

// 第一种情况
// 如果设置了 GOPATH = D:\golang
// 包名默认是从 $GOPATH/src/ 后开始计算的
import (    "add""main/sub"
)// 第二种情况
// 如果没有设置 GOPATH = D:\golang
// 只能使用相对路径
import (    "../add"               //上级目录"./sub"                //本级目录往下
)

特别注意:本文测试中,两种方法与 GOPATH 的设置相对应;即:

  • 如果源码在 GOPATH 目录下,包的引用只能使用第一种方法,不能使用相对路径(不知道这个是什么逻辑);
  • 如果源码不在 GOPATH 目录下,只能使用相对路径,而不能使用第一种方法;

六、函数

6.1 main() 函数

与之交互的函数是 main() 函数。 Go 中的所有可执行程序都具有此函数,因为它是程序的起点。

你的程序中只能有一个 main() 函数。 如果创建的是 Go 包,则无需编写 main() 函数。

main() 函数没有任何参数,并且不返回任何内容。 但这并不意味着其不能从用户读取值,如命令行参数。

6.2 init() 初始化函数

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是: init()函数没有参数也没有返回值。

import "fmt"var x = 100func init()  {fmt.Println(x)  //100
}
func main()  {fmt.Println("Hello!")   //Hello!
}

包中init函数的执行时机:

全局声明 ===> init() ====> main()

init()函数执行顺序:

init() 函数与 main() 函数对比:

(1)都是go语言中的保留函数。init()用于初始化信息,main()用于座位程序入口;

(2)两个函数定义的时候,不能有参数和返回值,只能由go程序自动调用,不能被引用;

(3)init()函数可以定义在任意包中,可以有多个。main()函数只能在main包下,并且只能有一个;

(4)存在依赖的包之间不能循环导入;

(5)一个包可以被其他多个包import,但是只能被初始化一次。

执行顺序:

  • 先执行init()函数,后执行main()函数
  • 对于同一个go文件中,调用顺序是从上向下的,也就是先写的先被执行,后写的后被执行
  • 对于同一个包下,将文件名称按照字符串进行排序,之后顺序调用哥哥文件中的init()函数
  • 不同包下,如果不存在依赖,按照main包中的import顺序来调用对应包中的init()函数;如果存在依赖,最后被依赖 的最先被初始化,导入顺序:main-->A-->B-->C,执行顺序,C-->B-->A--main

6.3 自定义函数

下面是用于创建函数的语法:

func name(parameters) (results) {body-content
}
  1. 使用 func 关键字来定义函数,然后为其指定名称。
  2. 在命名后,指定函数的参数列表。 你可以指定零个或多个参数。
  3. 你还可以定义函数的返回类型,该函数也可以是零个或多个。只需在末尾添加 return 行。
  4. 在 Go 中,函数可以返回多个值。 你可以采用类似于定义函数参数的方式来定义这些值。 换句话说,你可以指定一个类型和名称,但该名称是可选的。
  5. 如果不需要函数的某个返回值,可以通过将返回值分配给 _ 变量来放弃该函数。 _ 变量是 Go 忽略返回值的惯用方式。 它允许程序进行编译。

示例:

func main() {sum, _ := calc(os.Args[1], os.Args[2])    // 放弃calc函数的第 2 个返回值println("Sum:", sum)
}

6.4 更改函数参数值

将值传递给函数时,该函数中的每个更改都不会影响调用方。

Go 是“按值传递”编程语言。 这意味着每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。

在函数中对该变量所做的更改都不会影响你向函数发送的更改。

指针:

如果你希望在 自定义 函数中进行的更改会影响 main 函数中的变量,则需要使用指针。

指针是包含另一个变量的内存地址的变量。 当你发送指向某个函数的指针时,不会传递值,而是传递地址内存。 因此,对该变量所做的每个更改都会影响调用方。

在 Go 中,有两个运算符可用于处理指针:

  • & 运算符使用其后对象的地址。
  • * 运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。

示例:

package mainfunc main() {firstName := "John"updateName(&firstName)println(firstName)
}func updateName(name *string) {*name = "David"
}

运行前面的代码。 请注意,输出现在显示的是 David,而不是 John

首先要做的就是修改函数的参数,以指明你要接收指针。 为此,请将参数类型从 string 更改为 *string。(后者仍是字符串,但现在它是指向字符串的指针。)然后,将新值分配给该变量时,需要在该变量的左侧添加星号 (*) 以暂停该变量的值。 调用 updateName 函数时,系统不会发送值,而是发送变量的内存地址。 这就是前面的代码在变量左侧带有 & 符号的原因。

七、控制语句

7.1 if 语句

(1)不需使用括号将条件包含起来;

(2)大括号{}必须存在,即使只有一行语句;

(3)左括号必须在if或else的同一行;

(4)在if之后,条件语句之前,可以添加变量初始化语句,使用";"进行分隔;

(5)在有返回值的函数中,最终的return不能在条件语句中。

7.2 switch 语句

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

(2)case 后的各个表达式的值的数据类型,必须和 switch 的表达式数据类型一致;

(3)case 后的表达式如果是常量(字面量),则要求不能重复;

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

(5)switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough ;

(6)switch 从第一个判断表达式为 true 的 case 开始执行,如果 case 带有 fallthrough,程序会继续执行下一条 case,且它不会去判断下一个 case 的表达式是否为 true。

(7)switch 后面可以不带表达式,类似 if-else 分支来使用;

7.3 select 语句

(1)每个 case 都必须是一个通信;

(2)所有 channel 表达式都会被求值;

(3)所有被发送的表达式都会被求值;

(4)如果任意某个通信可以进行,它就执行,其他被忽略;

(5)如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行;

(6)如果有 default 子句,则执行该语句。如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。

7.4 break 语句

(1)用于循环语句中跳出循环,并开始执行循环之后的语句;

(2)break 在 switch(开关语句)中在执行一条 case 后跳出语句的作用;

(3)在多重循环中,可以用标号 label 标出想 break 的循环。

7.5 continue 语句

(1)有点像 break 语句。但是 continue 不是跳出循环,而是跳过当前循环执行下一次循环语句;

(2)for 循环中,执行 continue 语句会触发 for 增量语句的执行;

(3)在多重循环中,可以用标号 label 标出想 continue 的循环。

7.6 goto 语句

(1)无条件地转移到过程中指定的行。

(2)goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。

(3)但是,在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。

7.7 defer 函数

(1)在 Go 中,defer 语句会推迟函数(包括任何参数)的运行,直到包含 defer 语句的函数完成。

(2)通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟某个函数的运行。

(3)可以根据需要推迟任意多个函数。

(4)defer 语句按逆序运行(后进先出),先运行最后一个,最后运行第一个。

7.8 panic 函数与 recover 函数

panic 函数:

(1)运行时错误会使 Go 程序进入紧急状态。 可以强制程序进入紧急状态,但运行时错误(例如数组访问超出范围、取消对空指针的引用)也可能会导致进入紧急状态。

(2)内置 panic() 函数会停止正常的控制流。 所有推迟的函数调用都会正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。 此消息包含错误和堆栈跟踪,有助于诊断问题的根本原因。

(3)调用 panic() 函数时,可以添加任何值作为参数。 通常,你会发送一条错误消息,说明为什么会进入紧急状态。

recover 函数:

(1)有时,你可能想要避免程序崩溃,改为在内部报告错误。 或者,你可能想要先清理混乱情况,然后再让程序崩溃。 例如,你可能想要关闭与某个资源的连接,以免出现更多问题。

(2)Go 提供内置函数 recover(),允许你在出现紧急状况之后重新获得控制权。

(3)只能在已推迟的函数中使用此函数。

(4)如果调用 recover() 函数,则在正常运行的情况下,它会返回 nil,没有任何其他作用。

两者的关系:panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误,panic 用于主动抛出错误,recover 用来捕获 panic 抛出的错误。

(1)引发 panic 有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出;

(2)发生 panic 后,程序会从调用 panic 的函数位置或发生 panic 的地方立即返回,逐层向上执行函数的 defer 语句,然后逐层打印函数调用堆栈,直到被 recover 捕获或运行到最外层函数;

(3)panic 不但可以在函数正常流程中抛出,在 defer 逻辑里也可以再次调用 panic 或抛出 panicdefer 里面的 panic 能够被后续执行的 defer 捕获;

(4)recover 用来捕获 panic,阻止 panic 继续向上传递;

(5)recover 和 defer 一起使用,但是 defer 只有在后面的函数体内直接被调用才能捕获 panic 来终止异常,否则返回 nil,异常继续向外传递。

//以下三种方法捕获失败
defer recover()               //无效   defer fmt.Prinntln(recover)   //无效defer func(){func(){recover()             //无效,嵌套两层}()
}()//以下三种捕获有效
defer func(){recover()
}()func except(){recover()
}func test(){defer except()panic("runtime error")
}

7.9 示例:用户登录(流程控制)

package main
import "fmt"
func main() {// 实现登录验证,有三次机会,如果用户名和密码正确提示登录成功// 否则提示还有几次机会var name string var pwd stringvar loginChance = 3for i := 1 ; i <= 3; i++ {fmt.Println("请输入用户名")fmt.Scanln(&name)fmt.Println("请输入密码")fmt.Scanln(&pwd)if name == "user" && pwd == "888888" {fmt.Println("恭喜你登录成功!")break} else {loginChance--fmt.Printf("你还有%v次登录机会,请珍惜\n", loginChance)}}if loginChance == 0 {fmt.Println("机会用完,没有登录成功!")}
}

7.10 示例:错误处理(控制流中断)

package mainimport "fmt"func main() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered in main", r)}}()g(0)fmt.Println("Program finished successfully!")
}func g(i int) {if i > 3 {fmt.Println("Panicking!")panic("Panic in g() (major)")}defer fmt.Println("Defer in g()", i)fmt.Println("Printing in g()", i)g(i + 1)
}/*
Printing in g() 0
Printing in g() 1
Printing in g() 2
Printing in g() 3
Panicking!
Defer in g() 3
Defer in g() 2
Defer in g() 1
Defer in g() 0
Recovered in main Panic in g() (major)
*/

下面是运行代码时会发生的情况:

  1. 一切正常运行。 程序输出 g() 函数接收的值。
  2. 当 i 大于 3 时,程序会进入紧急状态。 会显示“Panicking!”消息。 此时,控制流中断,所有推迟的函数都开始输出“Defer in g()”消息。
  3. 程序崩溃,并显示 recover() 消息。

总结:

  1. 在发生未预料到的严重错误时,系统通常会运行对 panic() 的调用。 若要避免程序崩溃,可以使用 recover() 函数。
  2. 在 main() 函数中,你会将一个可以调用 recover() 函数的匿名函数推迟。
  3. 当程序处于紧急状态时,对 recover() 的调用无法返回 nil。 你可以在此处执行一些操作来清理混乱,但在这种情况下,你可以直接输出一些内容。
  4. panic() 和 recover() 的组合是 Go 处理异常的惯用方式。

八、数组

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。

8.1 声明与初始化

// Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
var variable_name [SIZE] variable_type// 一维数组的定义方式。例如以下定义了数组 balance 长度为 10 类型为 float32:
var balance [10] float32// 数组初始化,初始化数组中 {} 中的元素个数不能大于 [] 中的数字:
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}// 如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
//或
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}// 如果设置了数组的长度,我们还可以通过指定下标来初始化元素:
// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}// 常用的多维数组声明方式:
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type// 以下实例声明了三维的整型数组:
var threedim [5][10][4]int// 二维数组初始化
a := [3][4]int{  {0, 1, 2, 3} ,    /*  第一行索引为 0 */{4, 5, 6, 7} ,    /*  第二行索引为 1 */{8, 9, 10, 11},   /*  第三行索引为 2 */  //注意:必须要有逗号,因为后面一行的 } 不能单独一行。
}

8.2 赋值与访问

数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。例如:

package mainimport "fmt"func main() {var n [10]int /* n 是一个长度为 10 的数组 */var i,j int/* 为数组 n 初始化元素 */        for i = 0; i < 10; i++ {n[i] = i + 100 /* 设置元素为 i + 100 */}/* 输出每个数组元素的值 */for j = 0; j < 10; j++ {fmt.Printf("Element[%d] = %d\n", j, n[j] )}
}/*
Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109
*/

二维数组:

package mainimport "fmt"func main() {// Step 1: 创建数组values := [][]int{}// Step 2: 使用 appped() 函数向空的二维数组添加两行一维数组row1 := []int{1, 2, 3}row2 := []int{4, 5, 6}values = append(values, row1)values = append(values, row2)// Step 3: 显示两行数据fmt.Println("Row 1")fmt.Println(values[0])fmt.Println("Row 2")fmt.Println(values[1])// Step 4: 访问第一个元素fmt.Println("第一个元素为:")fmt.Println(values[0][0])
}/*
Row 1
[1 2 3]
Row 2
[4 5 6]
第一个元素为:
1
*/

使用函数修改数组的值:

package main
import ("fmt"
)// 函数(数组)
func test01(arr01 [3]int) {fmt.Println("arr01 arr old = ", arr01)arr01[0] = 88fmt.Println("arr01 arr new = ", arr01)
} // 函数(数组指针)
func test02(arr02 *[3]int) {fmt.Printf("arr02指针的值 = %p\n", arr02)fmt.Printf("arr02指针的地址 = %p\n", &arr02)fmt.Println("arr02 arr old = ", *arr02)    (*arr02)[0] = 88 fmt.Println("arr02 arr new = ", *arr02)
} func main() {arr := [3]int{11, 22, 33}fmt.Println("main arr old =", arr)test01(arr)fmt.Println("main arr from test01 =", arr)fmt.Println("----------以上传的是数组,数组值没有变化----------")fmt.Println("----------如果想修改数组,需要传数组指针----------")fmt.Printf("arr 的地址 = %p\n", &arr)test02(&arr)fmt.Println("main arr from test02 =", arr)
}/*
main arr old = [11 22 33]
arr01 arr old =  [11 22 33]
arr01 arr new =  [88 22 33]
main arr from test01 = [11 22 33]
----------以上传的是数组,数组值没有变化----------
----------如果想修改数组,需要传数组指针----------
arr 的地址 = 0xc0000a8120
arr02指针的值 = 0xc0000a8120
arr02指针的地址 = 0xc0000d4020
arr02 arr old =  [11 22 33]
arr02 arr new =  [88 22 33]
main arr from test02 = [88 22 33]
*/

创建各个维度元素数量不一致的多维数组:

package mainimport "fmt"func main() {// 创建空的二维数组animals := [][]string{}// 创建三一维数组,各数组长度不同row1 := []string{"fish", "shark", "eel"}row2 := []string{"bird"}row3 := []string{"lizard", "salamander"}// 使用 append() 函数将一维数组添加到二维数组中animals = append(animals, row1)animals = append(animals, row2)animals = append(animals, row3)// 循环输出for i := range animals {fmt.Printf("Row: %v\n", i)fmt.Println(animals[i])}
}/*
Row: 0
[fish shark eel]
Row: 1
[bird]
Row: 2
[lizard salamander]
*/

8.3 range遍历

package main
import (
"fmt"
)
func main() {    // 二维数组   var value = [3][2]int{{1, 2}, {3, 4}, {5, 6}}  // 遍历二维数组,使用 range  // 其实,这里的 i, j 表示行游标和列游标  // v2 就是具体的每一个元素  // v  就是每一行的所有元素 for i, v := range value {for j, v2 := range v {            fmt.Printf("value[%v][%v]=%v \t ", i, j, v2)        }        fmt.Print(v)        fmt.Println()    }
}/*
value[0][0]=1      value[0][1]=2      [1 2]
value[1][0]=3      value[1][1]=4      [3 4]
value[2][0]=5      value[2][1]=6      [5 6]
*/

九、切片

9.1 切片内部结构

struct Slice
{   byte*    array;       // actual datauintgo    len;        // number of elementsuintgo    cap;        // allocated number of elements
};

切片有 3 个组件:

  • 指针,指向基础数组可访问的第一个元素(并非一定是数组的第一个元素)。
  • 长度,指示切片中的元素数目,表示 slice 的长度。
  • 容量,显示切片开头与基础数组结束之间的元素数目。

下图显示了什么是切片:

当把 slice 作为参数,本身传递的是值,但其内容就 byte* array,实际传递的是引用,所以可以在函数内部修改,但如果对 slice 本身做 append,而且导致 slice 进行了扩容,实际扩容的是函数内复制的一份切片,对于函数外面的切片没有变化。

9.2 切片项

Go 支持切片运算符 s[i:j],其中:

  • s 表示数组。
  • i 表示指向它将使用的数组(或另一切片)的第一个元素的指针。
  • j 表示切片将使用的最后一个元素的位置。

换句话说,切片只能引用元素的子集。

例如,假设需要 4 个变量来表示一年的每个季度。 下图说明了它在 Go 中的显示效果:

若要用代码表示在上图中看到的内容,可使用以下代码:

package mainimport "fmt"func main() {months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}quarter1 := months[0:3]quarter2 := months[3:6]quarter3 := months[6:9]quarter4 := months[9:12]fmt.Println(quarter1, len(quarter1), cap(quarter1))fmt.Println(quarter2, len(quarter2), cap(quarter2))fmt.Println(quarter3, len(quarter3), cap(quarter3))fmt.Println(quarter4, len(quarter4), cap(quarter4))
}/*
[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3
*/

示例代码:

package mainimport "fmt"func main() {/* 创建切片 */numbers := []int{0,1,2,3,4,5,6,7,8}  printSlice(numbers)/* 打印原始切片 */fmt.Println("numbers ==", numbers)/* 打印子切片从索引1(包含) 到索引4(不包含)*/fmt.Println("numbers[1:4] ==", numbers[1:4])/* 默认下限为 0*/fmt.Println("numbers[:3] ==", numbers[:3])/* 默认上限为 len(s)*/fmt.Println("numbers[4:] ==", numbers[4:])numbers1 := make([]int,0,5)printSlice(numbers1)/* 打印子切片从索引  0(包含) 到索引 2(不包含) */number2 := numbers[:2]printSlice(number2)/* 打印子切片从索引 2(包含) 到索引 5(不包含) */number3 := numbers[2:5]printSlice(number3)}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}/*
len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]
*/

9.3 append() 和 copy() 函数

package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)/* 允许追加空切片 */numbers = append(numbers, 0)printSlice(numbers)/* 向切片添加一个元素 */numbers = append(numbers, 1)printSlice(numbers)/* 同时添加多个元素 */numbers = append(numbers, 2,3,4)printSlice(numbers)/* 创建切片 numbers1 是之前切片的两倍容量*/numbers1 := make([]int, len(numbers), (cap(numbers))*2)/* Go 具有内置函数 copy(dst, src []Type) 用于创建切片的副本。 * 创建一个切片副本,它会在后台生成新的基础数组,与原数组互不影响* 你需要发送目标切片和源切片,拷贝 numbers 的内容到 numbers1*/copy(numbers1,numbers)printSlice(numbers1)
}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}/*
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]
*/

9.4 容量cap变化规律

/*每次cap改变的时候指向array内存的指针都在变化。当在使用 append 的时候,如果 cap==len 了这个时候就会新开辟一块更大内存,然后把之前的数据复制过去。实际go在append的时候放大cap是有规律的。在 cap 小于1024的情况下是每次扩大到 2 * cap ,当大于1024之后就每次扩大到 1.25 * cap 。
*/package mainimport ("fmt""unsafe"
)func main() { // 每次cap改变,指向array的ptr就会变化一次s := make([]int, 1)fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))for i := 0; i < 5; i++ {s = append(s, i)fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))}fmt.Println("Array:", s)
}/*
len:1 cap: 1 array ptr: 0xc0000aa058
len:2 cap: 2 array ptr: 0xc0000aa0a0
len:3 cap: 4 array ptr: 0xc0000a8140
len:4 cap: 4 array ptr: 0xc0000a8140
len:5 cap: 8 array ptr: 0xc0000c20c0
len:6 cap: 8 array ptr: 0xc0000c20c0
Array: [0 0 1 2 3 4]
*/

9.5 示例:斐波纳契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以引入;

指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、55、89、……

在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

package mainimport "fmt"func fibonacci(n int) []int {if n < 2 {return make([]int, 0)}nums := make([]int, n)nums[0], nums[1] = 1, 1for i := 2; i < n; i++ {nums[i] = nums[i-1] + nums[i-2]}return nums
}func main() {var num intfmt.Print("What's the Fibonacci sequence you want? ")fmt.Scanln(&num)fmt.Println("The Fibonacci sequence is:", fibonacci(num))
}

十、映射

(1)Map 是一种无序的键值对的集合。

(2)Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

(3)Map 是一种集合,可以像迭代数组和切片那样迭代它。

(4)Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。

10.1 定义 Map

可以使用内建函数 make 也可以使用 map 关键字来定义 Map:

/* 声明变量,默认 map 是 nil ,nil map 不能用来存放键值对*/
var map_variable map[key_data_type]value_data_type/* 使用 make 函数 ,创建空映射,可以用来存放键值对*/
map_variable := make(map[key_data_type]value_data_type)/* 定义并初始化*/
studentsAge := map[string]int{"john": 32, "bob":  31,}

10.2 添加与删除

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。实例如下:

package mainimport "fmt"func main() {/* 创建map */countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome"}/* 添加映射项 */countryCapitalMap ["Japan"] = "Tokyo"countryCapitalMap ["India"] = "New delhi"fmt.Println("原始地图")/* 打印地图 *//* 方法1:可以直接打印输出映射fmt.Println(countryCapitalMap )  *//* 方法2:可使用for range打印输出映射 */        for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])}/* for range双参数输出for country , capital:= range countryCapitalMap {fmt.Printf("%s\t%s\n", country , capital)}*//*删除元素*/ delete(countryCapitalMap, "France")fmt.Println("法国条目被删除")fmt.Println("删除元素后地图")/*打印地图*/for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])}
}/*
原始地图
India 首都是 New delhi
France 首都是 Paris
Italy 首都是 Rome
Japan 首都是 Tokyo
法国条目被删除
删除元素后地图
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi
*/

10.3 判断存在

访问映射中没有的项时 Go 不会返回错误,这是正常的。

但有时需要知道某个项是否存在。 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。

package mainimport "fmt"func main() {studentsAge := make(map[string]int)studentsAge["john"] = 32studentsAge["bob"] = 31age, exist := studentsAge["christy"]if exist {fmt.Println("Christy's age is", age)} else {fmt.Println("Christy's age couldn't be found")}
}

十一、结构体

11.1 声明和初始化

/* 使用 struct 关键字,还要使用希望新的数据类型具有的字段及其类型的列表*/
type Employee struct {ID        intFirstName stringLastName  stringAddress   string
}/* 可像操作其他类型一样使用新类型声明一个变量*/
var john Employee/* * 可像操作其他类型一样使用新类型声明一个变量;* 请注意,该方式必须为结构中的每个字段指定一个值。
*/
employee01 := Employee{1001, "John", "Doe", "Doe's Street"}/* * 可更具体地了解要在结构中初始化的字段。* 为每个字段分配值的顺序不重要。 * 如果未指定任何其他字段的值,也并不重要。 Go 将根据字段数据类型分配默认值。
*/
employee02 := Employee{LastName: "Doe", FirstName: "John"}

11.2 结构嵌入

有时,你需要减少重复并重用一种常见的结构。通过 Go 中的结构,可将某结构嵌入到另一结构中。

package mainimport "fmt"type Person struct {ID        intFirstName stringLastName  stringAddress   string
}/* 第1种方法:重构结构体*/
type Employee struct {PersonManagerID int
}/* 第2种方法:创建新字段*/
type Contractor struct {Information PersonCompanyID int
}func main() {/** 第1种方法定义的使用* 无需指定 Person 字段的情况下访问 Employee 结构中的 FirstName 字段,因为它会自动嵌入其所有字段。 * 但在你初始化结构时,必须明确要给哪个字段分配值*/employee := Employee{Person: Person{FirstName: "John",},}employee.LastName = "Doe"fmt.Println(employee.FirstName)/** 第2种方法定义的使用* 若要引用 Person 结构中的字段,你需要包含员工变量中的 Information 字段*/var contractor Contractor contractor.Information.FirstName = "John"fmt.Println(contractor.FirstName)
}

11.3 JSON 序列化

可使用结构来对 JSON 中的数据进行编码和解码。 Go 对 JSON 格式提供很好的支持,该格式已包含在标准库包中。

JSON 编码示例:

package main
import ("fmt""encoding/json"
)type Monster struct {Name string `json:"monster_name"`       // 结构体的 tag 标签,反射机制Age int `json:"monster_age"`Birthday stringSal float64Skill string
}// 将 struct 进行序列化
func testStruct() {monster := Monster{Name :"牛魔王",Age : 500 ,Birthday : "2011-11-11",Sal : 8000.0,Skill : "牛魔拳",}// 将 monster 序列化data, err := json.Marshal(&monster)if err != nil {fmt.Printf("序列号错误 err=%v\n", err)}// 输出序列化后的结果fmt.Printf("monster序列化后 = %v\n", string(data))
}// 将 map 进行序列化
func testMap() {var a map[string]interface{}a = make(map[string]interface{})a["name"] = "红孩儿"a["age"] = 30a["address"] = "洪崖洞"//将a这个 map 进行序列化data, err := json.Marshal(a)if err != nil {fmt.Printf("序列化错误 err=%v\n", err)}// 输出序列化后的结果fmt.Printf("a map  序列化后 = %v\n", string(data))
}// 将切片进行序列化,  []map[string]interface{}
func testSlice() {var slice []map[string]interface{}var m1 map[string]interface{}m1 = make(map[string]interface{})m1["name"] = "jack"m1["age"] = "7"m1["address"] = "北京"slice = append(slice, m1)var m2 map[string]interface{}m2 = make(map[string]interface{})m2["name"] = "tom"m2["age"] = "20"m2["address"] = [2]string{"上海","深圳"}slice = append(slice, m2)// 将切片进行序列化操作data, err := json.Marshal(slice)if err != nil {fmt.Printf("序列化错误 err=%v\n", err)}// 输出序列化后的结果fmt.Printf("slice  序列化后 = %v\n", string(data))
}// 对基本数据类型序列化,对基本数据类型进行序列化意义不大
func testFloat64() {var num1 float64 = 2345.67data, err := json.Marshal(num1)if err != nil {fmt.Printf("序列化错误 err=%v\n", err)}// 输出序列化后的结果fmt.Printf("num1   序列化后 = %v\n", string(data))
}func main() {// 演示将结构体, map , 切片进行序列化testStruct()testMap()testSlice()testFloat64()
}/*
monster序列化后 = {"monster_name":"牛魔王","monster_age":500,"Birthday":"2011-11-11","Sal":8000,"Skill":"牛魔拳"}
a map  序列化后 = {"address":"洪崖洞","age":30,"name":"红孩儿"}
slice  序列化后 = [{"address":"北京","age":"7","name":"jack"},{"address":["上海","深圳"],"age":"20","name":"tom"}]
num1   序列化后 = 2345.67
*/

JSON 解码示例:

package main
import ("fmt""encoding/json"
)// 定义一个结构体
type Monster struct {Name string  Age int Birthday string Sal float64Skill string
}// 将 json 字符串,反序列化成 struct
func unmarshalStruct() {// json str 一般不是直接写入,在项目开发中,是通过网络传输获取到,或者是读取文件获取到str := "{\"Name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"牛魔拳\"}"var monster Monstererr := json.Unmarshal([]byte(str), &monster)if err != nil {fmt.Printf("unmarshal err=%v\n", err)}fmt.Printf("反序列化后 monster = %v monster.Name = %v \n", monster, monster.Name)
}// 将 json 字符串,反序列化成 map
func unmarshalMap() {str := "{\"address\":\"洪崖洞\",\"age\":30,\"name\":\"红孩儿\"}"// 定义一个 mapvar a map[string]interface{} // 注意:反序列化 map,不需要 make,因为 make 操作被封装到 Unmarshal 函数err := json.Unmarshal([]byte(str), &a)if err != nil {fmt.Printf("unmarshal err=%v\n", err)}fmt.Printf("反序列化后 map a = %v\n", a)}// 将 json 字符串,反序列化成切片
func unmarshalSlice() {str := "[{\"address\":\"北京\",\"age\":\"7\",\"name\":\"jack\"}," + "{\"address\":[\"上海\",\"深圳\"],\"age\":\"20\",\"name\":\"tom\"}]"   //定义一个slicevar slice []map[string]interface{}//反序列化,不需要make,因为make操作被封装到 Unmarshal函数err := json.Unmarshal([]byte(str), &slice)if err != nil {fmt.Printf("unmarshal err=%v\n", err)}fmt.Printf("反序列化后 slice = %v\n", slice)
}func main() {unmarshalStruct()unmarshalMap()unmarshalSlice()
}/*
反序列化后 monster = {牛魔王 500 2011-11-11 8000 牛魔拳} monster.Name = 牛魔王
反序列化后 map a = map[address:洪崖洞 age:30 name:红孩儿]
反序列化后 slice = [map[address:北京 age:7 name:jack] map[address:[上海 深圳] age:20 name:tom]]
*/

类型示例:

package mainimport ("encoding/json""fmt"
)/* 敲黑板,划重点:当要将结构体对象转换为 JSON 时,对象中的属性首字母必须是大写,才能正常转换为 JSON。*/
type Person struct {ID        intFirstName string `json:"name"`              // JSON 输出显示 name 而不是 FirstName LastName  stringAddress   string `json:"address,omitempty"` // 忽略空字段
}type Employee struct {PersonManagerID int
}func main() {employees := []Employee{Employee{Person: Person{LastName: "Doe", FirstName: "John",},},Employee{Person: Person{LastName: "Campbell", FirstName: "David",},},}data, _ := json.Marshal(employees)      // 若要将结构编码为 JSON,请使用 json.Marshal 函数fmt.Printf("%s\n", data)var decoded []Employeejson.Unmarshal(data, &decoded)          // 若要将 JSON 字符串解码为数据结构,请使用 json.Unmarshal 函数fmt.Printf("%v", decoded)
}/*
[{"ID":0,"name":"John","LastName":"Doe","ManagerID":0},{"ID":0,"name":"David","LastName":"Campbell","ManagerID":0}]
[{{0 John Doe } 0} {{0 David Campbell } 0}]
*/

十二、方法

Go 中的方法是一种特殊类型的函数,但存在一个简单的区别:你必须在函数名称之前加入一个额外的参数。 此附加参数称为 接收方

如你希望分组函数并将其绑定到自定义类型,则方法非常有用。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。

12.1 声明方法

package mainimport "fmt"/* 在声明方法之前,必须先创建结构*/
type triangle struct {size int
}type square struct {size int
}/* 附加的额外参数(t triangle)就是接收方*/
func (t triangle) perimeter() int {return t.size * 3
}/* 此方法属于不同的结构,可以为其指定相同的名称*/
func (s square) perimeter() int {return s.size * 4
}func main() {/* 如果尝试按平常的方式调用 perimeter() 函数,则此函数将无法正常工作,因为此函数的签名表明它需要接收方。* 因此,调用此方法的唯一方式是先声明一个结构,获取此方法的访问权限。* 通过对 perimeter() 函数的两次调用,编译器将根据接收方类型来确定要调用的函数。* 这有助于在各程序包之间保持函数的一致性和名称的简短,并避免将包名称作为前缀。*/t := triangle{3}s := square{4}fmt.Println("Perimeter (triangle):", t.perimeter())fmt.Println("Perimeter (square):", s.perimeter())
}/*
Perimeter (triangle): 9
Perimeter (square): 16
*/

方法的一个关键方面在于,可以为任何类型定义方法,而不只是针对自定义类型(如结构)进行定义。 但是,你不能通过属于其他包的类型来定义结构。 因此,不能在基本类型(如 string)上创建方法。

12.2 方法的重载

package mainimport "fmt"/* 结构体:三角形*/
type triangle struct {size int
}/* 结构体:彩色三角形* 方法可以重用来自一个结构的属性,以避免出现重复并保持代码库的一致性。 * 即使接收方不同,也可以调用已嵌入结构的方法。
*/
type coloredTriangle struct {trianglecolor string
}/* 三角形大小增加3倍*/
func (t triangle) perimeter() int {return t.size * 3
}/* 重载方法* 可以在 coloredTriangle 结构中更改 perimeter() 方法的实现* 因为方法需要额外参数(接收方),所以,可以使用一个同名的方法,只要此方法专门用于要使用的接收方即可。
*/
func (t coloredTriangle) perimeter() int {return t.size * 3 * 2
}func main() {t := triangle{3}                                    // 定义一个三角形fmt.Println("Perimeter (triangle):", t.perimeter())t1 := coloredTriangle{triangle{5}, "blue"}          // 定义一个彩色三角形fmt.Println("Size:", t1.size)/* 如果没有写重载方法 func (t coloredTriangle) perimeter() int,此处会从 triangle 结构调用 perimeter() 方法,而不必重新创建彩色三角形的方法* 如果写了重载方法 func (t coloredTriangle) perimeter() int,此处会从 coloredTriangle 结构调用 perimeter() 方法*/fmt.Println("Perimeter", t1.perimeter())            /* 如果你仍需要从 triangle 结构调用 perimeter() 方法,则可通过对其进行显示访问来执行此操作。*/fmt.Println("Perimeter (normal)", t1.triangle.perimeter())
}/*
Perimeter (triangle): 9
Size: 5
Perimeter 30
Perimeter (normal) 15
*/

在 Go 中,你可以 重载 方法,并在需要时仍访问 原始 方法。

12.3 方法的封装

在 Go 中,只需使用大写标识符,即可公开方法(public),使用非大写的标识符将方法设为私有方法(private)。

Go 中的封装仅在程序包之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。

比如,创建新程序包 geometry :

package geometrytype Triangle struct {size int
}func (t *Triangle) doubleSize() {t.size *= 2
}func (t *Triangle) SetSize(size int) {t.size = size
}func (t *Triangle) Perimeter() int {t.doubleSize()return t.size * 3
}

你可以使用上述程序包,具体如下所示:

func main() {t := geometry.Triangle{}t.SetSize(3)fmt.Println("Perimeter", t.Perimeter())
}

此时你应获得以下输出:

Perimeter 18

如要尝试从 main() 函数中调用 size 字段或 doubleSize() 方法,程序将死机,如下所示:

func main() {t := geometry.Triangle{}t.SetSize(3)fmt.Println("Size", t.size)fmt.Println("Perimeter", t.Perimeter())
}

在运行前面的代码时,你将看到以下错误:

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)

12.4 函数与方法的区别

(1)含义不同

  • 函数function是⼀段具有独⽴功能的代码,可以被反复多次调⽤,从⽽实现代码复⽤。⽽⽅法method是⼀个类的⾏为功能,只有该类的对象才能调⽤。

(2)⽅法有接受者,⽽函数⽆接受者

  • Go语⾔的⽅法method是⼀种作⽤于特定类型变量的函数,这种特定类型变量叫做Receiver(接受者、接收者、接收器);
  • 接受者的概念类似于传统⾯向对象语⾔中的this或self关键字;
  • Go语⾔的接受者强调了⽅法具有作⽤对象,⽽函数没有作⽤对象;
  • ⼀个⽅法就是⼀个包含了接受者的函数;
  • Go语⾔中, 接受者的类型可以是任何类型,不仅仅是结构体, 也可以是struct类型外的其他类型。

(3)函数不可以重名,⽽⽅法可以重名

  • 只要接受者不同,则⽅法名可以⼀样。

(4)调用方式不一样

  • 方法由struct对象通过(.点号)调用,而函数是直接调用。

十三、接口

与其他编程语言中的接口不同,Go 中的接口是满足隐式实现的。

示例:编写自定义 String() 方法来打印自定义字符串,具体如下所示:

package mainimport "fmt"type Person struct {Name, Country string
}func (p Person) String() string {return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}func main() {rs := Person{"John Doe", "USA"}ab := Person{"Mark Collins", "United Kingdom"}fmt.Printf("%s\n%s\n", rs, ab)
}/*
John Doe is from USA
Mark Collins is from United Kingdom
*/

如你所见,你已使用自定义类型(结构)来写入 String() 方法的自定义版本。 这是在 Go 中实现接口的一种常用方法。

13.1 示例:interface声明与使用方法

package main
import "fmt"// 声明/定义一个接口
type Usb interface {                     Start() Stop()
}type Phone struct {}  // 让Phone 实现 Usb接口的方法
func (p Phone) Start() {fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {fmt.Println("手机停止工作。。。")
}type Camera struct {}// 让Camera 实现 Usb接口的方法
func (c Camera) Start() {fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {fmt.Println("相机停止工作。。。")
}// Computer,可以识别 Phone 和 Camera 的接口
type Computer struct {}                   /* 编写一个 Working 方法,接收一个Usb接口类型变量* 该变量必须实现了 Usb 接口声明的所有方法* 通过usb接口变量来调用Start和Stop方法* 这里实际体现了多态特性,因为同一个参数可以接收多个类型变量
*/
func (c Computer) Working(usb Usb) {usb.Start()                           usb.Stop()
}func main() {// 创建结构体变量computer := Computer{}phone := Phone{}camera := Camera{}// 接口的使用computer.Working(phone)computer.Working(camera)
}/*
手机开始工作。。。
手机停止工作。。。
相机开始工作。。。
相机停止工作。。。*/

13.2 示例:创建用于管理在线商店的程序包

编写一个程序,此程序使用自定义程序包来管理在线商店的帐户。

  1. 创建一个名为 Account 的自定义类型,此类型包含帐户所有者的名字和姓氏。 此类型还必须加入 ChangeName 的功能。

  2. 创建另一个名为 Employee 的自定义类型,此类型包含用于将贷方数额存储为类型 float64 并嵌入 Account 对象的变量。 类型还必须包含 AddCreditsRemoveCredits 和 CheckCredits 的功能。 你需要展示你可以通过 Employee 对象更改帐户名称。

  3. 将字符串方法写入 Account 对象,以便按包含名字和姓氏的格式打印 Employee 名称。

  4. 最后,编写使用已创建程序包的程序,并测试此挑战中列出的所有功能。 也就是说,主程序应更改名称、打印名称、添加贷方、删除贷方以及检查余额。

下方是适用于商店程序包的代码:

package storeimport ("errors""fmt"
)type Account struct {FirstName stringLastName  string
}type Employee struct {AccountCredits float64
}func (a *Account) ChangeName(newname string) {a.FirstName = newname
}func (e Employee) String() string {return fmt.Sprintf("Name: %s %s\nCredits: %.2f\n", e.FirstName, e.LastName, e.Credits)
}func CreateEmployee(firstName, lastName string, credits float64) (*Employee, error) {return &Employee{Account{firstName, lastName}, credits}, nil
}func (e *Employee) AddCredits(amount float64) (float64, error) {if amount > 0.0 {e.Credits += amountreturn e.Credits, nil}return 0.0, errors.New("Invalid credit amount.")
}func (e *Employee) RemoveCredits(amount float64) (float64, error) {if amount > 0.0 {if amount <= e.Credits {e.Credits -= amountreturn e.Credits, nil}return 0.0, errors.New("You can't remove more credits than the account has.")}return 0.0, errors.New("You can't remove negative numbers.")
}func (e *Employee) CheckCredits() float64 {return e.Credits
}

下方是主程序用于测试所有功能的代码:

package mainimport ("fmt""store"
)func main() {bruce, _ := store.CreateEmployee("Bruce", "Lee", 500)fmt.Println(bruce.CheckCredits())credits, err := bruce.AddCredits(250)if err != nil {fmt.Println("Error:", err)} else {fmt.Println("New Credits Balance = ", credits)}_, err = bruce.RemoveCredits(2500)if err != nil {fmt.Println("Can't withdraw or overdrawn!", err)}bruce.ChangeName("Mark")fmt.Println(bruce)
}

13.3 示例:通过实现接口对结构体切片进行排序

package main
import ("fmt""sort""math/rand"
)type  Hero struct{                          // 声明Hero结构体Name stringAge int
}type HeroSlice []Hero                       // 声明一个Hero结构体切片类型// 实现Interface 接口1(sort.Sort要求)
func (hs HeroSlice) Len() int {return len(hs)
}// 实现Interface 接口2(sort.Sort要求)
// Less方法就是决定你使用什么标准进行排序
func (hs HeroSlice) Less(i, j int) bool {return hs[i].Age < hs[j].Age            // 按Hero的Age从小到大排序//return hs[i].Name < hs[j].Name           // 按Hero的Name排序,实际上可以按结构体的任意字段进行排序
}// 实现Interface 接口3(sort.Sort要求)
func (hs HeroSlice) Swap(i, j int) {hs[i], hs[j] = hs[j], hs[i]             // 交换
}func main() {  var intSlice = []int{0, -1, 10, 7, 90}  // 定义一个数组/切片sort.Ints(intSlice)                    // 对 intSlice切片进行排序,可以使用系统提供的一般方法fmt.Println(intSlice)/* 对结构体切片进行排序,本代码重点内容 */var heroes HeroSlice                    // 定义一个切片for i := 0; i < 10 ; i++ {              // 给切片赋值hero := Hero{Name : fmt.Sprintf("英雄|%d", rand.Intn(100)),Age : rand.Intn(100),}heroes = append(heroes, hero)     // 将 hero append到 heroes切片}for _ , v := range heroes {             // 排序前的顺序fmt.Println(v)}sort.Sort(heroes)                       // 调用sort.Sort,必须实现interface,才可以调用fmt.Println("-----------排序后------------")for _ , v := range heroes {             // 排序后的顺序fmt.Println(v)}}/*
[-1 0 7 10 90]
{英雄|81 87}
{英雄|47 59}
{英雄|81 18}
{英雄|25 40}
{英雄|56 0}
{英雄|94 11}
{英雄|62 89}
{英雄|28 74}
{英雄|11 45}
{英雄|37 6}
-----------排序后------------
{英雄|56 0}
{英雄|37 6}
{英雄|94 11}
{英雄|81 18}
{英雄|25 40}
{英雄|11 45}
{英雄|47 59}
{英雄|28 74}
{英雄|81 87}
{英雄|62 89}
*/

sort.Interface接口的说明:

// https://studygolang.com/pkgdoc
type Interface interface {// Len方法返回集合中的元素个数Len() int// Less方法报告索引i的元素是否比索引j的元素小Less(i, j int) bool// Swap方法交换索引i和j的两个元素Swap(i, j int)
}

一个满足sort.Interface接口的(集合)类型可以被本包的函数进行排序。方法要求集合中的元素可以被整数索引。

十四、类型断言

golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如 string , int , int64 甚至是自定义的 struct 类型都就此拥有了 interface{} 的接口。

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。

在Go语言中类型断言的语法格式如下:

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

  • 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
package main
import "fmt"type Point struct {x inty int
}func main() {var a interface{}var point Point = Point{1, 2}a = point  var b Pointb = a.(Point)        // 直接进行类型断言fmt.Println(b) var x interface{}var b2 float32 = 2.1x = b2               // 空接口,可以接收任意类型// 类型断言(带检测的)if y, ok := x.(float32); ok {fmt.Println("convert success")fmt.Printf("y 的类型是, %T 值是 %v\n", y, y)} else {fmt.Println("convert fail")}s := "hello world"if v, ok := interface{}(s).(string); ok {fmt.Println(v)}fmt.Println("继续执行...")
}/*
{1 2}
convert success
y 的类型是 float32, 值是 2.1
hello world
继续执行...
*/

接口变量的类型也可以使用一种特殊形式的 swtich 来检测。下面的代码片段展示了一个类型分类函数,它有一个可变长度参数,可以是任意类型的数组,它会根据数组元素的实际类型执行不同的动作:

func classifier(items ...interface{}) {for i, x := range items {switch x.(type) {case bool:fmt.Printf("Param #%d is a bool\n", i)case float64:fmt.Printf("Param #%d is a float64\n", i)case int, int64:fmt.Printf("Param #%d is a int\n", i)case nil:fmt.Printf("Param #%d is a nil\n", i)case string:fmt.Printf("Param #%d is a string\n", i)default:fmt.Printf("Param #%d is unknown\n", i)}}
}

十五、随机数

真随机和伪随机概念:

  1. 统计学伪随机性:在给定的随机比特流样本中,1 的数量大致等于 0 的数量,也就是说,“10”“01”“00”“11” 四者数量大致相等。说人话就是:“一眼看上去是随机的”。
  2. 密码学安全伪随机性:就是给定随机样本的一部分和随机算法,不能有效的演算出随机样本的剩余部分。
  3. 真随机性:其定义为随机样本不可重现。

根据以上几个标准,其对应的随机数也就分为以下几类:

  1. 伪随机数:满足第一个条件的随机数。
  2. 密码学安全的伪随机数:同时满足前两个条件的随机数。可以通过密码学安全伪随机数生成器计算得出
  3. 真随机数:同时满足三个条件的随机数

15.1 伪随机示例:math/rand

rand包实现了伪随机数生成器。

随机数从资源生成。包水平的函数都使用的默认的公共资源。该资源会在程序每次运行时都产生确定的序列。

如果需要每次运行产生不同的序列,应使用Seed函数进行初始化。

默认资源可以安全的用于多go程并发。

package main
import ("fmt""math/rand""time"
)func main() {// 返回一个取值范围在[0,n)的伪随机int值,如果n<=0会panic   // func Intn(n int) intrand1 := rand.Intn(100)                              //每次生成一个确定的值fmt.Println("rand1 = ", rand1)// 设置种子,使用给定的seed将默认资源初始化到一个确定的状态;如未调用Seed,默认资源的行为就好像调用了Seed(1)// func Seed(seed int64)// Unix将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位秒)// func (t Time) Unix() int64for i:=1; i<=10; i++{rand.Seed(time.Now().Unix())rand2 := rand.Intn(100)fmt.Println("rand2 = ", rand2)}// UnixNano将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位纳秒)// func (t Time) UnixNano() int64for i:=1; i<=10; i++{rand.Seed(time.Now().UnixNano())rand3 := rand.Intn(100)fmt.Println("rand3 = ", rand3)}}/*
rand1 =  81       //每次运行均输出该值
rand2 =  61       //每秒内输出相同的值
rand2 =  61
rand2 =  61
rand2 =  61
rand2 =  61
rand2 =  61
rand2 =  61
rand2 =  61
rand2 =  61
rand2 =  61
rand3 =  95       //每纳秒内输出相同的值
rand3 =  95
rand3 =  95
rand3 =  82
rand3 =  82
rand3 =  82
rand3 =  82
rand3 =  82
rand3 =  82
rand3 =  93
*/

15.2 真随机示例:crypto/rand

package mainimport ("crypto/rand""fmt""math/big"
)// crypto/rand包实现了用于加解密的更安全的随机数生成器
// 返回一个在[0, max)区间服从均匀分布的随机值,如果max<=0则会panic
// func Int(rand io.Reader, max *big.Int) (n *big.Int, err error)func main() {// 生成 10 个 [0, 100) 范围的真随机数for i := 0; i < 10; i++ {result, _ := rand.Int(rand.Reader, big.NewInt(100))fmt.Println(result)}
}/*
36
45
17
24
63
19
28
66
38
50
*/

十六、日期与时间

time包提供了时间的显示和测量用的函数。日历的计算采用的是公历。

16.1 type Time

type Time struct {// 内含隐藏或非导出字段
}

(1)Time代表一个纳秒精度的时间点;

(2)程序中应使用Time类型值来保存和传递时间,而不能用指针。就是说,表示时间的变量和字段,应为 time.Time 类型,而不是 *time.Time. 类型;

(3)一个Time类型值可以被多个go程同时使用;

(4)时间点可以使用 Before、After 和 Equal 方法进行比较;

(5)Sub方法让两个时间点相减,生成一个 Duration 类型值(代表时间段);

(6)Add方法给一个时间点加上一个时间段,生成一个新的Time类型时间点;

(7)Time零值代表时间点January 1, year 1, 00:00:00.000000000 UTC;因为本时间点一般不会出现在使用中,IsZero方法提供了检验时间是否显式初始化的一个简单途径。

(8)每一个时间都具有一个地点信息(及对应地点的时区信息),当计算时间的表示格式时,如 Format、Hour 和 Year 等方法,都会考虑该信息。Local、UTC和In方法返回一个指定时区(但指向同一时间点)的Time。修改地点/时区信息只是会改变其表示;不会修改被表示的时间点,因此也不会影响其计算。

16.2 示例:日期与时间

package main
import ("fmt""time"
)func main() {// 日期和时间相关函数和方法使用// 1. 获取当前时间now := time.Now()fmt.Printf("now=%v now type=%T\n", now, now)// 2.通过now可以获取到年月日,时分秒fmt.Printf("年=%v\n", now.Year())fmt.Printf("月=%v\n", now.Month())         //月份默认显示类型fmt.Printf("月=%v\n", int(now.Month()))    //月份类型转换fmt.Printf("日=%v\n", now.Day())fmt.Printf("时=%v\n", now.Hour())fmt.Printf("分=%v\n", now.Minute())fmt.Printf("秒=%v\n", now.Second())fmt.Printf("星期=%v\n", now.Weekday())fmt.Println(now.Date())                    //日期,返回三个参数fmt.Println(now.Clock())                  //时间,返回三个参数// 3.格式化日期时间fmt.Printf("当前日期和时间 %d-%d-%d %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())dateStr := fmt.Sprintf("当前日期和时间 %d-%d-%d %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())fmt.Printf("dateStr=%v\n", dateStr)// 格式化日期时间的第二种方式// Format根据layout指定的格式返回t代表的时间点的格式化文本表示。layout定义了参考时间:// Mon Jan 2 15:04:05 -0700 MST 2006fmt.Printf(now.Format("2006-01-02 15:04:05"))fmt.Println()fmt.Printf(now.Format("2006-01-02"))fmt.Println()fmt.Printf(now.Format("15:04:05"))fmt.Println()fmt.Printf(now.Format("2006"))fmt.Println()// 4.每隔0.1秒中打印一个数字,打印到10时就退出i := 0for {i++fmt.Println(i)// time.Sleep(time.Second)                   // 休眠,每1秒time.Sleep(time.Millisecond * 100)          // 休眠,每0.1秒if i == 10 {break}}// 5.Unix和UnixNano的使用fmt.Printf("unix时间戳 = %v\nunixnano时间戳 = %v\n", now.Unix(), now.UnixNano())}

16.3 type Duration

type Duration int64

Duration类型代表两个时间点之间经过的时间,以纳秒为单位。可表示的最长时间段大约290年。

const (Nanosecond  Duration = 1Microsecond          = 1000 * NanosecondMillisecond          = 1000 * MicrosecondSecond               = 1000 * MillisecondMinute               = 60 * SecondHour                 = 60 * Minute
)

常用的时间段。没有定义一天或超过一天的单元,以避免夏时制的时区切换的混乱。

要将Duration类型值表示为某时间单元的个数,用除法:

second := time.Second
fmt.Print(int64(second/time.Millisecond)) // prints 1000

要将整数个某时间单元表示为Duration类型值,用乘法:

seconds := 10
fmt.Print(time.Duration(seconds)*time.Second) // prints 10s

十七、cmd编译

17.1 两种编译方法

主要有两种 cmd 编译方法:

(1)go build:用于测试编译包,在项目目录下生成可执行文件(有main包)

(2)go install:主要用来生成库和工具

  • 一是编译包文件(无main包),将编译后的包文件放到 pkg 目录下($GOPATH/pkg)。
  • 二是编译生成可执行文件(有main包),将可执行文件放到 bin 目录($GOPATH/bin)。

相同点

  • 都能生成可执行文件

不同点

  • go build 不能生成包文件, go install 可以生成包文件
  • go build 生成可执行文件默认在当前目录下(可以通过参数指定生成目录), go install 生成可执行文件默认在bin目录下($GOPATH/bin

17.2 go build命令

go build [-o 输出名] [-i] [编译标记] [包名]

(1)如果参数为***.go文件或文件列表,则编译为一个个单独的包;

(2)当编译单个main包(文件),则生成可执行文件;

(3)当编译单个或多个包非主包时,只构建编译包,但丢弃生成的对象(.a),仅用作检查包可以构建;

(4)当编译包时,会自动忽略'_test.go'的测试文件。

17.3 go build示例

代码相对于 GOPATH 的目录关系如下:

D:\golang\.|└── src├─── chapter11|        └──── utils|        └──── gobuild|                ├──── lib.go|                └──── main.go└─── chapter12└──── event└──── main.go

(1)如果源码中没有依赖 GOPATH 的包引用,那么这些源码可以使用无参数 go build

// 在代码所在目录(./src/chapter11/gobuild)下使用 go build 命令
> cd src/chapter11/gobuild/
> go build
  • go build 在编译开始时,会搜索当前目录的 go 源码。这个例子中,go build 会找到 lib.go 和 main.go 两个文件。
  • 编译这两个文件后,生成当前目录名的可执行文件并放置于当前目录下,这里生成的可执行文件是 gobuild.exe 。

(2)编译同目录的多个源码文件时,可以在 go build 的后面提供多个文件名,go build 会编译这些源码,输出可执行文件

> cd src/chapter11/gobuild/> go build main.go lib.go                 //编译结果,生成main.exe
> go build lib.go main.go                 //编译结果,生成lib.exe
> go build -o test.exe main.go lib.go     //编译结果,生成test.exe
  • 使用“go build+文件列表”方式编译时,可执行文件默认选择文件列表中第一个源码文件作为可执行文件名输出。
  • 使用“go build+文件列表”方式编译时,文件列表中的每个文件必须是同一个包的 Go 源码。

(3)“go build+包”方式编译;在设置 GOPATH 后,可以直接根据包名进行编译

D:\> cd golangD:\golang> go build chapter11/gobuild                     // 后面接要编译的包名;包名是相对于 GOPATH 下的 src 目录开始的,生成默认的文件名 main.exe,保存在目录 GOPATH 下
D:\golang> go build -o test.exe chapter11/gobuild         // -o执行指定输出文件名为 test.exe,保存在目录 GOPATH 下
D:\golang> go build -o bin/test.exe chapter11/gobuild     // -o执行指定输出目录bin(GOPATH/bin),输出文件名为 test.exe

注意 :GOPATH 下的目录结构,源码必须放在 GOPATH 下的 src 目录下。所有目录中不要包含中文。

17.4 go install命令

(1)go install 命令的功能和 go build 命令类似,附加参数绝大多数都可以与 go build 通用。

(2)go install 只是将编译的中间文件放在 GOPATH 的 pkg 目录下,以及固定地将编译结果放在 GOPATH 的 bin 目录下。

(3)这个命令在内部实际上分成了两步操作:

  • 第一步是生成结果文件(可执行文件或者 .a 包),
  • 第二步会把编译好的结果移到 $GOPATH/pkg 或者 $GOPATH/bin。
go install [包名]

go install 的编译过程有如下规律:

(1)go install 是建立在 GOPATH 上的,无法在独立的目录里使用 go install;

(2)GOPATH 下的 bin 目录放置的是使用 go install 生成的可执行文件,可执行文件的名称来自于编译时的包名;

(3)go install 输出目录始终为 GOPATH 下的 bin 目录,无法使用 -o 附加参数进行自定义;

(4)GOPATH 下的 pkg 目录放置的是编译期间的中间文件。

17.5 go install示例

D:\> cd golangD:\golang> go install chapter11/gobuild

编译完成后的目录结构如下:

D:\golang\.|├── bin│    └─── gobuild.exe|├── pkg│    └─── chapter11│             └─── gobuild│                     └── lib.a|└── src├─── chapter11|        └──── utils|        └──── gobuild|                ├──── lib.go|                └──── main.go└─── chapter12└──── event└──── main.go

十八、错误处理策略

Go 的错误处理方法只是一种只需要 if 和 return 语句的控制流机制。

18.1 示例

package mainimport ("fmt""os"
)type Employee struct {ID        intFirstName stringLastName  stringAddress   string
}func main() {employee, err := getInformation(1001)if err != nil {// Something is wrong. Do something.} else {fmt.Print(employee)}
}func getInformation(id int) (*Employee, error) {employee, err := apiCallEmployee(1000)return employee, err
}func apiCallEmployee(id int) (*Employee, error) {employee := Employee{LastName: "Doe", FirstName: "John"}return &employee, nil
}

18.2 处理策略:

当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。 因此,一个常见策略是继续使用该模式在子例程中传播错误。 例如,子例程(如上一示例中的 getInformation)可能会将错误返回给调用方,而不执行其他任何操作,如下所示:

func getInformation(id int) (*Employee, error) {employee, err := apiCallEmployee(1000)if err != nil {return nil, err // Simply return the error to the caller.}return employee, nil
}

你可能还需要在传播错误之前添加更多信息。 为此,可以使用 fmt.Errorf() 函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误,如下所示:

func getInformation(id int) (*Employee, error) {employee, err := apiCallEmployee(1000)if err != nil {return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)}return employee, nil
}

另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟,如下所示:

func getInformation(id int) (*Employee, error) {for tries := 0; tries < 3; tries++ {employee, err := apiCallEmployee(1000)if err == nil {return employee, nil}fmt.Println("Server is not responding, retrying ...")time.Sleep(time.Second * 2)}return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

最后,可以记录错误并对最终用户隐藏任何实现详细信息,而不是将错误打印到控制台。

创建可重用的错误:

有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在 Go 中,你可以使用 errors.New() 函数创建错误并在若干部分中重复使用这些错误,如下所示:

var ErrNotFound = errors.New("Employee not found!")func getInformation(id int) (*Employee, error) {if id != 1001 {return nil, ErrNotFound}employee := Employee{LastName: "Doe", FirstName: "John"}return &employee, nil
}

18.3 用于错误处理的推荐做法

在 Go 中处理错误时,请记住下面一些推荐做法:

  • 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
  • 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
  • 创建尽可能多的可重用错误变量。
  • 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
  • 在记录错误时记录尽可能多的详细信息,并打印出最终用户能够理解的错误。

十九、日志记录

19.1 log包

Go 提供了一个用于处理日志的简单标准包。 可以像使用 fmt 包一样使用此包。 该标准包不提供日志级别,且不允许为每个包配置单独的记录器。 如果需要编写更复杂的日志记录配置,可以使用记录框架执行此操作。

log.Print() 函数将日期和时间添加为日志消息的前缀。

import ("log"
)func main() {log.Print("Hey, I'm a log!")
}/*
2020/12/19 13:39:17 Logging in Go!
*/

log.Fatal() 函数记录错误并结束程序,就像使用 os.Exit(1) 一样。

package mainimport ("fmt""log"
)func main() {log.Fatal("Hey, I'm an error log!")fmt.Print("Can you see me?")
}/*
2020/12/19 13:53:19  Hey, I'm an error log!
exit status 1
*/

注意最后一行 fmt.Print("Can you see me?") 未运行。 这是因为 log.Fatal() 函数调用停止了该程序。

在使用 log.Panic() 函数时会出现类似行为,该函数也调用 panic() 函数,如下所示:

package mainimport ("fmt""log"
)func main() {log.Panic("Hey, I'm an error log!")fmt.Print("Can you see me?")
}/*
2020/12/19 13:53:19  Hey, I'm an error log!
panic: Hey, I'm an error log!goroutine 1 [running]:
log.Panic(0xc000060f58, 0x1, 0x1)/usr/local/Cellar/go/1.15.5/libexec/src/log/log.go:351 +0xae
main.main()/Users/christian/go/src/helloworld/logs.go:9 +0x65
exit status 2
*/

你仍获得日志消息,但现在还会获得错误堆栈跟踪。

另一重要函数是 log.SetPrefix()。 可使用它向程序的日志消息添加前缀。 例如,可以使用以下代码片段:

package mainimport ("log"
)func main() {log.SetPrefix("main(): ")log.Print("Hey, I'm a log!")log.Fatal("Hey, I'm an error log!")
}/*
main(): 2021/01/05 13:59:58 Hey, I'm a log!
main(): 2021/01/05 13:59:58 Hey, I'm an error log!
exit status 1
*/

19.2 记录到文件

除了将日志打印到控制台之外,你可能还希望将日志发送到文件,以便稍后或实时处理这些日志。

为什么想要将日志发送到文件? 首先,你可能想要对最终用户隐藏特定信息。 他们可能对这些信息不感兴趣,或者你可能公开了敏感信息。 在文件中添加日志后,可以将所有日志集中在一个位置,并将它们与其他事件关联。 此模式为典型模式:具有可能是临时的分布式应用程序,例如容器。

让我们使用以下代码测试将日志发送到文件:

package mainimport ("log""os"
)func main() {file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)if err != nil {log.Fatal(err)}defer file.Close()log.SetOutput(file)log.Print("Hey, I'm a log!")
}

运行前面的代码时,在控制台中看不到任何内容。 在目录中,你应看到一个名为 info.log 的新文件,其中包含使用 log.Print() 函数发送的日志。 请注意,需要首先创建或打开文件,然后将 log 包配置为将所有输出发送到文件。 然后,可以像通常做法那样继续使用 log.Print() 函数。

二十、反射

示例:

package main
import ("fmt""reflect"
)// 定义了一个Monster结构体
type Monster struct {Name  string `json:"name"`Age   int `json:"monster_age"`Score float32 Sex   string}// 方法的排序默认是按照函数名的排序(ASCII码)
// 方法,返回两个数的和,在 3 个方法中排第1,标号0
func (s Monster) GetSum(n1, n2 int) int {return n1 + n2
}
// 方法, 接收四个值,给s赋值,在 3 个方法中排第3,标号2
func (s Monster) Set(name string, age int, score float32, sex string) {s.Name = names.Age = ages.Score = scores.Sex = sex
}// 方法,显示s的值,在 3 个方法中排第2,标号1
func (s Monster) Print() {fmt.Println("------start------")fmt.Println(s)fmt.Println("-------end-------")
}func TestStruct(a interface{}) {typ := reflect.TypeOf(a)                // 获取reflect.Type 类型fmt.Println("reflect.TypeOf",typ)val := reflect.ValueOf(a)               // 获取reflect.Value 类型fmt.Println("reflect.ValueOf",val)kd := val.Kind()                          // 获取到a对应的类别fmt.Println("val.Kind",kd)fmt.Println()if kd !=  reflect.Struct {                // 如果传入的不是struct,就退出fmt.Println("expect struct")return}num := val.NumField()                      // 获取到该结构体有几个字段fmt.Printf("struct has %d fields\n", num) // 变量结构体的所有字段for i := 0; i < num; i++ {fmt.Printf("Field %d: 值为=%v\n", i, val.Field(i)) // 获取到该结构体的字段tagVal := typ.Field(i).Tag.Get("json")             // 获取到struct标签, 注意需要通过reflect.Type来获取tag标签的值if tagVal != "" {                                  // 如果该字段有tag标签就显示,否则就不显示fmt.Printf("Field %d: tag为=%v\n", i, tagVal)}} fmt.Println()numOfMethod := val.NumMethod()                           // 获取到该结构体有多少个方法fmt.Printf("struct has %d methods\n", numOfMethod)val.Method(1).Call(nil)                                // 调用第 2 个方法;方法的排序默认是按照 函数名的排序(ASCII码)// 调用结构体的第1个方法Method(0)var params []reflect.Value                             // 声明了 []reflect.Valueparams = append(params, reflect.ValueOf(10))params = append(params, reflect.ValueOf(40))res := val.Method(0).Call(params)                      // 传入的参数是 []reflect.Value, 返回[]reflect.Valuefmt.Println("res=", res[0].Int())                      // 返回结果, 返回的结果是 []reflect.Value
}func main() {//创建了一个Monster实例var a Monster = Monster{Name:  "huangshulang",Age:   400,Score: 30.8,}//将Monster实例传递给TestStruct函数TestStruct(a)
}/*
reflect.TypeOf main.Monster
reflect.ValueOf {huangshulang 400 30.8 }
val.Kind structstruct has 4 fields
Field 0: 值为=huangshulang
Field 0: tag为=name
Field 1: 值为=400
Field 1: tag为=monster_age
Field 2: 值为=30.8
Field 3: 值为=struct has 3 methods
------start------
{huangshulang 400 30.8 }
-------end-------
res= 50
*/

二十一、nil

(1)nil 指针解引用会令程序崩溃;

(2)方法可以通过简单的措施类防范接收 nil 值;

示例:

package mainimport "fmt"// 定义一个结构体
type person struct{age int
}// 结构体方法中,处理nil
func (p *person) birthday(){if p != nil {p.age++}
}func main(){// 普通指针var nowhere *intfmt.Println(nowhere)                    // nil//fmt.Println(*nowhere)                 // panic.go //go:nosplitif nowhere != nil{                      // 解决方法fmt.Println(*nowhere)}    // 结构体var nobody *personfmt.Println(nobody)                     // nilnobody.birthday()                       // 如果方法中没有处理nil,panic.go// 函数var fn func(a,b int) intfmt.Println(fn == nil)                  // true,因为 fn 没有被赋予任何函数// 切片var soup []stringfmt.Println(soup == nil)                // true fmt.Println(len(soup))                  // len 可以处理 nil 切片for _,ingredient := range soup{         // range 可以处理 nil 切片fmt.Println(ingredient)             // 0}soup = append(soup,"onion","celery")    // append 可以处理 nil 切片fmt.Println(soup)                       // [onion celery]// 接口var v interface{}                       // 接口变量的值和类型都是 nil,该变量是 nilfmt.Printf("%T %v %v \n",v,v,v == nil)  // nil nil truevar p *intv = p                                   // 接口变量的值是 nil,但类型不是nil,该变量就不是 nilfmt.Printf("%T %v %v \n",v,v,v == nil)  // *int nil false
}/*
<nil>
<nil>
true
true
0
[onion celery]
<nil> <nil> true
*int <nil> false
*/

第61篇 笔记-Go 基础相关推荐

  1. 《算法笔记》——基础篇习题选择结构

    <算法笔记>--基础篇习题 第二章 C/C++快速入门--2.3选择结构 [习题A] 一元二次方程求根 Problem Description Thinking Notes Code Im ...

  2. 深度学习word2vec笔记之基础篇

    深度学习word2vec笔记之基础篇 声明: 1)该博文是多位博主以及多位文档资料的主人所无私奉献的论文资料整理的.具体引用的资料请看参考文献.具体的版本声明也参考原文献 2)本文仅供学术交流,非商用 ...

  3. jqGrid 学习笔记整理——基础篇

    jqGrid 学习笔记整理--基础篇 jqGrid 实例中文版网址:http://blog.mn886.net/jqGrid/ 国外官网:http://www.trirand.com/blog/ 本人 ...

  4. 极客时间 Redis核心技术与实战 笔记(基础篇)

    Redis 概览 Redis 知识全景图 Redis 问题画像图 基础篇 基本架构 数据结构 数据类型和底层数据结构映射关系 全局哈希表 链式哈希解决哈希冲突 渐进式 rehash 不同数据结构查找操 ...

  5. Java学习笔记之基础篇

    Java学习笔记之基础篇 目录 Java如何体现平台的无关性? 面向对象(OO)的理解 面向对象和面向过程编程的区别 面向对象三大特征 静态绑定和动态绑定(后期绑定) 延伸:类之间的关系 组合(聚合) ...

  6. 1、Latex学习笔记之基础入门篇

    目录 一.Latex基础 1.架构 2.引用.脚注 3.单栏.双栏 4.常用快捷键 5.宏包 6.空格 7.换行.行间距 8.换段 9.下划线 10.引号 11.注释 12.字体 13.缩进 14.超 ...

  7. HTML5学习笔记 —— JavaScript基础知识

    HTML5学习笔记 -- JavaScript基础知识 标签: html5javascriptweb前端 2017-05-11 21:51 883人阅读 评论(0) 收藏 举报 分类: JavaScr ...

  8. 学习MSCKF笔记——四元数基础

    学习MSCKF笔记--四元数基础 学习MSCKF笔记--四元数基础 1. 四元数基本性质 1.1 加法 1.2 乘法 1.3 共轭 1.4 模 1.5 逆 1.6 单位四元数 1.7 指数 1.8 对 ...

  9. php基础教学笔记,php学习笔记:基础知识

    php学习笔记:基础知识 2.每行结尾不允许有多余的空格 3.确保文件的命名和调用大小写一致,是由于类Unix系统上面,对大小写是敏感的 4.方法名只允许由字母组成,下划线是不允许的,首字母要小写,其 ...

  10. 2023java面试看完这篇笔记薪资和offer稳了!

    新的一年抓住机会,不管跳槽涨薪,还是学习提升,这篇笔记你都不应该错过. 为了帮大家节约时间,整理了这篇**[Java面试核心知识点整理]以及[金三银四高频面试合集]**希望大家在新的一年都能拿到理想的 ...

最新文章

  1. 在计算机网络中光缆的工作原理是什么,计算机网络原理期中考试试卷(A)
  2. React Native开发之npm start加速
  3. 冒泡排序python实现
  4. 几天后自动领取java怎么做的_java获取几天前和几天后的日期
  5. 2019年自考计算机应用基础(实践),2019年自考计算机应用基础模拟题及答案(8)...
  6. 微软面向初学者的机器学习课程:1.1-机器学习介绍
  7. .NET Conf 2020 - 基于ASP.NET Core构建可热插拔的插件化系统
  8. Java命令行界面(第20部分):JSAP
  9. phpstudy(自己电脑主机做服务器,手机网站界面打不开)
  10. linux socket bind 内核详解,Socket与系统调用深度分析(示例代码)
  11. 計算機二級-java09
  12. 最新软件外包公司排名-中国IT人力外包公司排名
  13. 方舟外服服务器网站,方舟外服开服表,固定更新
  14. typora 公式对齐_让 Markdown 写作更简单 Typora 完全使用指南
  15. 367个公益宣传PPT模板免费下载网站
  16. 微信小程序中进行地图导航
  17. javaweb实现邮箱验证码
  18. 软件需求工程 高校教学平台 测试报告
  19. 【requests库】爬取Pixiv日榜图片 并保存到本地
  20. Apple pay 苹果支付

热门文章

  1. fortran快速入门
  2. 鼠标键盘控制多台计算机,一个软件即可一套键盘鼠标控制多台电脑
  3. C语言知识点总结 (一 )
  4. nit计算机应用基础考试系统,NIT考试计算机应用基础试题
  5. 程序员代码大全c语言,程序员大神教你,新手零基础学C语言编程代码训练
  6. Alex 的 Hadoop 菜鸟教程: 第15课 Impala 安装使用教程
  7. DBeaver连接GBase数据库
  8. 声道测试音频_一音成佛的尺八音色,电吹管的单声道和双声道录音对比(2)
  9. Unity基础——List的用法
  10. 如何发挥Intel傲腾持久内存最大能力?