go学习1

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 buildgo installgo 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 中被抛弃。

  1. ```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
2
3
4
5
6
7
8
9
10
11
12
13
// 用var声明,默认会将变量置为0值
var (
salary float64
firstName string
gender bool
)

fmt.Println(salary,firstName,gender)

var i,j int
i,j = 5,8
j,i = i,j//交换i,j位置
fmt.Println(i,j)// 8,5

Types and Zero Values

由于Go还是算静态语言的一种,因此每一个变量都需要有一个类型。我们之所以可以用:=定义变量是因为go帮我们做了隐式变量推导。

1
2
3
4
func main() {
var a = 3
var b = 4.2
}

如果我们将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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
%v        值的默认格式
%+v  类似%v,但输出结构体时会添加字段名
%#v    Go语法表示值
%T    Go语法表示类型
%%   百分号表示

//如下示例:
type Sample struct {
Title string
name string
Age int
}

func main() {

s := Sample{"测试", "wentao", 26}

fmt.Printf("%v\n", s) // {测试 wentao 26}
fmt.Printf("%+v\n", s) // {Title:测试 name:wentao Age:26}
fmt.Printf("%#v\n", s) // main.Sample{Title:"测试", name:"wentao", Age:26}
fmt.Printf("%T\n", s) // main.Sample
fmt.Printf("%v%%\n", s.Age) // 26%
}

布尔值

1
2
3
4
5
%t  truefalse 
//如下示例
func main() {
fmt.Printf("%t\n", true) //true
}

整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%b  表示二进制
%c 该值对应的unicode吗值
%d 表示十进制
%o 表示八进制
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于"U+%04X"

//如下示例
func main() {

fmt.Printf("%b\n", 26) //11010
fmt.Printf("%c\n", 0x4E2D) //中
fmt.Printf("%d\n", 0x12) //18
fmt.Printf("%o\n", 20) //24
fmt.Printf("%q\n", 0x4E2D) //'中'
fmt.Printf("%x\n", 14) //e
fmt.Printf("%X\n", 14) //E
fmt.Printf("%U\n", 0x4E2D) //U+4E2D
}

浮点数与复数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%b  无小数部分、二进制指数的科学计数法,如-123456p-78;参见strconv.FormatFloat
%e 科学计数法,例如 -1234.456e+78
%E 科学计数法,例如 -1234.456E+78
%f 有小数点而无指数,例如 123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
//如下示例
func main() {

fmt.Printf("%b\n", 10.45) //5882827013252710p-49
fmt.Printf("%e\n", 10.45) //1.045000E+01
fmt.Printf("%E\n", 10.45) //1.045000E+01
fmt.Printf("%f\n", 10.45) //10.450000
fmt.Printf("%F\n", 10.45) //10.450000
fmt.Printf("%g\n", 10.45) //10.45
fmt.Printf("%G\n", 10.45) //10.45
}

string与[]byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%s  输出字符串表示(string类型或[]byte) 
%q 双引号围绕的字符串,由Go语法安全地转义
%x 十六进制,小写字母,每字节两个字符 (使用a-f)
%X 十六进制,大写字母,每字节两个字符 (使用A-F)

//如下示例:
package main

import "fmt"

func main() {

fmt.Printf("%s\n", []byte("go开发")) //go开发
fmt.Printf("%s\n", "go开发") //go开发
fmt.Printf("%q\n", "go开发") //"go开发"
fmt.Printf("%x\n", "go开发") //676fe5bc80e58f91
fmt.Printf("%X\n", "go开发") //676FE5BC80E58F91
}

Slice

1
2
3
4
5
6
7
8
9
10
11
%p       切片第一个元素的指针
//如下示例
package main

import "fmt"

func main() {

fmt.Printf("%p\n", []byte("go开发")) //0xc42001a0d8
fmt.Printf("%p\n", []int{1, 2, 3, 45, 65}) //0xc420020180
}

point

1
2
3
4
5
6
7
8
9
10
%p       十六进制内存地址,前缀ox
package main

import "fmt"

func main() {

str := "go开发"
fmt.Printf("%p\n", &str) //0xc42000e1e0
}

Constants in Go

Go 语言中声明常量使用的关键字是const。常量的使用非常广泛,比如说圆周率,再比如说一些明确的错误信息等一些容易被多次使用的值,一般都会使用常量进行实例化,使其在需要更改时,更容易维护,同时增加代码可读性。

常量在声明的时候就必须初始化,但是和变量不一样,常量声明了以后,不使用也不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
//常量定义
const secondsInHour = 3600

duration := 234
fmt.Printf("Duration in seconds: %v\n", duration*secondsInHour)
//多常量定义
const n, m int = 4, 5
const n1, m1 = 6, 7
//在使用这种方式进行多常量定义的时候,如果后面的常量未初始化,会自动沿用第一个常量的值
const (
min1 = -400
min2
min3
)
fmt.Println(min1, min2, min3) //-400 -400 -400
}

常量声明规则

  • 声明之后无法修改
  • 给常量赋值的时候不能涉及运行时计算,如:const power = math.Pow(2,3)

  • 我们不能给一个常量付一个变量的值

1
2
t := 5
const tc = t //会报错
  • 特殊情况,当我们使用内建函数(如len)时,可以使用其返回结果给常量赋值
1
const l1 = len("hello")

Constant Expressions Typed vs Untyped Constants

当我们在申明常量的时候,如果指明了常量类型,那么就无法做隐式类型转换了。

1
2
3
4
5
6
7
8
9
10
const x int = 5
const y float64 = 2.2 * x //会报错


//但是,如果我们不确定常量数值类型,就不会报错。 此时go给了x一定的freedom,能让它做隐式类型转换
const x = 5
const y float64 = 2.2 * x //不会报错

const x = 5
const y = 2.2 * x //不会报错

IOTA

iota是golang的常量计数器,只能在常量的表达式中使用。

使用iota时只需要记住以下两点

1.iotaconst关键字出现时将被重置为0。

2.const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。

可以用这个关键词来实现枚举结构

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
31
32
33
34
35
36
const (
n1 = iota //0
n2 //1 顺延第一行的表达式
n3 //2
n4 //3
)

const (
n1 = iota //0
n2 //1
_ //丢弃该值,常用在错误处理中
n4 //3
)

const (
n1 = iota //0
n2 = 100 //100 即使没有出现iota,iota 也会自增1
n3 = iota //2
n4 //3
)

const n5 = iota //0

const (
_ = iota // iota = 0
KB = 1 << (10 * iota) // iota = 1, <<移位操作,速度比乘除法快 ,因此KB = 2^10
MB = 1 << (10 * iota) // 以此类推
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3 常量如果没有初始化,会顺延第一行的表达式
e, f //3,4
)

注意:常量只能声明布尔值、整数值、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
2
3
4
5
6
7
8
//声明一个包含5个元素的整型数组,并设置为零值
var array [5]int
//使用数组字面量声明数组
array := [5]int{10,20,30,40,50}
//隐式声明数组长度
array := [...]int{10,20,30,40,50}
//声明数组并指定特定元素
array := [5]int{0:10,2:30} // => [10 0 30 0 0]

切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//使用make(声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。)
//长度和容量都是5个元素
slice := make([]string, 5)
//长度为3,容量为5个元素
//3是这是切片的长度,表示切片中实际包含的元素个数
//5是切片的容量,表示底层数组从切片的第一个元素开始,到底层数组末尾的元素个数。
slice := make([]int, 3, 5)

//使用字面量声明
//长度和容量都是4个元素
slice := []string{"red","blue","green","yellow"}
//这行代码创建了一个字符串切片 slice,其中包含了 100 个元素
//其中前 99 个元素的值为空字符串,而最后一个元素的值为 "!!"。
//在大括号内,我们使用键值对的方式来初始化切片的元素。键表示切片中的索引,值表示对应索引处的元素值
//这里的 99 是键,表示切片中的索引位置,而 "!!" 是值,表示该索引位置的元素的值。
slice := []string{99:"!!"}

在之后会详细介绍

Map Type

  • 在 Go 语言中,Map(映射)是一种集合类型,用于存储键值对(key-value)数据。Map 是无序的,每个键在 Map 中必须是唯一的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func main() {
// 初始化一个map,键是string类型,值是map类型(又是一个string-string键值对)
studentMap := make(map[string]map[string]string)
// 先初始化 map 大小
studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "tom"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "北京长安街"

studentMap["stu02"] = make(map[string]string) // 这句话不能少
studentMap["stu02"]["name"] = "mary"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "上海黄浦江"
fmt.Println(studentMap)
//map[stu01:map[address:北京长安街 name:tom sex:男] stu02:map[address:上海黄浦江 name:mary sex:女]]

fmt.Println(studentMap["stu02"])
//map[address:上海黄浦江 name:mary sex:女]

}

Struct Type (User defined type)

struct和C++中的类似,可以将其适用于结构类型

1
2
3
4
type Car struct {
brand string
price int
}

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
2
3
func main(){
a := int8(255+1)//会报错overflow
}

而在运行时,对于变量的计算,Go 语言允许进行溢出操作。当对 var b int8 = 127 进行 b+1 的计算时,编译器不会报告溢出错误,因为这是在运行时动态计算的,而非在编译时。

1
2
3
4
func main(){
var x uint8 = 255
x++ // overflow, x is 0
}

同样的,向下溢出操作如下:

1
2
3
var b int8 = -128
b--
fmt.Printf("%d\n",b+1)

有意思的是,浮点数也会溢出,向上溢出到正无穷 ,比如:

1
2
3
f := float32(math.MaxFloat32)
f = f*1.2
fmt.Println(f) // => +Inf

如果需要计算很大的,可能溢出的数,可以使用 math/big 包, 它可以进行高精度计算,处理大数和分数等复杂数学运算。它对于需要处理精确度要求较高的场景非常有用,如密码学、金融计算、科学计算等。

Converting Numeric Types

在 Go 中,变量转换通常需要显式进行类型转换,以确保类型安全性。Go 语言不支持隐式变量转换,这是为了避免潜在的类型错误。

比如:

1
2
3
4
5
6
7
8
9
func main(){
var x = 3 // int type
var y = 3.1 // float type

// x = x*y //会报错
//如果要相乘,必须:
newx := x*int(y) //=> 9
newy := float(x)*y //=> 9.3
}

需要注意的是:在 Go 中,intint64 是不同的类型。它们表示不同的整数类型,具有不同的大小和范围。

  • int 类型是一个有符号整数类型,其大小和范围在不同的操作系统和架构上可能会有所不同。在大多数情况下,int 的大小为 32 位或 64 位。
  • int64 类型是一个明确表示 64 位有符号整数的类型

由于 intint64 是不同的类型,因此不能直接进行类型转换。你需要使用显式类型转换来将一个类型转换为另一个类型。

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

import "fmt"

func main() {
var x int = 10
var y int64

// 将 x 的值转换为 int64 类型,并赋值给 y
y = int64(x)

fmt.Println(x, y)
}

Converting Numbers to Strings and Strings to Numbers

在 Go 中,可以使用 fmt.Sprintf 函数将不同类型的值转换为字符串类型。该函数允许使用格式化字符串来构建一个字符串,其中可以包含不同类型的值。比如int类型和float类型

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

import "fmt"

func main() {
// 将整数转换为字符串
num := 42
str := fmt.Sprintf("%d", num)
fmt.Println(str) // 输出: "42"
// 将浮点数转换为字符串
myStr := fmt.Sprintf("%f",44.2)
fmt.Println(myStr) // 输出 "44.2"

}

我们可以用strconv来实现字符串往浮点数的转换

1
2
3
4
5
6
s1 := "3.123" // type string
fmt.Printf("%T\n",s1)

var f1,err = strconv.ParseFloat(s1,64)
_ = err
fmt.Println(f1)

我们可以用 Atoi 和 Itoa来实现整数和字符串之间的转换,更加方便

1
2
i,err := strconv.Atoi("-50")	// -50, int类型
s2 := strconv.Itoa(20) // "20",string类型

Defined(Named) Types

在 Go 中,”defined type” 和 “source type” 是类型系统中的两个概念。

  • Defined Type(定义类型):在 Go 中,你可以使用 type 关键字创建一个新的类型,该类型基于一个已有的类型。这种通过 type 关键字定义的类型称为 “defined type”。它们在语法上与其基础类型相同,但在类型系统中被视为不同的类型。

    例如,假设你有一个 int 类型的变量 x,你可以使用 type 关键字创建一个新类型 MyInt,它是基于 int 类型的:

    1
    2
    type MyInt int
    var x MyInt = 10
  • Source Type(源类型):源类型是指定义类型的基础类型,也就是定义类型是基于的类型。在定义类型中,源类型扮演着基础类型的角色,提供了定义类型的底层实现和行为。

    在上述示例中,int 就是 MyInt 的源类型。MyInt 类型继承了 int 类型的所有属性和方法,因此可以在 MyInt 类型的变量上执行与 int 类型相同的操作。

在Defined Type和 Source Type之间,可以进行隐式类型转换,所以我们看到10可以直接赋值给 MyInt类型的 x

需要注意的是,两个不同的 Defined Type之间,虽然他们可能源于同一个 source type,但是他们之间没有办法进行隐式类型转换,需要显式类型转换!!

比如说:

1
2
3
4
5
6
7
8
9
type km float64
type mile float64
func main() {
var parisToLondon km = 465
var distanceInMiles mile
//distanceInMiles = parisToLondon * 0.621 //会报错,因为 mile 和 parisToLandon不是一种类型的
distanceInMiles = mile(parisToLondon) * 0.621
fmt.Println(distanceInMiles)
}

distanceInMiles = mile(parisToLondon) * 0.621 的底层逻辑是,将parisToLondon转换成mile类型,由于mile类型的底层类型是float,因此他们可以相乘并赋值给mile类型的distanceInMiles

我们再举一个例子:

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

type second uint
type duration second

type minute = uint

func main() {
var t1 duration = 10

var x uint = t1
_ = x
}

在上面的的代码中,secondduration 都是通过类型定义创建的新类型。它们并不是别名类型。对于类型定义而言,尽管它们的底层类型相同,但在类型系统中它们被视为不同的类型。

在这种情况下,你可以将 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 中,byteuint8 是相同的类型,它们是别名关系。同样地,runeint32 也是相同的类型,它们也是别名关系。它们之间可以进行隐式类型转换,并且可以互相替代使用。

在特定的上下文中

  • byte 表示一个无符号的8位整数,通常用于表示字节数据

  • 而使用 rune 表示一个Unicode码点,通常用于处理字符和文本数据。

Program Flow Control in Go

If-Else

在 Go 中,if-else 是一种条件语句,用于根据条件的真假来执行不同的代码块。它的基本语法如下:条件不需要加括号,但是代码块需要使用花括号 {} 来界定。另外,条件表达式的结果必须是布尔类型的值(truefalse)。

1
2
3
4
5
if condition {
// 当条件为真时执行的代码块
} else {
// 当条件为假时执行的代码块
}

Command Line Arguments: os.Args

在 Go 中,os.Args 是一个字符串切片([]string),用于获取命令行参数。

当我们在终端或命令行中执行一个可执行程序时,可以通过命令行参数来传递额外的信息给程序。os.Args 变量提供了对这些命令行参数的访问。

os.Args 切片的第一个元素是可执行程序的名称,后面的元素是传递给程序的命令行参数。下标从 0 开始。

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

import (
"fmt"
"os"
)

func main() {
// 获取所有命令行参数
args := os.Args

// 打印可执行程序的路径
fmt.Println("程序路径:", args[0])

// 打印其他命令行参数
fmt.Println("命令行参数:")
for i := 1; i < len(args); i++ {
fmt.Println(i, args[i])
}
}

假设我们将上述程序保存为 commandline.go,然后在终端中执行以下命令:

1
go run naive.go arg1 arg2 arg3

程序的输出将会是:

1
2
3
4
5
程序路径: /var/folders/lp/52cjrdtd4_59hlqcsgrcwc_m0000gn/T/go-build1548655087/b001/exe/naive
命令行参数:
1 arg1
2 arg2
3 arg3

我们再看一个例子:

1
2
3
4
5
6
7
func main() {
// 获取所有命令行参数
var result, err = strconv.ParseFloat(os.Args[1], 64)
fmt.Printf("%T\n", os.Args[1])
fmt.Printf("%T\n", result)
_ = err
}

运行go run naive.go 50 可得到如下打印结果:这也说明os.Args中的参数都是字符串形类型的

1
2
string
float64

Simple If Statement

对于一些If语句,我们可以对其进行简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 func main() {

// converting string to int:
i, err := strconv.Atoi("45")

// error handling
if err != nil {
fmt.Println(err)
} else {
fmt.Println(i)

}

// simple (short) statement -> the same effect as the above code
// 把第一句话和判断放在一起,中间用逗号隔开
if i, err := strconv.Atoi("34"); err == nil {
fmt.Println("No error. i is ", i)
} else {
fmt.Println(err)
}
}

For Loops

在 Go 中,for 是用于循环执行代码块的关键字。Go 提供了几种不同形式的 for 循环,以满足不同的需求。

  1. 基本的 for 循环:
1
2
3
for initialization; condition; post {
// 循环体
}

在基本的 for 循环中,我们可以指定初始化语句、循环条件和后置语句。初始化语句在循环开始前执行一次,循环条件在每次迭代前进行判断,循环体执行完后会执行后置语句。

示例:

1
2
3
for i := 0; i < 5; i++ {
fmt.Println(i)
}
  1. for 循环的初始化和后置语句是可选的:
1
2
3
for condition {
// 循环体
}

在这种形式的 for 循环中,我们只需要指定循环条件。如果条件为真,则执行循环体。在每次迭代结束后,循环条件会被重新评估。

示例:

1
2
3
4
5
i := 0
for i < 5 {
fmt.Println(i)
i++
}
  1. 除了上述基本的 for 循环形式外,Go 还提供了 range 关键字来迭代集合类型(如数组、切片、映射等)的元素。range 循环会依次迭代集合中的每个元素,并提供索引和对应的值。
1
2
3
4
numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
fmt.Println(index, value)
}

在 Go 中没有专门的 while 关键字,但是你可以使用 for 循环来实现类似 while 的功能。你可以通过省略初始化语句和后置语句,并只保留循环条件,来使用 for 循环作为 while 循环的替代。

下面是一个示例,展示如何在 Go 中使用 for 循环来实现 while 循环:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
i := 0
for i < 5 {
fmt.Println(i)
i++
}
}

这样,我们就实现了一个类似于 while 循环的行为,只要循环条件为真,就会不断地执行循环体。

需要注意的是,在使用 for 循环作为 while 循环的替代时,你需要确保在循环体内部更新循环条件的值,以免出现死循环。在上述示例中,我们在循环体内部使用 i++ 来逐步增加 i 的值,以确保循环条件最终不再满足,从而终止循环。

For and Continue Statements

在 go中,也有continue关键词 ,当continue语句执行时,它会立即终止当前迭代的执行,并跳到循环的下一次迭代。

1
2
3
4
5
6
for i := 1; i <= 10; i++ {
if i%2 != 0 {
continue // 跳过奇数
}
fmt.Println(i)
}

上述代码将打印出:

1
2
3
4
5
2
4
6
8
10

在上面的示例中,当i为奇数时,continue语句将被执行,跳过当前迭代,然后继续执行下一次迭代。只有当i为偶数时,fmt.Println(i)语句才会执行。

For and Break Statements

在Go语言中,break是一个控制流语句,用于在循环或switch语句中立即终止当前的执行流程并跳出循环或switch语句。

break语句执行时,它会立即终止当前的循环或switch语句的执行,并将控制转移到循环或switch语句之后的下一行代码。

下面是一个使用break语句的示例,演示了在循环中使用break来提前结束循环的情况:

1
2
3
4
5
6
7
goCopy code
for i := 1; i <= 10; i++ {
if i == 5 {
break // 当 i 等于 5 时终止循环
}
fmt.Println(i)
}

需要注意的是,break语句只会影响当前所在的循环或switch语句。如果有嵌套循环或嵌套switch语句,break语句只会跳出最内层的循环或switch语句。如果想要跳出外层的循环或switch语句,可以使用标签(label)来标识循环或switch语句,并在break语句中指定标签。

Label Statement

Goto

Switch Statement

Scopes in Go

Arrays in Go

Slice in Go

-------------本文结束,感谢您的阅读-------------