Go 语言开发快速回顾:语法、数据结构和流程控制
Go语言简单、高效具备很强的语言表达能力支持静态类型安全同时提供动态语言的特性。不仅如此它还支持自动垃圾回收能够有效防止内存泄漏并从底层支持协程并发充分利用计算机的硬件性能。基于以上种种优势Go目前在软件行业发挥着重要作用不少优秀的开源软件都是基于Go进行开发的包括 Docker、Etcd 和 Kubernetes 等。虽说近几年来Go语言发展比较迅猛但是总体而言它还是属于新生代语言。鉴于我们课程接下来的实践都是围绕着Go来展开的所以在介绍微服务各个组件的详情之前非常有必要对Go的语法补充一些前置知识。基础语法Go的语法与C语言相似但显得更加精炼。下面我们通过一个简单的小程序来熟悉下Go的基础语法代码如下/ / fileName: simple.go package main import ( fmt sync ) func input(ch chan string) { defer wg.Done() defer close(ch) var input string fmt.Println(Enter EoF to shut down: ) for { _, err : fmt.Scanf(%s, input) if err ! nil{ fmt.Println(Read input err: , err.Error()) break } if input EoF{ fmt. Println(Bye!) breakch } ch - input } } func output(ch chan string) { defer wg.Done() for value : range ch{ fmt.Println(Your input: , value) } } var wg sync.waitGroup func main() { ch : make(chan string) wg .Add(2) go input(ch // 读取输入 go output(ch // 输出到命令行 wg.wait() fmt.Println(Exit!) }上述示例代码的功能为从命令行接受用户的输入并输出到命令行中可在文件所在命令行下执行g0run simple.go启动程序。代码中使用 go 关键字启动了两个协程分别处理读取输入和输出到命令行协程为Go中执行代码片段的用户级轻量线程协程之间通过chan 进行数据传输chan 中的数据传输遵循先进先出的顺序并保证每次只能有一个协程发送或者接收数据。每一个 Go 文件都需要在文件开头标注所属的包Go程序是由包组成的上述代码中的 Go 文件位于main 包下。Go 中规定可执行程序必须具备 main 包并且在 main 包下具备可执行的 main 函数。通过import关键字就可以导入其他包中的代码进行使用。从上述代码中我们可以了解到Go的部分语法特点变量声明时变量类型位于变量名后;对于 if和 for 等语句的子句条件无须使用) 包起来;语句结束无须使用;之类的分隔符通过换行区分;{必须紧跟在对应语句的后面不可另起一行。为了使 Go 程序运行起来接下来我们就介绍一些简单的 Go 编译工具和命令。第一个go run 命令。该命令将直接编译和执行源码中的 main 函数你可以在命令后添加参数这部分参数会作为代码可以接受的命令行输入提供给程序。以上面的 simple.go 小程序为例子进入到simple.go 文件的目录下执行如下命令即可运行 simple.go 小程序:go run simple.go第二个go build 命令。该命令通过 Go 的并发特性对代码进行函数粒度的并发编译它会将源码编译为可执行文件默认将编译该目录下的所有源码。也可以在命令后添加多个文件名go build 将编译这些源码输出可执行文件。比如进入到我们上面 simple.go 文件的目录下执行如下命令go build -o simple // -o 用于指定生成可执行文件名称 或者 go build simple.go都将在当前目录下生成一个 simple 的可执行文件可双击直接运行。接下来我们就详细讲解一下 Go 语言中的函数声明、变量的声明与初始化、指针、struct 等基础语法。1.函数声明Go 中使用关键字 func 来声明函数声明形式如下:func funcName(params)(return params){ function body }在同一个包内函数名不可重名。如果函数希望能在包外被访问则需要将函数名的首字母大写表示该函数是包外公开的。函数可以接受〇个或者以上参数在声明参数列表时需要注意参数名在前、类型在后。对于连续类型相同的多个参数可以省略参数类型在最后一个参数中保留类型即可。Go的函数支持多返回值和命名返回值但是命名返回值和非命名返回值不能混合使用。一个综合的函数声明示例如下func Div(dividend, divisor int)(quotient, remainder int) { quotient dividend/divisor remainder dividend%divisor return }上述代码中声明了一个包外公开的 Div 函数接受 dividend、divisor 两个参数并返回quotient、remainder两个命名返回值它们可以在函数体中被直接赋值使用。这里需要注意的是在使用命名返回值时我们需要在函数结束时显式使用return 语句进行返回。2.变量的声明与初始化Go 中使用关键字var 声明变量变量名在前类型在最后声明形式如下:var name T在 Go中声明的变量都必须被使用否则会编译失败。变量声明之后会被默认初始化为初值。当然我们也可以在声明时直接进行初始化使用赋值符号“形式如下var name T expressionGo 中支持类型推荐在变量声明和初始化可以省略类型由编译器根据右边的表达式推导变量的类型。var a 100a 中的类型会被编译器推导为int。Go 中还支持短变量声明和初始化形式更加精简:name : expression该种形式需要:的左值存在没有定义过的变量且无法在函数外使用。短变量形式支持多个变量的声明和初始化如value, - : fmt.Println()在多个短变量的声明和初始化中需要保证左值最少有一个新变量否则编译会失败。对于不需要使用的变量可以使用匿名变量来处理如上述例子中使用来接受函数调用返回error表示赋予该标识符的值将被直接舍弃无法在后续代码中使用。匿名变量可以在代码中多次声明使用。3.指针Go支持指针操作。指针使得开发人员可以直接操作内存空间能够有效地提升程序的执行性能但是传统的指针容易带来指针偏移错误、忘记释放内存等问题大大提升了指针编程的门槛。Go移除了指针的运算能力并引I入了自动垃圾回收机制使得Go中的指针在具备高效的内存访问效率同时不会出现非法修改越界数据和指针占用内存忘记回收等问题。Go 中的指针使用方式与C类似为取址符号*为取值符号。声明一个指针类型如下:var name *T这里T为指向T类型的指针。我们可以通过指针直接读取和修改变量的值如下面例子所示:package main import fmt func main() { name : aoho p : name fmt.Println(name is, *p) *p zhu fmt.Println(name is, name) }上述代码中使用了“获取了 name 变量的指针并通过该指针读取和修改了 name 变量的值。4. structGo 中不存在类但是存在与C 类似的 struct 类型。struct 作为一种复合类型由一组字段组成每一个字段都有自己的类型和名称。struct 的定义需要结合 type 和 struct 关键字形式如下:type structName struct{ valuel T1 value2 T2 .... }和函数声明类似struct 名在同一个包内不能重复将 struct 名的首字母大写表示该 struct 可以在包外被访问。struct 中的字段如果希望包外公开同样需要将字段名的首字母大写。一个简单的结构体定义如下:type Student struct { StudentID int64 Name string birth string }在上述声明的 Student struct 中StudentID 和 Name 字段在包外是可访问的birth 字段只能在包内访问。struct可以通过多种形式实例化一个新的结构体struct 中的字段可以通过进行读取和修改如下面代码所示:package main import fmt func main() { s0 : Student{} / / Key:Value s1 : Student{ StudentID:1, Name:s1, birth:19900101, } //字段赋值顺序与结构体字段定义顺序一致 s2 : Student{ 2, s2, 19900102, } //获取指针 s3 : student{ StudentID:3, Name:s3, birth:19900103, } fmt.Println(s0, sl, s2, s3) fmt.Println(s0.Name, s1.Name, s2.Name, s3.Name) }数据结构在日常开发中除了掌握基本的语法外语言的一些基本数据结构也必须要掌握合理使用数据结构能够带来更高的执行效率。Go 中常使用的数据结构有 array数组、slice切片和map字典。下面让我们来一一回顾它们的用法。1.array (数组)数组为一段连续的内存空间存储了固定数量和固定类型的数据数组的大小在声明的时候就已经固定下来。数组的声明样式如下var name [size]T这表示声明了一个长度为 size、存储类型为T的数组。一个简单初始化和使用数组的例子如下:package main import fmt func main() { var numList1 [3]int numList1[0] 0 numList1[1] 1 numList1[2] 2 numList2 : [3]int{0, 1, 2}; fmt.Println(numListl, numList2) }在上述代码中我们可以在声明数组后使用下标的方式对数组成员进行访问和赋值如numList1;也可以在声明数组时直接初始化数组内的数据如numeList2。2.slice (切片)数组的大小是固定的在使用时难免会遇到扩容的情况。切片作为数组一个连续片段的引I用它的大小动态可变我们可以简单将切片理解为动态数组。切片底层由数组实现在添加数据时如果切片对应的底层数组不足以容纳新的成员时该切片将会进行扩容重新申请一段连续的内存空间作为新数组通常为原有数组的2倍然后将原有切片的数据复制到新数组中把新的成员添加新的数组中创建新的切片并指向新数组最后返回新的切片如果当前切片对应的数组可以容纳更多的数据添加的操作将在原有数组上进行这将会覆盖掉原有数组的值接着创建新的切片并指向原数组最后返回新的切片。切片和数组的关系可以通过下图理解切片中持有指向底层存储数据数组的指针长度指当前切片中存储数据的长度容量指当前切片的容量即当前切片从它的第一个数据到其对应数组末尾的长度可以简单理解为切片在其对应数组中可使用的长度。切片可以从数组中直接生成它将直接关联原有数组生成样式如下slice : source[begin:end]它从数组中选择一个半开区间包含begin 位置的数据但不包含end 位置的数据。也可以使用 make函数创建一个新的切片样式如下所示slice : make([]T, size, cap)在创建时可以指定当前切片的长度和容量。一个简单使用切片的例子如下:package main import fmt func main() { arrl : [...]int{0,1,2,3} slice1 : arr1[0:4] slice2 : make([]int, 4, 4) for i : 0; i 4 ;i { slice2[i] i } slice3 : append(slice1, 5) slice4 : []int{0, 1, 2, 3} fmt.Println(slicel, slice2, slice3) }访问和修改切片中的数据与数组一致通过下标即可实现。在上述代码中对slice1添加新的数据时由于 slice1的容量不足以容纳新的数据它将发生扩容操作生成一个新切片 slice3它和 slice1对应的底层数组不同slice1对应数组中的数据不会发生变化。还有一个需要注意的问题是由于 append函数每次都会返回新的切片为了避免数据的丢失在每次使用 append 函数添加新的数据后都要保证后续使用的切片为 append 函数最新返回的切片。最后在声明和初始化 slice4 时只要不指定“”中的大小即可声明和初始化一个切片否则将会声明和初始化一个固定长度的数组。3.map (字典)map 为 Go 提供的映射关系容器它将键映射到对应的值。声明一个 map 的样式如下:var name map[keyType]valueType这其中keyType 即键类型valueType 即键映射的值类型。map 的初值为 nil无法直接使用可以使用 make 函数进行初始化样式如下:var name map[keyType]valueType name make(map[keyType]valueType)一个简单的使用 map 的例子如下:package main import fmt func main() { map1 : make(map[int]string) map1[1] 01 map1[2] 02 map1[3] 03 map2 : map[int]string{ 1: 01, 2: 02, 3: 03, } fmt.Println(map1[0]) value,ok : mapl[1];if ok{ fmt.Println(1 is, value) }else { fmt.Println(1 is not existed!) } fmt.Println(map2) }在初始化 map 后可以直接为 map 添加 key-value 映射关系如 map1也可以在初始化时直接使用key-value 对为 map 添加映射关系如 map2。判断一个键是否在 map 中存在可以使用以下句式:value,ok : map[key]如果键存在于 map 中ok 将会返回 true。如果直接查询 map 中不存在的键将会返回值类型的初值如上述例子中查询 map1[0]将会返回 int 类型的初值 0。流程控制逻辑判断对于日常开发来说必不可少接下来我们就来了解 Go 中常用的流程控制语句for、if-else、switch 和 defer.1.for 循环Go 中循环语句仅提供for 循环结构基本形式如下:for init;condition;end{ circle body }其中init 为初始化语句仅在第一次循环前执行condition 为条件表达式在每次循环中判断是否满足执行条件énd 为结束语句在每次循环结束时执行。上述三者都可以缺省此时for将变成一个无限循环语句。如果缺省初始化语句和结束语句for 将变得类似 while 语句而 Go 中不存在 while 关键字。一个简单的for 循环使用例子如下:package main import fmt func main() { sum1 : 0 for i : 0 ; i 10 ; i{ sum1 i } sum2 : 1 for sum2 100{ sum2 * 2 } sum3 : 1 for { if sum3 100{ sum3 * 2 }else { break } } fmt.Println(suml, sum2, sum3) }在 for 循环中可以使用 break 关键字跳出当前循环或者使用 continue 关键字跳到下一个循环。2.分支控制Go 中提供两种分支控制语句分别为 if-else 语句和 switch 语句。 if-else 语句用于进行条件分支控制简单的表达式如下if conditionl { branch1 } else if condition2 { branch2 }else { branch3 }switch是比if-else 更为简便的用于编写大量条件分支的方法。Go 中的 switch与其他编程语言类似但存在不同之处Go 中的 switch 只执行匹配 case 后面的代码块无须使用 break 关键字跳出 switch 选择体。除非明确使用 fallthrough 关键字对上下两个 case 进行连接否则 switch 执行完匹配 case 后面的代码块后将退出switch。一个简单的 switch 例子如下:package main import ( fmt time ) func main() { nowTime : time.Now() Span classhljs-keywordswitch/span nowTime.Weekday(){ span classhljs-keywordcase/span time.Saturday: fmt.Println(span classhljs-stringtake a rest/span) span classhljs-keywordcase/span time.Sunday: fmt.Println(span classhljs-stringtake a rest/span) Span classhljs-keyworddefault/span: fmt.Println(span classhljs-stringyou need to work/span) } span classhljs-keywordswitch/span { Span classhljs-keywordcase/span nowTime.Weekday() gt; time.Mondayamp;amp; nowTime.Weekday() lt; time.Friday: fmt.Println(span classhljs-stringyou need to work/span) span classhljs-keyworddefault/span: fmt.Println(span classhljs-stringtake a rest/span) }当 switch 后没有携带需要判断的条件时就可以在case 后面使用判断表达式如上述代码所示这种写法就与 if-else 语句十分类似但显得更为清晰。3.defer延迟执行Go 中提供 defer 关键字来延迟执行函数被 defer 延迟执行的函数会在 return 返回前执行所以一般用来进行资源释放等清理工作。多个被 defer 的函数会按照先进后出的顺序被调用类似栈数据结构的使用。我们通过一个简单的例子来理解 defer 关键字的使用package main import fmt func add(a, b int) int { return a b } func main() a : 1 b : 2 defer fmt.Println(front result: , add(a, b)) // 3 a3 b4 defer fmt.Println(last result: , add(a, b)) // 7 a5 b6 fmt.Println(add(a, b)) / / 11 }按照defer 先进后出的执行顺序预期的执行结果为1173在上述例子中还需要注意的是传递给defer执行的延迟函数的参数会被立即解析而非等待到正式执行时才被解析。小结无可否认Go是一门优秀的服务端开发语言具备语法简单、性能优越、静态类型安全、自动垃圾回收等诸多优点。主要介绍了Go基本语法包括函数声明、变量声明与初始化、指针和 struct;数据结构包括数组、切片和字典;流程控制包括 for 循环、if-else 与 switch 分支控制、defer 延迟执行。