《Go语言开发实战》笔记

1 基础部分

Go语言基本结构及说明

package main
import (
        "fmt"
        "runtime"
)
func main()  {
        fmt.Println("hello world")
        // 查看版本
        fmt.Println(runtime.Version())
}

func main()是程序入口。所有Go函数以关键字func开头,每一个可执行程序都必须包含main()函数,通常是程序启动后第一个执行的函数,如果有init()函数则会先执行init()函数。

除此之外,还有以下几点值得注意。

  • (1)只有package名称为main的包可以包含main()函数。
  • (2)一个可执行程序有且仅有一个main包。
  • (3)通过import关键字来导入其他非main包。
  • (4)可以通过import关键字单个导入,也可以同时导入多个。

2 基本语法

2.1 变量

变量的本质是计算机分配的一小块内存,专门用于存放指定数据,在程序运行过程中该数值可以发生改变;变量的存储往往具有瞬时性,或者说是临时存储,当程序运行结束,存放该数据的内存就会释放,该变量就会随着内存的释放而消失。就像日常生活中存放水的水杯,当水杯损坏的时候,装在里面的水也会流失掉。

变量又分为局部变量和全局变量。

  • 局部变量,是定义在大括号({})内部的变量,大括号的内部也是局部变量的作用域。
  • 全局变量,是定义在函数和大括号({})外部的变量。

Go 语言的变量名由字母、数字、下画线组成,首个字符不能为数字;Go 语法规定,定义的局部变量若没有被调用会发生编译错误。

变量声明

未初始化变量的默认值有如下特点:

  • 整型和浮点型变量默认值:0。
  • 字符串默认值为空字符串。
  • 布尔型默认值为false。
  • 函数、指针变量、切片默认值为nil。

初始化变量的标准格式如下:

var a int = 199 //初始化变量的标准格式
var b = 100 //初始化变量的编译器自动推断类型格式
c := 123 //初始化变量的简短声明格式

使用 := 赋值操作符可以高效地创建一个新的变量,称为初始化声明。声明语句省略了 var 关键字,变量类型将由编译器自动推断。这是声明变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。该变量名必须是没有定义过的变量,若定义过,将发生编译错误。

package main
func main(){
  // 这样写是错误的
        var a = 123
        a := 222
}
./variable.go:7:4: no new variables on left side of :=

多个短变量声明和赋值中,至少有一个新声明的变量出现在左侧,那么即便其他变量名可能是重复声明的,编译器也不会报错。情况如下所示:

package main
import "fmt"
func main(){
  // 这种情况不会报错
        var a = 666
        a, b := 1,2
        fmt.Println("a>>>",a) // 1
        fmt.Println("n>>>",b) // 2
}

虽然这种方法不会报错,但是在使用过程中应尽量避免。

变量多重赋值

变量多重赋值是指多个变量同时赋值。

Go语法中,变量初始化和变量赋值是两个不同的概念。Go语言的变量赋值与其他语言一样,但是Go提供了其他程序员期待已久的多重赋值功能,可以实现变量交换。多重赋值让Go语言比其他语言减少了代码量。

go语言中使用多重赋值进行变量交换的例子:

package main
import "fmt"
func main(){
        var a = 666
        a, b := 111,222
        // 变量交换
        b, a = a, b
        fmt.Println("a>>>",a) // 222
        fmt.Println("n>>>",b) // 111
}

需要注意的是,多重赋值时,左值和右值按照从左到右的顺序赋值。这种方法在错误处理和函数当中会大量使用。

匿名变量

Go语言的函数可以返回多个值,而事实上并不是所有的返回值都用得上。那么就可以使用匿名变量,用下画线“_”替换即可。

例如,定义一个函数,功能为返回两个int型变量,第一个返回10,第二个返回20,第一次调用舍弃第二个返回值,第二次调用舍弃第一个返回值,具体语法格式如下所示。

package main
import "fmt"

func getData()(int, string, bool){
        return 666,"whw",false
}

func main(){
        var a = 666
        a, b, _ := getData()
        fmt.Println("a>>>",a) // 666
        fmt.Println("n>>>",b) // whw
}

匿名变量既不占用命名空间,也不会分配内存。

2.2 数据类型

在Go语言中,有以下几种数据类型:

基本数据类型(原生数据类型):整型、浮点型、复数型、布尔型、字符串、字符(byte、rune)。

复合数据类型(派生数据类型):数组(array)、切片(slice)、映射(map)、函数(function)、结构体(struct)、通道(channel)、接口(interface)、指针(pointer)。

整型

整型分两大类。有符号整型:int8、int16、int32、int64、int。

无符号整型:uint8、uint16、uint32、uint64、uint。

其中uint8就是byte型,int16对应C语言的short型,int64对应C语言的long型。

![image-20201022114615738](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022114615738.png)

浮点型

![image-20201022114652127](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022114652127.png)

常量math.MaxFloat32表示float32能获取的最大值,大约是3.4×1038;常量math.SmallestNonzeroFloat32表示float32能获取的最小值,大约为1.4×10-45。

常量math.MaxFloat64表示float64能获取的最大值,大约是1.8×10308;常量math.SmallestNonzeroFloat64表示float64能获取的最小值,大约为4.9×10-324。

复数型

复数型用于表示数学中的复数,如1+2j、1-2j、-1-2j等。关于复数型的说明,如表2.3所示。

![image-20201022115103164](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022115103164.png)

布尔型

布尔型用预定义标识符bool表示。在C语言中,对于布尔型的值定义,非0表示真,0表示假。而在Go语言中,布尔型的值只可以是常量true或者false。

声明方式如下所示。

var flag bool

布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串

字符串在Go语言中是以基本数据类型出现的,使用字符串就像使用其他原生基本数据类型int、float32、float64、bool一样。

字符串在C++语言中,以类的方式进行封装,不属于基本数据类型。

在go中使用字符串:

var s1 string //定义名为s1的字符串类型变量
s1 = "HelloWorld" //变量赋值

student1 := "火影whw" //以自动推断方式初始化

有些字符串没有现成的文字代号,所以只能用转义字符来表示。常用的转义字符如表2.4所示:

![image-20201022115523155](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022115523155.png)

定义多行字符串的方法如下:

  • 双引号书写字符串被称为字符串字面量(string literal),这种字面量不能跨行。
  • 多行字符串需要使用反引号“`”,多用于内嵌源码和内嵌数据。
  • 在反引号中的所有代码不会被编译器识别,而只是作为字符串的一部分。
  • 多行字符串定义方式如例2-1所示。
package main
import "fmt"

func getString()(string){
        s1 := `
                x := 123
                y := 666
                ss := "A Hero\'s Country!"
     `
        return s1
}

func main(){
        a := getString()
        fmt.Println("a>>>",a)
}

字符

字符串中的每一个元素叫作“字符”,定义字符时使用单引号。Go语言的字符有两种,如表2.5所示。

![image-20201022120100841](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022120100841.png)

声明示例如下(注意必须是单引号!):

package main
import "fmt"

func main(){
        // 注意必须是单引号!!!
        var a byte = \'s\'
        var b rune = \'王\'
        fmt.Println("a>>>",a) // 115
        fmt.Println("b>>>",b) // 29579
}

2.3 打印格式化

打印格式化通常使用fmt包,通用的打印格式如表:

![image-20201024100236018](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024100236018.png)

具体的使用方法:

package main
import "fmt"
func main(){
        str := "wanghw"
        fmt.Printf("%T, %v \n",str,str) //string, wanghw

        var a rune = \'王\'
        fmt.Printf("%T, %v \n",a,a) //int32, 29579

        var b byte = \'b\'
        fmt.Printf("%T, %v \n",b,b) //uint8, 98

        var c int32 = 123
        fmt.Printf("%T, %v \n",c,c) //int32, 123
}

通过上例可以看出,使用通用的格式打印,输出的结果可能不是自己想要的,为了确保输出结果与需求一致,还需要学习具体格式的打印方式。

布尔型打印格式

![image-20201024100347282](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024100347282.png)

package main

import "fmt"

func main(){
        var flag bool
        // flag默认是false
        fmt.Printf("%T, %t \n",flag,flag) //bool, false

        flag = true
        fmt.Printf("%T, %t \n",flag,flag) //bool, true
}

整型打印格式

![image-20201024100705972](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024100705972.png)

package main
import "fmt"
func main(){
        fmt.Printf("%T, %d \n",123, 123) //int, 123
        fmt.Printf("%T, %5d \n",123, 123) //int,   123
        fmt.Printf("%T, %05d \n",123, 123) //int, 00123
        fmt.Printf("%T, %b \n",123, 123) //int, 1111011
        fmt.Printf("%T, %o \n",123, 123) //int, 173
        fmt.Printf("%T, %c \n",98, 98) //int, b
        fmt.Printf("%T, %q \n",98, 98) //int, \'b\'
        fmt.Printf("%T, %x \n",123, 123) //int, 7b
        fmt.Printf("%T, %X \n",123, 123) //int, 7B
        fmt.Printf("%T, %U \n",\'王\', \'王\') //int32, U+738B
}

浮点型与复数型打印格式

![image-20201024101359915](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024101359915.png)

package main
import "fmt"
func main(){
        fmt.Printf("%b \n",123.23433) //8671845041675824p-46
        fmt.Printf("%f \n",123.2) //123.200000
        fmt.Printf("%.2f \n",123.22222) //123.22
        fmt.Printf("%e \n",123.22222) //1.232222e+02
        fmt.Printf("%E \n",123.22222) //1.232222E+02
        fmt.Printf("%.1e \n",123.22222) //1.2e+02
        fmt.Printf("%F \n",123.22222) //123.222220
        fmt.Printf("%g \n",123.22222) //123.22222
        fmt.Printf("%G \n",123.22222) //123.22222 
}

关于复数的打印格式如下

package main
import "fmt"
func main(){
        var value complex64 = 2.2 + 22i
        value2 := complex(2.2,222)
        fmt.Println(real(value)) //2.2
        fmt.Println(imag(value)) //22
        fmt.Println(value2) //(2.2+222i)
}

字符串打印与字节数组的打印格式

![image-20201024102229661](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201024102229661.png)

package main
import "fmt"
func main(){
        arr := []byte{\'a\',\'b\',\'c\',\'d\'}
        fmt.Printf("%s \n","火影whw")//火影whw
        fmt.Printf("%q \n","火影whw")//"火影whw"
        fmt.Printf("%x \n","火影whw")//e781abe5bdb1776877
        fmt.Printf("%X \n","火影whw")//E781ABE5BDB1776877

        fmt.Printf("%T, %s \n",arr, arr)//[]uint8, abcd
        fmt.Printf("%T, %q \n",arr, arr)//[]uint8, "abcd"
        fmt.Printf("%T, %x \n",arr, arr)//[]uint8, 61626364
        fmt.Printf("%T, %X \n",arr, arr)//[]uint8, 61626364 
}

2.4 数据类型转换

Go语言采用数据类型前置加括号的方式进行类型转换,格式如:T(表达式)。T表示要转换的类型;表达式包括变量、数值、函数返回值等。

类型转换时,需要考虑两种类型之间的关系和范围,是否会发生数值截断。就像将1000毫升的水倒入容积为500毫升的瓶子里,余出来500毫升的水便会流失。值得注意的是,布尔型无法与其他类型进行转换

package main
import "fmt"
func main(){
        a := 100
        b1 := float64(a)
        b2 := string(a)
        fmt.Println("b1>>>",b1)
        fmt.Println("b2>>>",b2)
}

浮点型与整型之间转换

float和int的类型精度不同,使用时需要注意float转int时精度的损失。

package main

import "fmt"

func main() {
        chinese := 90
        english := 98.9

        avg1 := (chinese + int(english)) / 2
        avg2 := (float64(chinese) + english) / 2

        fmt.Println("avg1>>>",avg1) // 94
        fmt.Println("avg2>>>",avg2) // 94.45
}

整型转字符串类型

这种类型的转换,其实相当于byte或rune转string。

int数值是ASCII码的编号或unicode字符集的编号,转成string就是根据字符集,将对应编号的字符查找出来。当该数值超出unicode编号范围,则转成的字符串显示为乱码。例如,19968转string,就是“一”。

备注:

  • ASCII字符集中数字的十进制范围是48~57;
  • ASCII字符集中大写字母的十进制范围是65~90;
  • ASCII字符集中小写字母的十进制范围是97~122;
  • unicode字符集中汉字的范围是4e00~9fa5,十进制范围是19968~40869。

详情如下:

![image-20201022140218438](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022140218438.png)

具体的使用方法如下:

package main
import "fmt"
func main() {
        a := 97
        x := 19969

        ret1 := string(a)
        ret2 := string(x)
        fmt.Println("ret1>>>",ret1) // a
        fmt.Println("ret2>>>",ret2) // 丁
}

Go语言中不允许字符串转int

package main
import "fmt"
func main() {
        a := "丁"
  // 不能将字符串转为int
        ret1 := int64(a)
        fmt.Println("ret1>>>",ret1) // a
}

会上报下面的错误:

cannot convert a (type string) to type int64

2.5 常量

常量是一个简单值的标识符,在程序运行时,不会被修改。常量中的数据类型只可以是布尔型、数字型(整型、浮点型和复数型)和字符串。常量的定义格式如下:

const A string = "wanghw"

const B = "whw"

const C, D = "www", "waa"

常量定义后未被使用,不会在编译时报错。

常量用于枚举

Go语言现阶段没有提供枚举,可以使用常量组模拟枚举。

假设数字0、1和2分别代表未知性别、女性和男性。格式如例:

package main
import "fmt"
const (
        Unknown = 0
        Female = 1
        Male = 2
)
func main(){
        fmt.Println("ret>>",Unknown, Female, Male)
        // 结果
        // ret>> 0 1 2
}

常量组中如果不指定类型和初始值,则与上一行非空常量的值相同:

package main
import "fmt"
const (
        a = 10
        b
        c
)
func main(){
        fmt.Println("ret>>",a, b, c)
        // 结果
        // ret>> 10 10 10
}

iota

iota,特殊常量值,是一个系统定义的可以被编译器修改的常量值。

iota只能被用在常量的赋值中,在每一个const关键字出现时,被重置为0,然后每出现一个常量,iota所代表的数值会自动增加1。

iota可以理解成常量组中常量的计数器,不论该常量的值是什么,只要有一个常量,那么iota就加1

package main
import "fmt"
const (
        a = 12
        b = iota
        c = iota
)
func main(){
        fmt.Println("ret>>",a, b, c)
        // 结果
        // ret>> 12 1 2
}

常量组中如果不指定类型和初始值,则与上一行非空常量的值相同:

package main
import "fmt"
const (
        a = iota
        b
        c
)
func main(){
        fmt.Println("ret>>",a, b, c)
        // 结果
        // ret>> 0 1 2
}

2.6 类型别名与类型定义

类型别名是Go1.9版本添加的新功能。说到类型别名,无非是给类型名取一个有特殊含义的外号而已,就像武侠小说中的东邪西毒。假如在教室中,有两个同学叫张三,老师为了区分他们,通常会给他们起个别名:大张三、小张三。对于编程而言,类型别名主要用于解决兼容性的问题。

![image-20201022201545626](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022201545626.png)

该语句是将NewString定义为string类型。通过type关键字,NewString会形成一种新的类型。NewString本身依然具备string的特性。

type StringAliaa = string

该语句是将StringAlias定义为string的一个别名。使用StringAlias与string等效。别名类型只会在代码中存在,编译完成时,不会有别名类型。

出于对程序性能的考虑,建议如下:

  • 尽可能地使用 := 去初始化声明一个变量(在函数内部)。
  • 尽可能地使用字符代替字符串。

2.7 Go语言运算符

运算符用于在程序运行时执行数学或逻辑运算。

Go语言内置的运算符包括算术运算符关系运算符逻辑运算符位运算符赋值运算符其他运算符

逻辑运算符

Go语言的逻辑运算符如表所示。假定A值为True,B值为False。

![image-20201022201853056](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022201853056.png)

位运算符

位运算符对整数在内存中的二进制位进行操作

位运算符比一般的算术运算符速度要快,而且可以实现一些算术运算符不能实现的功能。如果要开发高效率程序,位运算符是必不可少的。位运算符用来对二进制位进行操作,包括:按位与(&)、按位或(|)、按位异或(^)、按位左移(<<)、按位右移(>>)。

Go语言支持的位运算符如表2.15所示。假定A为60,B为13:

![image-20201022202344812](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022202344812.png)

(其他略)

其他运算符

![image-20201022202518777](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022202518777.png)

运算符优先级

![image-20201022202636361](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022202636361.png)

当然,读者可以通过使用括号来临时提升某个表达式的整体运算优先级。

3 Go语言的流程控制

3.1 流程控制概述

3.2 if条件判断语句

![image-20201022203808845](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022203808845.png)

先判断if的布尔表达式,如果为true,其后紧跟的语句块执行,如果为false,再判断else if的布尔表达式,如果为true,其后紧跟的语句块执行,如果为false,再判断下一个else if的布尔表达式,以此类推,当最后一个else if的表达式为false时,执行else语句块。

在if语句的使用过程中,应注意以下细节:

  • 不需使用括号将条件包含起来。
  • 大括号{}必须存在,即使只有一行语句。
  • 左括号必须在if或else的同一行。
  • 在if之后,条件语句之前,可以添加变量初始化语句,使用“;”进行分隔。

![image-20201022203944476](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022203944476.png)

if的特殊写法

if语句还有一个变体。它的语法如下所示。

if statement; condition {
  // 代码块
}
package main
import "fmt"

func main(){
        if num := 10; num %2 == 0{
                fmt.Println("偶数")
        } else{
                fmt.Println("奇数")
        }
}
// 结果
// 偶数

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

3.3 if嵌套语句

![image-20201022204550341](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022204550341.png)

![image-20201022204716080](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022204716080.png)

3.4 switch语句

![image-20201022204847524](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022204847524.png)

switch语句执行的过程自上而下,直到找到case匹配项,匹配项中无须使用break,因为Go语言中的switch默认给每个case自带break。因此匹配成功后不会向下执行其他的 case 分支,而是跳出整个 switch。可以添加fallthrough(中文含义是:贯穿),强制执行后面的case分支。fallthrough必须放在case分支的最后一行。如果它出现在中间的某个地方,编译器就会报错。变量var1可以是任何类型,而val1和val2则可以是同类型的任意值。类型不局限于常量或整数,但必须是相同类型或最终结果为相同类型的表达式。case后的值不能重复,但可以同时测试多个符合条件的值,也就是说case后可以有多个值,这些值之间使用逗号分隔,例如:case val1, val2, val3。switch后的表达式可以省略,默认是switch true。

示例

![image-20201022205034536](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205034536.png)

接下来再看一个案例,判断某年某月的天数

![image-20201022205101341](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205101341.png)

类型转换

switch语句还可以被用于type switch(类型转换)来判断某个interface变量中实际存储的变量类型。关于interface变量的知识将在后续的章节中介绍。下面演示type switch的语法。其语法结构如下所示。

![image-20201022205240516](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205240516.png)

![image-20201022205257559](/Users/wanghongwei/Library/Application Support/typora-user-images/image-20201022205257559.png)

3.5 for循环语句

循环语句表示当条件满足时,可以反复地执行某段代码。

for是Go语言中唯一的循环语句,Go没有while、do...while循环。

按语法结构来分,Go语言的for循环有4种形式,只有第一种使用分号。

for循环中for关键字后不能加小括号。

语法结构一

for 初始预计init; 条件表达式condition; 结束语句post {
  // 循环体代码
}

先执行初始语句,对控制变量赋初始值。初始语句只执行一次。

其次根据控制变量判断条件表达式的返回值,若其值为true,满足循环条件,则执行循环体内语句,之后执行结束语句,开始下一次循环。

执行结束语句之后,将重新计算条件表达式的返回值,如果是true,循环将继续执行,否则循环终止。然后执行循环体外语句。

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

初始语句、条件表达式和结束语句3种组成部分都是可选的。因此这种基本的for循环语法结构又能演化出4种略有不同的写法。

初始语句是在第一次循环前执行的语句,一般为赋值表达式,给控制变量赋初始值。如果控制变量在此处被声明,其作用域将被局限在这个for的范围内——在for循环中声明的变量仅在循环范围内可用。初始语句可以省略不写,但是初始语句之后的分号必须要写。

省略初始语句的写法:

package main
import "fmt"
func main(){
        a := 3
        for ; a<5; a++{
                fmt.Printf("%d ", a)
        }
}
//3 4

条件表达式(condition)是控制循环与否的开关:如果表达式为true,则循环继续;否则结束循环。条件表达式可以省略不写,之后的分号必须要写。省略条件表达式默认形成无限循环。

省略条件表达式的写法:

package main
import "fmt"
func main(){
        a := 3
        for ; ; a++ {
                if a > 5 {
                        fmt.Printf("%d ", a)
                        break
                }
        }
}
//6

结束语句(post),一般为赋值表达式,使控制变量递增或者递减。post语句将在循环的每次成功迭代之后执行。

语法结构二

for关键字后只有1个条件表达式,效果类似其他编程语言中的while循环。其语法结构如下所示。

for 循环条件condition{
  // 循环体代码
}
package main
import "fmt"
func main(){
        var a int
        for a<10{
                fmt.Print(a)
                a ++
        }
}
//0123456789

语法结构三

for关键字后无表达式,效果与其他编程语言的for(;