go学习-part1
简介
背景
在这学期的区块链以及数据库的课程中,需要我们使用go语言来编写Fabric中的链码、共识算法等程序。那么go语言为什么这么有用呢?
go语言执行速度更快。因为go可以直接将代码编译成machine code,不像python这类解释性语言,需要一个python解释器。在执行速度方面,Golang总是比Java领先一步。基于Golang的程序速度超快,编译也很迅速, 开发人员喜欢使用Golang来满足更快的后端开发的要求。
go有着活跃的开发者社区。目前,有超过100万的开发者精通Golang的工作。这个数字预计在未来会有更大的增长。拥有一个强大而活跃的开发者社区,可以确保在开发过程中遇到的任何问题都能得到支持。
go有着全面的开发工具。 诚然,它没有类似于Java的庞大的第三方工具,然而,Go配备了一套全面的工具,使开发人员的编码变得简单。Go提供的IDE,如Visual Studio Code、Goland等
此外,go语言可以通过内嵌c代码的形式调用c语言,也可以通过调用共享库函数的方式实现; 至于c语言调用go函数,则可以通过go build将go代码编译成共享库提供给c代码使用
可扩展性强。在为一个项目选择编程语言时,可扩展性往往是一个重要的因素。没有人希望在以后需要为应用程序引入新功能时被卡住。Golang提供了更大的可扩展性空间。它提供了在同一时间执行多种功能的可能性。当你选择Golang时,你可以在未来更长时间内使用它。
与此同时,go语言也存在着一些缺点:
- 编程更消耗时间。Golang并不像Python一样具有解释性,而是一种编译型语言,所以实现同一个功能所需要的代码量要比python更多
- golang不支持泛型。如果不支持泛型,代码的重复性可能会很高,因为需要重写多个函数来处理不同类型的参数。这就像Golang所基于的C语言一样,缺乏对通用函数的支持会严重限制代码的可重用性,降低开发过程中的效率。
项目结构
在进行Go语言开发的时候,我们的代码总是会保存在$GOPATH/src
目录下。在工程经过go build
、go install
或go get
等指令后,会将下载的第三方包源代码文件放在 目录下,产生的二进制可执行文件放在 $GOPATH/bin
目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg
下。
如何编译
go build
go build
这个指令用来编译指定 packages 里的源码文件以及它们的依赖包,编译的时候会到 $goPath/src/package
路径下寻找源码文件。go build 还可以直接编译指定的源码文件,并且可以同时指定多个。
usage: go build [-o output] [-i] [build flags] [packages]
-o
只能在编译单个包的时候出现,它指定输出的可执行文件的名字。
-i
会安装编译目标所依赖的包,安装是指生成与代码包相对应的.a
文件,即静态库文件(后面要参与链接),并且放置到当前工作区的 pkg 目录下,且库文件的目录层级和源码层级一致。build flags 参数,
build, clean, get, install, list, run, test
这些命令会共用一套:
参数 | 作用 |
---|---|
-a | 强制重新编译所有涉及到的包,包括标准库中的代码包,这会重写 /usr/local/go 目录下的 .a 文件 |
-n | 打印命令执行过程,不真正执行 |
-p n | 指定编译过程中命令执行的并行数,n 默认为 CPU 核数 |
-race | 检测并报告程序中的数据竞争问题 |
-v | 打印命令执行过程中所涉及到的代码包名称 |
-x | 打印命令执行过程中所涉及到的命令,并执行 |
-work | 打印编译过程中的临时文件夹。通常情况下,编译完成后会被删除 |
我们拿区块链上的一个项目来展示如何编译。首先,这个项目结构如下图所示:
- 主目录下有naive.go, 其实就是main包,里面有个func main函数.这是go程序的入口
- zkv模块,是一个简单的键值对数据库
- zlog模块,用于日志记录、打印
- zpbft模块,实现算法
我们可以用 go build naive.go
即可编译naive.go文件,得到mac下的可执行文件 naive
也可以用go build -o main2 main.go
讲编译得到的文件命名为main2
当然,我们可以用更复杂的编译指令: go build -v -x -work -o bin/hello main.go
(naive被改名了)。-v
会打印所编译过的包名字,-x
打印编译期间所执行的命令,-work
打印编译期间生成的临时文件路径,并且编译完成之后不会被删除。
执行结果如下,我们看到,一开始编译了一系列package文件,在编译这些文件的时候调用了go语言的静态库(.a文件)。在EOF指令出现后,说明已经编译完成,然后会将编译好的静态文件相连接,最终生成可执行文件,并将其移动到bin目录下,改名为hello
go install
go install
用于编译并安装指定的代码包及它们的依赖包。相比 go build
,它只是多了一个“安装编译后的结果文件到指定目录”的步骤。
使用这条指令,会在GOPATH目录下的pkg文件夹中生成.a文件,在bin文件夹生成可执行文件
go run
go run
用于编译并运行命令源码文件。如 go run -x -work main.go
gofmt
gofmt
可以帮我们吧源代码文件排列的更好。比如说当我们打印一些多余的空格的时候,会将空格去掉。在一些IDE中(如VsCode和Goland), 每次保存文件都会自动执行 gofmt
Go Basics
Variables in Go
在Go语言中,一旦一个变量被声明,它就必须被调用,否则会出现错误。但有时你并不需要使用从一个函数得到的所有返回值。因此,我们可以使用_
空白表示符
_
被用于抛弃值,你不能得到它的值,如值 5 在:_, b = 5, 7 中被抛弃。
```go
//声明变量
var x int = 7
var s string
s1 = “Learning Go!”//打印使用Println,不同元素之间用 , 隔开
var age int = 30
fmt.Println(“age: “, age)var name = “Dan”
fmt.Println(“Your name is: “, name)1
2
3
4
5
6
7
8
9
10
2. 我们也可以用更简单的方式声明: `age := 30` ,可以不用显式定义变量类型
### Multiple Declarations
我们可以用一行代码来定义多个变量
```go
car,cost := "Audi",50000
fmt.Println(car, cost)
但是函数体内不允许使用 := 重复声明同名变量。 因为 := 通常是用来声明新的变量的,如果我们要重复定义,可以使用 =
或者,我们可以在左侧出现至少1个新的变量,也可以规避这个错误
var
此外,我们可以用 var 来进行多变量声明,可读性会更强:
1 | // 用var声明,默认会将变量置为0值 |
Types and Zero Values
由于Go还是算静态语言的一种,因此每一个变量都需要有一个类型。我们之所以可以用:=
定义变量是因为go帮我们做了隐式变量推导。
1 | func main() { |
如果我们将b赋值给a,go会马上报错,因为这种隐式变量转换在go语言中是不被允许的
同样的,int和string之间也无法进行隐式类型转换
go中如何表达0/空
- 在数字类型中: 0
- 在布尔类型中: false
- 在字符串类型中: “”
- 在指针类型中: nil
Naming Conventions in Go
现在我们来介绍一下Go语言的命名规则:
- 变量必须以字母或者下划线
_
开头 - 大小写敏感
- Go的25个关键词不能被作为变量名
- 命名尽量简短、但保持可读性、变量可以使用驼峰命名法
Package fmt
fmt是Go标准库中很重要的一个包。它可以帮助我们格式化并打印内容
Println
函数可以帮我们打印一行内容,可以是不同类型的变量的组合,中间用,
分隔.
Printf
函数是用来格式化输出内容的, 但是每次打印结束不会换行。比如:
通用verbs
1 | %v 值的默认格式 |
布尔值
1 | %t true或false |
整数
1 | %b 表示二进制 |
浮点数与复数
1 | %b 无小数部分、二进制指数的科学计数法,如-123456p-78;参见strconv.FormatFloat |
string与[]byte
1 | %s 输出字符串表示(string类型或[]byte) |
Slice
1 | %p 切片第一个元素的指针 |
point
1 | %p 十六进制内存地址,前缀ox |
Constants in Go
Go 语言中声明常量使用的关键字是const
。常量的使用非常广泛,比如说圆周率,再比如说一些明确的错误信息等一些容易被多次使用的值,一般都会使用常量进行实例化,使其在需要更改时,更容易维护,同时增加代码可读性。
常量在声明的时候就必须初始化,但是和变量不一样,常量声明了以后,不使用也不会报错。
1 | func main() { |
常量声明规则
- 声明之后无法修改
给常量赋值的时候不能涉及运行时计算,如:
const power = math.Pow(2,3)
我们不能给一个常量付一个变量的值
1 | t := 5 |
- 特殊情况,当我们使用内建函数(如len)时,可以使用其返回结果给常量赋值
1 | const l1 = len("hello") |
Constant Expressions Typed vs Untyped Constants
当我们在申明常量的时候,如果指明了常量类型,那么就无法做隐式类型转换了。
1 | const x int = 5 |
IOTA
iota
是golang的常量计数器,只能在常量的表达式中使用。
使用iota
时只需要记住以下两点
1.iota
在const
关键字出现时将被重置为0。
2.const
中每新增一行常量声明将使iota
计数一次(iota可理解为const
语句块中的行索引)。
可以用这个关键词来实现枚举结构
1 | const ( |
注意:常量只能声明布尔值、整数值、rune constant(即int32)、complex constant(复数)、浮点数值、字符串值。
不能声明数组常量、结构常量,它们用var声明
Go Data Types
Numeric types
int8, int16, int32, int64 :注意,go有溢出检测,如果 声明
var i1 int8 = -129
会直接报错。uint8, uint16, uint32, uint64: 无符号整数
uint is an alias for uint32 or uint64 based on platform. 直接使用uint/int的话,会随着平台的不同而变化
- int is an alias for int32 or int64 based on platform.
- float32, float64: 如果小数点之前为0,则0可省略 ( -.5 -3. -0. 1.4).
- complex64, complex128.
- byte (alias for uint8).
- rune (alias for int32).
Bool type
- 布尔值,只有 true / false 两个值
String type
字符串类型,用双引号
现在来介绍go中的复合变量
Array and Slice Type
数组
数组是一个长度固定的数据类型,用于存储一段具有相同类型的元素的连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。切片
切片是围绕动态数组的概念构建的,可以按需自动增长和缩小
切片是一个很小的对象,对底层数组进行了抽象,并提供了相关的操作方法。切片有3个字段分别是指向底层数组的指针
,切片访问的元素个数(即长度)
和切片允许增长到的元素个数(即容量)
切片的长度和容量在概念上有以下区别:
- 长度(Length):切片的长度表示切片中实际包含的元素个数。长度可以通过内置函数
len()
来获取。在上述示例中,切片slice
的长度为 3,表示切片中有 3 个元素。 - 容量(Capacity):切片的容量表示切片从第一个元素开始算起,到底层数组末尾的元素个数。容量可以通过内置函数
cap()
来获取。在slice := make([]int, 3, 5)
这个例子中,切片slice
的容量为 5,表示切片底层的数组中还有 5 - 3 = 2 个空闲的位置。
数组
1 | //声明一个包含5个元素的整型数组,并设置为零值 |
切片
1 | //使用make(声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。) |
在之后会详细介绍
Map Type
- 在 Go 语言中,Map(映射)是一种集合类型,用于存储键值对(key-value)数据。Map 是无序的,每个键在 Map 中必须是唯一的。
1 | package main |
Struct Type (User defined type)
struct和C++中的类似,可以将其适用于结构类型
1 | type Car struct { |
Pointer Type
指针存储了一个变量的地址、如果未初始化,默认为 nil
Function and Interface Type
函数类型、接口类型
Channel Type
通道类型 为 通信而设计。
谁会用到它呢?协程,就是Go协程(goroutine),使用 go语句并发执行的函数或方法(concurrently executing functions)。
通信 包括 发送、接收指定的元素类型的 值。
没有被初始化的 通道 的值为 nil
Operations On Types: Arithmetic and Assignment Operators
和C++一样,不赘述
Comparison and Logical Operators
和C++一样,不赘述
Overflows
在go中也有向上溢出和向下溢出的概念。在 Go 中,整数溢出的行为与编译时和运行时的上下文有关。
在编译时,对于常量表达式,编译器会进行常量折叠和溢出检查。如果一个常量表达式的结果溢出了其类型的取值范围,编译器会在编译时就发出溢出错误。
比如,在声明的时候,如果检测到overflow,会直接报错:
1 | func main(){ |
而在运行时,对于变量的计算,Go 语言允许进行溢出操作。当对 var b int8 = 127
进行 b+1
的计算时,编译器不会报告溢出错误,因为这是在运行时动态计算的,而非在编译时。
1 | func main(){ |
同样的,向下溢出操作如下:
1 | var b int8 = -128 |
有意思的是,浮点数也会溢出,向上溢出到正无穷 ,比如:
1 | f := float32(math.MaxFloat32) |
如果需要计算很大的,可能溢出的数,可以使用 math/big
包, 它可以进行高精度计算,处理大数和分数等复杂数学运算。它对于需要处理精确度要求较高的场景非常有用,如密码学、金融计算、科学计算等。
Converting Numeric Types
在 Go 中,变量转换通常需要显式进行类型转换,以确保类型安全性。Go 语言不支持隐式变量转换,这是为了避免潜在的类型错误。
比如:
1 | func main(){ |
需要注意的是:在 Go 中,int
和 int64
是不同的类型。它们表示不同的整数类型,具有不同的大小和范围。
int
类型是一个有符号整数类型,其大小和范围在不同的操作系统和架构上可能会有所不同。在大多数情况下,int
的大小为 32 位或 64 位。int64
类型是一个明确表示 64 位有符号整数的类型
由于 int
和 int64
是不同的类型,因此不能直接进行类型转换。你需要使用显式类型转换来将一个类型转换为另一个类型。
1 | package main |
Converting Numbers to Strings and Strings to Numbers
在 Go 中,可以使用 fmt.Sprintf
函数将不同类型的值转换为字符串类型。该函数允许使用格式化字符串来构建一个字符串,其中可以包含不同类型的值。比如int类型和float类型
1 | package main |
我们可以用strconv来实现字符串往浮点数的转换
1 | s1 := "3.123" // type string |
我们可以用 Atoi 和 Itoa来实现整数和字符串之间的转换,更加方便
1 | i,err := strconv.Atoi("-50") // -50, int类型 |
Defined(Named) Types
在 Go 中,”defined type” 和 “source type” 是类型系统中的两个概念。
Defined Type(定义类型):在 Go 中,你可以使用
type
关键字创建一个新的类型,该类型基于一个已有的类型。这种通过type
关键字定义的类型称为 “defined type”。它们在语法上与其基础类型相同,但在类型系统中被视为不同的类型。例如,假设你有一个
int
类型的变量x
,你可以使用type
关键字创建一个新类型MyInt
,它是基于int
类型的:1
2type MyInt int
var x MyInt = 10Source Type(源类型):源类型是指定义类型的基础类型,也就是定义类型是基于的类型。在定义类型中,源类型扮演着基础类型的角色,提供了定义类型的底层实现和行为。
在上述示例中,
int
就是MyInt
的源类型。MyInt
类型继承了int
类型的所有属性和方法,因此可以在MyInt
类型的变量上执行与int
类型相同的操作。
在Defined Type和 Source Type之间,可以进行隐式类型转换,所以我们看到10可以直接赋值给 MyInt类型的 x
需要注意的是,两个不同的 Defined Type之间,虽然他们可能源于同一个 source type,但是他们之间没有办法进行隐式类型转换,需要显式类型转换!!
比如说:
1 | type km float64 |
distanceInMiles = mile(parisToLondon) * 0.621
的底层逻辑是,将parisToLondon转换成mile类型,由于mile类型的底层类型是float,因此他们可以相乘并赋值给mile类型的distanceInMiles
我们再举一个例子:
1 | package main |
在上面的的代码中,second
和 duration
都是通过类型定义创建的新类型。它们并不是别名类型。对于类型定义而言,尽管它们的底层类型相同,但在类型系统中它们被视为不同的类型。
在这种情况下,你可以将 10
隐式转换为 duration
类型,因为 10
是一个无类型常量,它可以自动转换为 duration
类型。这是由于编译器在编译时会将无类型常量转换为目标类型。
然而,将 duration
类型的变量赋值给 uint
类型的变量时,需要进行显式类型转换,因为在类型系统中,它们被视为不同的类型。
所以,10
可以隐式转换为 duration
类型,但 duration
类型的变量不能隐式转换为 uint
类型。
Alias Declarations
在 Go 中,Alias Declaration(别名声明)是一种创建类型别名的语法。它允许你为现有的类型创建一个新的名称,以便在代码中更清晰地表达其含义或提供更具可读性的标识符。和 上面的 named type不一样,别名类型和原始类型具有相同的底层结构和行为,它们是完全兼容的,可以互相替代使用。
Alias Declaration 使用 type
关键字,后面紧跟新的类型名称和等号,然后是现有的类型。
下面是一个示例,展示了如何使用 Alias Declaration 创建类型别名:
1 | type MyFloat64 = float64 |
需要注意的是,在 Go 中,byte
和 uint8
是相同的类型,它们是别名关系。同样地,rune
和 int32
也是相同的类型,它们也是别名关系。它们之间可以进行隐式类型转换,并且可以互相替代使用。
在特定的上下文中
用
byte
表示一个无符号的8位整数,通常用于表示字节数据而使用
rune
表示一个Unicode码点,通常用于处理字符和文本数据。
Program Flow Control in Go
If-Else
在 Go 中,if-else
是一种条件语句,用于根据条件的真假来执行不同的代码块。它的基本语法如下:条件不需要加括号,但是代码块需要使用花括号 {}
来界定。另外,条件表达式的结果必须是布尔类型的值(true
或 false
)。
1 | if condition { |
Command Line Arguments: os.Args
在 Go 中,os.Args
是一个字符串切片([]string
),用于获取命令行参数。
当我们在终端或命令行中执行一个可执行程序时,可以通过命令行参数来传递额外的信息给程序。os.Args
变量提供了对这些命令行参数的访问。
os.Args
切片的第一个元素是可执行程序的名称,后面的元素是传递给程序的命令行参数。下标从 0 开始。
1 | package main |
假设我们将上述程序保存为 commandline.go
,然后在终端中执行以下命令:
1 | go run naive.go arg1 arg2 arg3 |
程序的输出将会是:
1 | 程序路径: /var/folders/lp/52cjrdtd4_59hlqcsgrcwc_m0000gn/T/go-build1548655087/b001/exe/naive |
我们再看一个例子:
1 | func main() { |
运行go run naive.go 50
可得到如下打印结果:这也说明os.Args中的参数都是字符串形类型的
1 | string |
Simple If Statement
对于一些If语句,我们可以对其进行简化:
1 | func main() { |
For Loops
在 Go 中,for
是用于循环执行代码块的关键字。Go 提供了几种不同形式的 for
循环,以满足不同的需求。
- 基本的
for
循环:
1 | for initialization; condition; post { |
在基本的 for
循环中,我们可以指定初始化语句、循环条件和后置语句。初始化语句在循环开始前执行一次,循环条件在每次迭代前进行判断,循环体执行完后会执行后置语句。
示例:
1 | for i := 0; i < 5; i++ { |
for
循环的初始化和后置语句是可选的:
1 | for condition { |
在这种形式的 for
循环中,我们只需要指定循环条件。如果条件为真,则执行循环体。在每次迭代结束后,循环条件会被重新评估。
示例:
1 | i := 0 |
- 除了上述基本的
for
循环形式外,Go 还提供了range
关键字来迭代集合类型(如数组、切片、映射等)的元素。range
循环会依次迭代集合中的每个元素,并提供索引和对应的值。
1 | numbers := []int{1, 2, 3, 4, 5} |
在 Go 中没有专门的 while
关键字,但是你可以使用 for
循环来实现类似 while
的功能。你可以通过省略初始化语句和后置语句,并只保留循环条件,来使用 for
循环作为 while
循环的替代。
下面是一个示例,展示如何在 Go 中使用 for
循环来实现 while
循环:
1 | package main |
这样,我们就实现了一个类似于 while
循环的行为,只要循环条件为真,就会不断地执行循环体。
需要注意的是,在使用 for
循环作为 while
循环的替代时,你需要确保在循环体内部更新循环条件的值,以免出现死循环。在上述示例中,我们在循环体内部使用 i++
来逐步增加 i
的值,以确保循环条件最终不再满足,从而终止循环。
For and Continue Statements
在 go中,也有continue关键词 ,当continue
语句执行时,它会立即终止当前迭代的执行,并跳到循环的下一次迭代。
1 | for i := 1; i <= 10; i++ { |
上述代码将打印出:
1 | 2 |
在上面的示例中,当i
为奇数时,continue
语句将被执行,跳过当前迭代,然后继续执行下一次迭代。只有当i
为偶数时,fmt.Println(i)
语句才会执行。
For and Break Statements
在Go语言中,break
是一个控制流语句,用于在循环或switch
语句中立即终止当前的执行流程并跳出循环或switch
语句。
当break
语句执行时,它会立即终止当前的循环或switch
语句的执行,并将控制转移到循环或switch
语句之后的下一行代码。
下面是一个使用break
语句的示例,演示了在循环中使用break
来提前结束循环的情况:
1 | goCopy code |
需要注意的是,break
语句只会影响当前所在的循环或switch
语句。如果有嵌套循环或嵌套switch
语句,break
语句只会跳出最内层的循环或switch
语句。如果想要跳出外层的循环或switch
语句,可以使用标签(label)来标识循环或switch
语句,并在break
语句中指定标签。