Golang万字小册总结

640 阅读20分钟

本篇文章来自我在掘金历史文章中Golang系列,合并一下总结查看

一.Go语言变量&常量

Go语言变量的三种定义方式

一.使用var关键字

可放在函数内,或直接放在包内

var s1,s2 string = "hello world"

可使用var()集中定义变量

var (

    name string

    age int

    gender string

)

二.让编译器自己决定变量类型

var a, b, c, s = 3, 4, true, "def"

三.使用:=定义变量

只能在函数内使用,可以同时声明初始化多个变量

a = 3

a, b, c, s := 3, 4, true, "def"

Go语言的自建变量类型

布尔型(bool): false,true

整数型:(u)int8,(u)int16,(u)int32,(u)int64有对应的长度大小

整数类型 int 和 uint不规定长度,根据操作系统来指定是32bit或者 64bit

指针:uintptr

字符串类型:byte,rune

byte:字节(8位),ASCII 码的一个字符

rune:字符型(32位,四字节),一个 UTF-8 字符

用来表示 Unicode 字符的 rune 类型和 int32 类型是等价的

byte 和 uint8 也是等价类型,byte类型一般用于声明一个数据而不是整数

浮点数类型:float32,float64

复数:complex64,complex128

complex:复数,实部虚部,实部虚部各占一半空间,complex64实部虚部各32位

常量说明

变量就是可以改变的数,没有固定的值,常量就是在不会修改的值,定义常量的数值在编译阶段就已经确定了,运行时不能够修改,在Go语言中常量的数据类型可以是布尔型,数字类型和字符串类型,数字类型包括整数类型,浮点数类型,复数,常量的定义的关键字为:const ,使用const来定义常量跟Javascipt ES6的用法一样,一旦定义就不能允许修改。

最基础的采用const定义常量方法:

const s1 string = "hello"

还可以使用类似定义变量的方式让编译器决定常量的数据类型的方法:

const s2 = "world"

也可以同时声明多个常量

const s3,s4 = "hello","world"

还可以用括号定义多个常量

const(

    s5 = "hello"

    s6 = "world"

)

常量在日常开发工作中很少会遇到,但是Go语言中支持一种特殊的常量方式:iota,这是Go语言的一个独特的语法糖。当iota和const一起用的时候,随着每一个常量的增加,iota也会随着常量的个数加1,只能作用在const常量声明里 简单用法:

const a = iota //此时iota的值为 0 

const (

    b = iota //此时iota的值为 0, const 再次出现的时候,都会让 iota 初始化为0,定义下一个常量才会加1 

    c  // 此时iota的值为 1 iota的值自动赋值给c

    d  // 此时d的值为 2 iota的值自动赋值给d

)

从上面的例子可以看出iota也可以叫做常量计数器,记录从iota出现开始,记录定义常量的个数。还可以用乘法*来实现一些高级的用法,比如实现一个等差数列:

   const (

        a = iota*2 //此时iota的值为 0, 乘2也等于0

        b  // 此时b的值为 2 ,1*2=2,iota的值自动赋值给c

        c  // 此时c的值为 4 ,2*2=4 iota的值自动赋值给d

        d // 此时d的值为 6 ,2*2=4 iota的值自动赋值给d

    )

突发奇想,实现一个自然数的完全平方数的数列:

const (

    a = iota*iota //此时iota的值为 0, 乘2也等于0

    b  // 此时b的值为 1 ,1*1=2,iota的值自动赋值给c

    c  // 此时c的值为 4 ,2*2=4 iota的值自动赋值给d

    d // 此时d的值为 9 ,3*3=4 iota的值自动赋值给d

    )

常量可以用len(), cap()等内置函数来计算常量的长度和容量等信息。

二.Go语言流程控制

条件语句

Go语言的条件语句主要有if,switch, select

if条件语句

简单的if是日常开发中最常用的条件语句了

与PHP不同的是判断条件是不用加()

还有就是{}前面的{不能独占一行

例如:

const a = 1

if a>3{

    fmt.Println("a大于3")

}

还有一个就是if else:

const a = 1

if a>3{

    fmt.Println("a大于3")

}else{

    fmt.Println("a不大于3")

}

switch

switch一般与case和default一起用

例如:

var s1 = 1;

switch s1 {

    case 1:

        fmt.Println("s1=1")  //只会在这里输出

    case 2:

        fmt.Println("s1=2")

    default:

        fmt.Println("s1啥也不是")

        }

case 自带break,PHP在switch的时候语言每个case里面加defalt非常繁琐,如果要执行后面的case 可以用fallthrough关键字。如果case里面有fallthrough他会无条件执行下一个case里面的内容。不会管下个case条件是不是True。

switch的可以是任何类型但是要与case的类型相同,所以说不能用switch case同时判断一个变量是否是整数还是字

符串。

var s1 = 1

switch s1 {

    case 1:

        fmt.Println("s1=1")  //会在这里输出

        fallthrough;

    case 2:

        fmt.Println("s1=2") //也会在这里输出

    default:

        fmt.Println("s1啥也不是")

    }

循环语句

for

跟其他的语言不一样,没有while True这种循环

for {

    fmt.Println("hello") 

}

这样就是死循环,省略了True,当然加个True也行

for True {

     fmt.Println("hello")

}

for循环可以使用条件判断,限制条件用来跳出循环

for i=0;i<10;i++ {

    fmt.Println(i)

}

select

select 这里就不仔细说,先埋好坑,等着我们讲到channel Go语言管道这里再说,select 能让程序线程在多个channel的操作上等待,goroutine(协程)和channel(管道)之间select起着很重要的作用,后面的这些东西才是Go语言的设计的魅力所在,Go语言并发思想才是Go语言的精髓。

break continue goto

break,continue,goto三个关键字。用来控制循环体内的执行流程,break和其他语言一样。跳出当前整个的for循环或者switch循环。

continue跳出当前的循环,进入下一轮循环。

goto这个东西在很多语言里面都是不提倡使用的,很容易造成执行混乱,在这里Go语言也一样不推荐使用,它的意思是跳到标记的地方执行代码。

要补充说明一下经常和 for一起使用的关键字range,range 跟Python一样的,用于遍历数组,切片,集合等数据结构中的元素。这些数据结构后面再详细说明,现在了解下最简单是使用方法遍历字符串:

for i,j :=range "go" {

    fmt.Println(i,j); //i是下标,j是字符串的ascii码(Go语言特点,后面会用UTF8的包解析这个)

}

三.Go语言函数

函数几乎是每个语言都要用到的语法,函数的封装目的就是代码复用,Go语言跟C和C++一样使用了一个叫main()函数的入口函数,main()函数作为了整个程序执行的入口,可想而知Go语言函数是多么重要,Go语言也有类似面对对象的思想,要深入理解后面的面对对象代码复用就必须熟悉函数这里的知识。

函数基础

Go语言的函数关键字就是func,func让我联想到的是Javascipt和php的函数function的简写,定义一个函数例子:

func test(){

    fmt.Println("hello world")

}

test()  //调用test函数,输出hello world

函数的参数与返回值

参数

跟大多数语言一样可以传递参数,用在函数内部进行计算,参数必须指定参数类型。可以传递单个,多个参数,不定参数(不指定参数个数) 参数类型除了常用的整数,字符串还可以传递指针,函数,结构体(后面在讲)等等。

看下传递不定参数的例子:

var s1,s2,s3 = "hello","world","Go"

func test(args ...string){

    for _,value := range args{  //_表示一个遍历下标的,因为它不输出,但是要给它一个位置,它很特殊,因为Go语言定义的变量不使用就会报错,但是_除外

        fmt.Println(value)  //value就是args里面的每一个值

    }

}

test(s1,s2,s3) //在main函数中执行字符串个数可以传递多个,个数不确定,依次输出"hello","world","Go"

指针

因为指针通常会跟函数一起用,用指针传递节约内存空间,用值传递就会占用一个内存空间存放同样的值。指针就是存放变量或者常量。函数等的内存地址,可以用 &表示指针存放的地址代表变量存放的内存地址 ,内存地址不可以参与运算,可以用指针代表的值进行运算,想要学会Go语言的高级功能这个必须要深入理解,这里举一个简单的指针参数的例子():

var s1 int = 1

func test(a * int){ //test函数接收一个int类型的指针参数,形参为a,形参不懂可以去百度一下,哈哈

    fmt.Println(*a)  //输出a这个指针所存放实际的值,1

    mt.Println(&a)  //输出a这个指针的地址,例如像:0xc00000e031

}

test(&s1)  //在main函数中执行,传递实参的内存地址的值,可以让test函数自动识别这是个指针,而不是字符串。

这里非常重要,对于没有使用过指针这个概念的语言的人来说还是有点难度。再来举一个例子加深印象,使用指针交换两个变量的值

var s1 ,s2 = "hello","world" //这里又换了一种定义变量的方法,记不清楚看第二篇,哈哈,让编译器自己决定变量类型





func change(s1, s2 *string) {  //接收指针存放的值为string类型的变量的内存地址(变量的内存地址也叫指针)

    // 取s1的指针的地址, 给临时变量value

    value := *s1

    // 取s2的指针的地址, 给s1指针指向的变量

    *s1 = *s2

    // 将s1的指针的地址(也就是中间临时变量的指针地址)给s2的指针指向的变量

    *s2 = value

}

//下面三句话在main函数执行

fmt.Println("s1:",s1,"s2:",s2) //输出原值 s1: hello s2: world

change(&s1,&s2);  //传入两个变量的内存地址函数内交换

fmt.Println("s1:",s1,"s2:",s2)  // 输出交换后的值s1: world s2: hello

指针基础用法到此了解这里,更高级的用法在后面

返回值

Go语言函数的肯定会有返回值,返回值这个东西可有可无,返回值用return 这个关键字。在定义函数的时候 还可以定义它的返回的类型和个数,名称,这样做的好处是可以做到用在函数最后使用单个关键字return 来直接返回函数处理后的结果(前提在函数中定义了返回值的名称),例如:

//这里又换了一个定义变量的方式

var (

    name string = "Go"

    age int = 12

    gender string = "name"

)



//经过实验证明形参名不能与返回参数名字相同,不然会出现重复定义的错误

func test(name string , age int,gender string )(nameplus string , ageplus int,genderplus string ) { //返回值这里可以省略返回值名字,只需要类型就行了,但是return那里必须指定变量名

    nameplus = name+" nb"; //加减需要搞清楚变量类型

    ageplus = age+10086

    genderplus = gender+"???"

    return  //只要在return指定定义了返回值名称,就可以单独使用return返回这些返回值,真方便

}



fmt.Println(test(name, age,gender)); //在main函数执行,输出内容:Go nb 10098 name???

闭包,匿名函数,函数也可以传递参数,函数也可以当作返回值

闭包

Go语言的闭包简单来说就是能够读取其他函数内部变量的函数,我们直接先来个例子(完整的):

package main

import "fmt"



func test() func(int) int {

    sum := 10

    return func (v1 int) int {

        sum += v1

        return sum

    }

}



func main() {

    //函数也可以当作变量赋值,此时mytest可以叫做‘回调函数’,必须要做这一步,因为test函数没有接收参数,mytest表示接收test函数返回值(返回值是一个匿名函数,可以传参),下面可以传参。

    mytest := test() //这个时候也可看作已经调用了test函数,sum已经初始化为10

    for i := 0; i<10; i++ {

        //循环的第一遍,输出10,test里sum = sum+0;循环第二遍,输出11,i为2

        //依次类推接下来输出13,16,20,25....55

        //为什么sum每次循环没有被初始化?实际上mytest调用的是里面的匿名函数,没有名字的函数,sum在上面mytest := test();已经初始化了

        //后面的每一次循环都会使用上次循环计算完的sum,每次循环都会return sum给下次用。

        fmt.Println(mytest(i))

    }

}

闭包实现了一个匿名函数,test里面return 的函数就是匿名函数,没有函数名。闭包的特点就是匿名函数内部引用了外部函数的参数或变量,sum就是内部函数调用的外部函数的变量,那么假如不用闭包怎么实现这个功能呢?闭包有什么好处呢?我们来写个代码试试:

package main

import "fmt"



func main() {

    sum :=10    

    for i := 1; i <= 10; i++ {

        sum += i;

        fmt.Println(sum)

    }

}

可以看到此时sum这个变量和闭包里面的sum生命周期就不一样了,闭包匿名函数使用的外部函数变量生命周期随着闭包,没有调用它就被内存回收机制回收了,sum就是局部变量,每次不是复制出来的,所以比第二种快很多,而第二种情况,sum还可以被后面的代码使用,每次都会给sum全局变量重新复制赋值。main函数执行完成才会被回收。

四.数组&切片

数组

Go语言数组和其他语言区别有点大,Go语言规定了数组容量,其他并且在日常开发中不推荐使用,很少用使用,因为数组的长度是固定的,推荐使用切片,切片是数组的变种,切片长度是可以变的,我们下期再讲切片,我们现在讲讲数组,我们先来定义一个数组:

var arr1 [5] int  //定义了一个长度(容量)为5,数组内容元素为int类型

var arr2 = [5]int{} //定义了一个长度(容量)为5,数组内容元素为int类型

[]里定义数组容量,{}里初始化数组元素,数组元素个数不能大于数组容量

var arr3 = [5]int{1,2,3,4,5} //给这个数组初始化:

还可以定义并初始化一个不定长度的数组并让元素的个数来决定数组长度:

var arr4 := [...]int{1,2,3,4,5}

通过数组下标访问数组元素:

var num1 = arr4[0]  //1

fmt.Println(num1)

修改第一个元素:

arr4[0] =9

fmt.Println(arr4)  //[9 2 3 4 5]

讲函数的时候我们用了for循环遍历了字符串,这次我们来遍历数组:

for i,j := range arr4{

    fmt.Printf("i为:%d,j为:%d;",i,j); //i为:0,j为:9;i为:1,j为:2;i为:2,j为:3;i为:3,j为:4;i为:4,j为:5;

}

二维数组

arr5 := [2][4]int{{1, 2, 3, 4 }, {5, 6, 7, 8}}  //外层两个数组,每个数组4个元素

二维数组元素可以比定义的容量少,剩余的会被初始化为0,初始化的值取决于数组元素的数据类型:

arr6 := [2][4]int{{1, 2, 3}, {5, 6, 7}} 

fmt.Println(arr6)  //输出[[1 2 3 0] [5 6 7 0]]

一维的数组也同理,可以初始化一半的容量的数组

arr7 := [4]int{5, 6}  //外层两个数组,每个数组4个元素

fmt.Println(arr7)   //输出[5 6 0 0]

切片

数组的长度是固定的,切片的长度是可变的。现在我们来看下切片的例子:

func main() {

    slice1 := []string{"a", "b", "c"}  //[]里没有定义长度

    fmt.Println(slice1) //[a b c]

    slice2 := make([]int, 3, 5) //使用make初始化一个长度为3的数组切片,元素初始值为整数0,容量为5的切片

    fmt.Println(slice2) //[0 0 0]

    slice3 := make([]int, 3) //使用make初始化一个长度为3的数组切片,元素初始值为整数0

    fmt.Println(slice3) //[0 0 0 0 0]

}

切片扩容

在切片的底层数据结构中会有一个数组,切片可以看作对某个数组的的片段的引用,所以数组是一个值类型的变量,切片是一个引用类型的变量。值类型和引用类型的区别:值类型的变量是直接存放实际的数据,引用类型的变量存放的则是数据的地址,切片的容量就是这个底层数组的长度,那么现在切片需要扩容假如超过了这个底层数组的长度怎么办?

我们先来看看看如何扩容切片,这个操作肯定是非常常见的:

slice4 := append(slice2, 3,4,5,6,7,8,9) //使用append()来扩容  slice2定义了在上面容量只有5,现在加了这么多int类型元素

fmt.Println(slice4) //[0 0 0 3 4 5 6 7 8 9]

假如我们用数组扩容呢:

array1 := [3]string("1","2","3")

array2 := append(array1,"4","5")  //会报错, first argument to append must be slice,

fmt.Println(array2)  

啊,原来append这个方法专门为切片使用的,数组的所有元素在定义的时候就已经确定了,只能修改,上篇有说,没有给值的默认是为该数组元素类型的默认值。

原来数组这么不中用,感觉啥也干不了,在切片扩容时,超过底层数组容量的时候,底层数组会自动扩容,原有的元素和新元素一并拷贝到新切片,这个切片是基于旧的数组,这个底层数组永远不会变,只是在后面加元素。数组原理扩容属于一个扩展知识,一般会按底层数组的容量1.25倍扩展,如果追加的元素过多,这个倍数是2倍,有兴趣可以百度搜一下扩容原理。

切片总结

数组的容量永远是它的长度,而切片的容量就是底层数组的长度。切片比数组多了一个容量这个概念,在面试的时候经常会考这个,但实际中在大多数情况下不用关心这个问题,但是用Go语言就必须清楚它所有的设计细节,不然到真实使用的时候展现不出它的优点。

五.Map

Go语言的第三种集合类型,Map(映射),一个无序的 K-V 键值对集合,Key的数据类型必须相同,Value也一样,但是Key的数据类型和Value的数据类型可以不一样,看看如何初始化Map,添加数据

func main() {

    dict1 := make(map[int]string)  //使用make定义一个空映射,value类型为string,key类型为int

    fmt.Println(dict1)  //map[]

    dict1[0] = "hello"

    dict1[1] = "world"

    fmt.Println(dict1)    //map[0:hello 1:world]

}

删除键值

如果要删除其中一个键值对,那么就要用delete方法,还是拿上面的dict例子:

delete(dict1,0)

fmt.Println(dict1)    //map[1:world]

Go 语言的map 可以获取不存在的键值对,如果键不存在,会返回该map的类型的零值。

零值补充知识:

如果map的value类型为string,获取map不存在的键值对的时候会返回为" "

依次类推bool类型零值为 false

int类型零值为 0

float类型零值为 0.0

[]第二个参数

map使用[]一般用来获取键对应的值,只有一个值,其实还可以获取两个值,第二值是bool类型,判断这个键值是不是存在。来举个例子:

    myvalue1,status1:=dict1[1]  //在上面dict1为:map[1:world]

    if status1 {

        fmt.Println("键值:"+myvalue1) //键值:world

    }

    _,status2:=dict1[0]

    if !status2 {

        fmt.Println(status2)  //false

    }

遍历map

来使用for 循环来遍历下map:

    dict3 := map[string]int{"hello":0,"word":1,"go":2}

    for key,val := range dict3{

        fmt.Println(key,val)  //hello 0   word 1   go 2

    }

map没有容量,只有长度,也就是键值对的个数,也是用内置方法len计算:

fmt.Println(len(dict3))   //3

Map补充知识

我看这个map跟json挺像,试试转字符串:

package main

import (

    "fmt"

    "encoding/json"  //处理json的包

)



func main() {

    dict3 := map[string]int{"hello":0,"word":1,"go":2}  //定义一个map

    str, err := json.Marshal(dict3)  //json转成[]byte类型:[123 34 103 111 34 58 50 44 34 104 101 108 108 111 34 58 48 44 34 119 111 114 100 34 58 49 125]   ,跟易语言挺像,哈哈

    if err !=nil{

        fmt.Println("转换失败")

    }else{

        fmt.Printf("%c",str)  //格式化字符串输出 :[{ " g o " : 2 , " h e l l o " : 0 , " w o r d " : 1 }]

    }

}

继续补充Go格式化输出%后面各种字母的意思,(必须搭配fmt.Printf使用,不是fmt.Println):

printf("%c",a);输出单个字符。

printf("%d",a);输出十进制整数。

printf("%f",a);输出十进制浮点数。

printf("%o",a);输出八进制数。

printf("%s",a);输出字符串。

printf("%u",a);输出无符号十进制数。

printf("%x",a);输出十六进制数。

总结

map键值对是无序的,Go语言的map是hash结构的,平均访问时间复杂度是O(1)的,所以通过键取值效率非常高。

六.Go语言方法

方法不是和函数是一样的吗?Go语言里面方法和函数是两个概念。有什么不同呢?我们先来回顾一补充下方法是什么。

函数的回顾补充

我们知道Go语言一个非常重要的函数:main 函数。我们在举例子的时候都会用到它。main函数通常也会调用其他函数,我们来一个完整例子:

package main

import "fmt"



func add(a int, b int ) int {

    return a + b

}



func main() {

    a:=1 

    b:=2 

    fmt.Println(add(a, b)) //3

}

总结一下一般函数有几个部分组成:

关键字:func

函数名:add

函数的传入参数:a int,b int

函数的返回值类型:int

函数的执行体: return a + b

补充:Println 这个也叫函数,这个函数属于fmt这个包的。Println是一个公有函数,可以在fmt这个包外面调用,公有函数首字母必须大写,小写字母开头的函数只能在同一个包里面被调用执行,任何一个函数只属于一个包。

方法

方法最大的不同就是方法必须要有一个接收者,方法不要接收者几乎是一个函数,接收者就是一个类型,这样就把一个类型和一个方法绑定在一起了,这个方法就是这个类型的方法。有点懵,下面举个例子:

package main

import "fmt"







type Test int

func (test *Test) plus(){   //test Test 

    return test+10086

}



func (test *Test) plus(){   //test Test 

    return test+10086

}



func main(){

    Test.plus(1)

}

type我们会在后面的结构体里面说。在上面例子中定义了一个名字为Test 类型为string的类型。可以理解为面对对象中的类名,class 的名字,type声明了一个接收者。

type 和下面的func的内容就是一个完整的方法。

func 关键字后第一个括号里面 test Test 指定一个接收者,也就是绑定了一个接收者。

Show就是方法名 Show() 是类型 Test 的方法。

main函数中 Test.Show("mytest") 就是调用了接收者绑定的方法

一个接收者可以拥有有多个方法。这样看来像不像面对对象的类里面的方法?

值类型接收者与指针类型接收者

在很多Go项目中我们可以看到很多指针类型的代码,对于不经常用指针的新手很不友好。我们来认真研究研究,函数接受指针类型的参数还可以很快的理解,方法接受者如果传入一个指针类型的参数一时半会可能弄不明白其中的逻辑。

方法的接收者除了有值类型还有指针类型,先要明白一点,在方法中接受的如果不是指针类型,要修改传入的内容是不生效的,值类型会复制一份修改,指针类型不会复制会直接修改传入的值,举一个例子:

package main

import "fmt"



type Test int



func (test *Test) Pointer(){  

    *test = 10086

}



func (test Test) Value()(){   

    test = 10000

}



func (test Test) Show()(){   

    fmt.Println(test)

}



func main(){

    TestValue := Test(1)  

    TestValue.Show() //TestValue调用了类型名为Test的接收者的方法 ,接受者值为1 类型为int,输出1

    TestValue.Value() //值传递修改值

    TestValue.Show() //输出1,没有改成10000

    TestValue.Pointer()  //指针传递修改了

    TestValue.Show() //修改为10086 



}

方法和接收者使用指针传递会非常常见,要提高警惕哦,方法的调用者在上面的例子为TestValue,是一个值类型的变量。方法的调用者也可以是一个指针类型的变量,这里可以简单说下Go语言器帮助开发者自动做的一些优化:

如果使用一个值类型变量调用指针类型接收者的方法,不用传递指针参数,编译器会自动让值类型变量的指针调用接收者方法。

同样的原理,如果使用一个指针类型变量调用值类型接收者的方法,编译器会自动使用指针所指向的值去调用接收者的方法。

七 .Go语言结构体&多态

结构体

Go 语言的结构体可以包含多种数据类型,Go语言的数组只能包含一种数据类型,可以使用type和struct定义一个结构体:

type test struct {   //声明一个结构体,名字为test

    value int        //声明其中一个字段,名字为value ,数据类型为int

    name string      //声明其中一个字段,名字为name, 数据类型为string

}

结构体内的字段是唯一的,不能重复,也可以把类型相同的字段放在一行:

type letsgo struct{

    data1,data2,data3 int

}

也可以定义一个空的结构体,没有内容:

type test strut{   

}

结构体初始化

定义了一个结构体不使用也不会报错,因为结构体只是规定了一组数据的内存布局,只有实例化才会分配内存。

type test strut{

    myvalue int

    mydata string

}

var mytest test   //实例化一个结构体,mytest为结构体实例,test为结构体类型,这个东西将会非常常见

一种基础的实例化方式:

type letsgo struct{

    data1,data2,data3 int

}

var p letsgo

p.data1 = 1

p.data2 = 2

p.data3 = 3

fmt.Println(p)  //{1,2,3},如果没有给data2,data3赋值,那么输出的值就是{1,0,0}

可以在实例化的时候给其中字段赋值,必须按照顺序来:

type letsgo struct{

    data1,data2,data3 int

}

var p =letsgo{1,30,3}

fmt.Println(p)  //{1,30,3}

如果不按照顺序呢:

type letsgo struct{

    data1,data2,data3 int

}

var p =letsgo{data3:3,data2:30,data1:1}

fmt.Println(p)  //{1,30,3},还是按照结构体的顺序输出

嵌套结构体

结构体还可以套娃,结构体里面还可以弄一个结构体:

type father struct{

    name string

    other son

}



type son struct{

    name string

}

嵌套结构体也可以用上面的方法一样初始化:

package main

import "fmt"



func main() {    

    type son struct{

        name string

    }    



    type father struct{

        name string

        other son  //son 的定义一定要在上面,不然会报错。

    }



    p:=father{

        name:"张三",

        other:son{

            name :"张三的儿子",

        },

    }

    fmt.Println(p)  //{张三 {张三的儿子}}

    fmt.Println(p.other.name)  //访问结构体使用.  输出:张三的儿子

}

访问结构体用.,上面的例子已经说明白了。第二层也是用.,

把结构体作为函数参数

填补一下说函数的时候挖的坑,直接来一个例子:

package main

import "fmt"

type father struct{

    name string

}

func printName ( fa father ){

    fmt.Println(fa.name)  

}

func main() {

    p:=father{

        name:"张三",

    }

    printName(p)  //输出张三

}

再试下用结构体指针作为函数参数:

package main

import "fmt"

type father struct{

    name string

}

func printName ( fa *father ){

    fmt.Println(fa.name)  //读取传递过来的fa的地址保存的结构体,输出结构体其中的字段

}

func main() {

    p:=father{

        name:"张三",

    }

    printName(&p)  //传递结构p的地址,输出张三

}

这里讲了很多代码例子,因为发现结构体特性用代码说出来比较直接。

Go语言接口声明

Go语言的接口与上篇讲的结构体有点类似,同样使用type 关键字开始定义,接口的关键字是: interface,

是一个高度抽象的类型,可以使用它实现面对对象的特性,接口主要是定义一些方法名称,输入参数和返回类型的集合。通常用由结构体(sctuct)来声明方法并实现它们,定义两个接口的例子:

type Tester interface {

    Say()   //Say()是个方法,没有返回值

}



type Helloer interface {

    Say() string /Say()是个方法,返回类型为string

}

接口的名字一般用er结尾,通俗规定,可以快速识别它是一个接口类型,只用接口里面只有方法的声明没有实现,并且不用func关键字。

接口实现及多态

任何类型只要实现的接口中定义的方法集合,那么这个接口就是属于这个类型。这些类型通常使用结构体(struct),又来举一个例子:

package main

import "fmt"

type Person interface {  //定义一个接口

    Run()   // 跑

    Walk()  // 走

}



type Man struct {  //定义一个Man的接口体,也是一个类型,用来实现接口,这个类也叫接口的实现类

    name string

}



func (d Man) Run(){  //接口里定义的方法,下面用实现类来调用它

    fmt.Println("在跑的人叫",d.name)

}



func (d Man) Walk(){ 

    fmt.Println("在走的人叫",d.name)

}



func main() {

    var p Person  //定义一个接口类型的变量

    p = Man{"哈哈"}  //给接口类型的变量赋值,指定接口的实现类

    p.Run()  //调用接口实现类的方法 ,接口实现

    p.Walk()

}

这个例子就是展现了面对对象种的多态,多态一般理解就是一种事务的多种形态,在上面的代码可以看出不管定义了什么类型的接口实现类都可以让这个实现这个Person的接口,让这个实现类有Run和Walk的方法(功能),例如再定义一个Dog实现类:

package main

import "fmt"

type Person interface {  //定义一个接口

    Run()   // 跑

    Walk()  // 走

}



func (d Man) Run(){  //接口里定义的方法

    fmt.Printf("在跑的狗%d岁了\n",d.age)

}



func (d Man) Walk(){

    fmt.Printf("在走的狗%d岁了\n",d.age)

}



type Dog struct {  //定义一个狗的结构体,结构体也是一个类型,下面用来实现接口

    age int

}



func main() {

    var p Person

    p = Man{12}

    p.Run()

    p.Walk()

}

甚至可以扩大到任何定义的类型可以实现任何接口,这样是不是很好理解了。不同类型对接口里定义的方法的调用就是Go语言实现的类似面对对象这种编程思想中的多态。

八.Go语言错误处理

err != nil

Go语言的错误处理一直被人诟病,也没有try,catch的,跟其他语言相比真的很反人类,

Go语言处理错误时通常都是将返回的错误与 nil 比较。nil 值表示了没有错误发生,而非 nil 值表示出现了错误。所以说在经常会出现下面几行代码:

if err != nil {

        fmt.Println("error:",err)

        return

    }

如果自己要实现一个函数,想要把错误信息传递到调用者的话,就要在函数的返回值那里定义一个error类型,这个error 类型实际上是用了 Error() 方法的 error接口,实现该接口的类型都可以当作一个错误类型:

type error interface {  //error接口

    Error() string   //Error方法

}

在很多第三方包方法或者内置包方法都会使用到这个error接口,来一个实际例子:

package main

import (

    "fmt"

    "strconv"

)



func main() {

    resp, err := strconv.Atoi("hello")  //strconv数据类型转换的内置包,Atoi方法是把string转换为int,现在里面是hello无法转为int,所以下面会报错

    if err != nil {

            fmt.Println(err) //输出错误:strconv.Atoi: parsing "hello": invalid syntax

    }else{

            fmt.Println(resp)

    }

}

我们自己写代码也可以使用这个方法,把错误信息传递到调用者,让调用者判断是否有误,如下面所示:

package main

import (

    "fmt"

    "errors"

)



func ErrorMake(a,b int) (int,error){

    if b==0 {

        return 0,errors.New("b不能为零")

    }else {

        return a / b,nil

    }

}

func main() {

    var a,b=1,0

    resp,err := ErrorMake(a,b)

    if err!=nil {

        fmt.Println(err)  //b不能为零

    }else {

        fmt.Println(resp)

    }

}

panic和defer

panic 单词的意思就是恐慌,在Go语言的作用就是主动抛出错误,我经常使用Python的时候使用raise主动抛出错误,然后给try except接受这个错误,进入错误处理的程序,可以看看Pyhon的代码:

try:

    a = input("输入一个数:")

    if(not a.isdigit()):

        raise ValueError("a 必须是数字")

except ValueError as e:

    print("引发异常:",repr(e))  //输入除数字之外的东西就会主动到这里来输出错误信息

panic呢?主动抛出错误的例子:

package main

import "fmt"



func showpanic(data int){

    if data == 0{

        panic("传入参数为零。不支持哦")

    }else{ 

        fmt.Println(data)

    }

}





func main() {

    showpanic(1)  //输出1

    showpanic(0)  //主动抛出错误 传入参数为零。不支持哦

}

panic 是一个非常严重的错误,会导致程序终止运行,程序终止运行后一般会需要配合释放一些资源,比如关闭文件,关闭网络连接,退出登录等等,在Go语言经常会看到使用panic主动抛出错误可以配合defer使用来实现错误主动抛出,主动接受,defer是什么呢?

defer 关键字用于修饰一个函数或者方法,在该函数或者方法在返回前执行,这里有个经常会考的点,多个defer,倒序执行,堆栈一样先进后出,先简单看看defer例子:

package main

import "fmt"



func testdefer(data string)  (return_data string) {

    defer fmt.Println("666")

    return data+"10086"  

}



func main() {

    fmt.Println(testdefer("call "))

}

依次会输出:

666

call 10086

defer 在抛出错误之前,函数或方法return之前都会执行:

package main

import "fmt"



func login(password string)  (return_data string) {

    defer fmt.Println("666")

    return data+"10086"  

}



func main() {

    fmt.Println(testdefer("call "))

}

panic 和defer一起使用:

package main

import "fmt"



func login(password string)  (return_data string) {

    defer fmt.Println("记录")



    if (password=="123456"){

        return "登录成功"

    }else{

        panic ("致命错误,GG")

    }

}

func main() {

    fmt.Println(login("123456"))

    fmt.Println(login("654321"))

}

输出:

记录

登录成功

记录

panic: 致命错误,GG

....

这个panic 关键字一般是是不推荐使用的,因为使用它代表了程序的终止运行,一般使用error,error抛出错误是不会终止程序的。

九.Go语言模块管理

GO环境变量

在Go语言中需要引入很多包,跟其他语言一样。来巩固一下Go语言包管理的知识,首先要说一下安装的环境变量。这里只说Win10的安装,在Win10上安装Go语言过后,安装完成后go会自动设置两个环境变量GOROOT和GOPATH,如果删除了第二次安装就需要配置环境变量。

有三个环境变量,GOROOT、GOPATH 和 GOBIN。

GOROOT:Go语言安装的目录

GOPATH:自己定义的工作目录,工作区目录,可以有多个

GOBIN:GO 程序生成的可执行文件的路径

初次安装完成后可以使用go version看下前两个环境变量安装好了没:

接下来使用go env查看相关的环境变量:

GOAPATH

GOBIN可以暂时不用管,它现在并不重要,需要注意的是GOPATH,定义了一个工作区,设置好这个工作区后,我们一般会在这个工作区下面再新建三个文件夹,分别为:src,pkg,bin。

src:存放go源码文件,自己写的文件;

pkg:用于存放编译后生成的归档文件;

bin:用来存放编译后的可执行文件;

GO Modules

但是使用GOPATH这个方法进行项目开发有些缺点:无法保证多个项目在一个工作区下面有的不同版本号的需求。没有版本控制的概念,不能指定当前项目的第三方包版本号,所以也就有了在Go1.13+的Go Module工作模式。

使用Go Modules 方式创建一个项目不建议创建在GoPATH/src下面,需要与GOPATH方式分开

使用Go mod -w GO111MODULE=on打开Go Modules模式支持。auto 模式下只要项目包含了go.mod文件就会启用Go Modeules。

打开这个模式之后GOPATH 是没有用的,go 会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod 下载依赖。打开这个模式过后,再新建一个项目后需要使用go mod init 模块名在项目根目录下生成go.mod文件,里面会记录依赖包的名字版本信息,不用管GOPATH了。

除了设置GO111MODULE还需要设置一下GOPROXY,因为某些原因在国内很多包都下载不下来,跟其他语言一样,这时候就需要切换下载GO包的源地址(下载源),使用go env -w GOPROXY=https://goproxy.cn,direct,然后使用go mod download *****就会从国内镜像站下载到本地。

Go mod

可以看下go mod 其他常用指令:

    1. go mod tidy 整理项目依赖包删掉多余的下载没有的
    1. go mod vendor 生成vendor目录,导入依赖,会把依赖包转移到vendor文件夹下,使用Git时候记得在 .gitignore文件中忽略这个文件夹
    1. go clean -modcache 清理mod 依赖
    1. go mod verify 验证依赖是否正确
    1. go get github.com/项目地址/包名@version 更新到某个版本

还可以使用 go mod help查看go mod命令帮助。

十.Go语言包导入

内置包导入

Go语言项目中可以看到要导入很多包,使用import 关键字。例如

import "fmt"

或者

import (

    "fmt"

)

除了fmt包,Go语言有很多非常常见的标准库(内置包)介绍一下(内置包可以不指定包的路径):

  1. io:文件操作,进行文件读写,通常用来对文件流,字节流进行处理
  2. strconv:数据类型转换,这个包名字可以看作是string convert两个单词的缩写。
  3. error:对错误操作的包
  4. math:包含了一些数学常量和一些数据数学函数
  5. os:提供一些系统相关的函数,例如文件权限操作,文件创建删除,获取主机名,获取进程ID等等
  6. time:提供时间显示,转换,计算的函数

本地自定义包导入

那么那些自定的包们如何使用导入呢?有两种方法:使用相对路径和绝对路径。例如:

使用相对路径: import "./model" ,但是不推荐这种方法。

使用绝对路径import "mypkg/model",会加载GOPATH/src/mypkg/model模块。

上篇文章说过打开Go Modules模式情况下这种GOPATH/src/这段路径就会失效,所以导入相同目录下包使用类似绝对路径的方法,绝对路径的根目录就会从项目的文件名开始找包。举个例子:

打开Go Modules 模式,新建一个文件夹GoStudy文件夹做为一个项目文件夹,新建一个main.go作为项目的入口文件,在项目文件夹根目录新建一个文件夹mypkg,在文件夹mypkg下面新建一个mypkg.go的文件。作为自定义本地包,输入以下内容作为测试:

package mypkg



import "fmt"



func Show(){  //记得函数名字首字母大写,要在其他包调用它

    fmt.Println("my name is mypkg")  

}

此时的目录文件结构为:

接下来在main.go输入下面内容:

package main

import (

    "fmt"

    "GoStudy/mypkg"

)

func main() {

    mypkg.Show()

    fmt.Println("hello")

}

在这个项目文件夹输入go mod init GoStudy 在项目根目录下生成go.mod文件,会自动生成以下内容:

module GoStudy



go 1.16

然后就可以运行测试main.go:

可以看到自己的自定义的包中的Show函数已经被调用了。

远程包导入

假如我们要使用Go语言大名鼎鼎的Gin框架,这就是一个远程的包。

我们直接使用以下命令:

go get github.com/gin-gonic/gin

如上图所示,可能会下载很多与其相关其他的包,然后在入口文件main.go的import 那里加上这个包GitHub链接,然后测试一下最简单的Gin框架吧

package main

import (

    "fmt"

    "GoStudy/mypkg"

    "github.com/gin-gonic/gin"

)

func main() {

    mypkg.Show()

    fmt.Println("hello")

    r := gin.Default()

    r.GET("/", func(c *gin.Context) {

            c.String(200, "Hello,土味挖掘机")

    })

    r.Run() // listen and serve on 0.0.0.0:8080

}

命令行输入go run main.go ,浏览器打开:127.0.0.1:8080

import 其他用法

import 方式

还需要说说 import 有三种导入方式。实现三种功能。

import _ "fmt"

执行fmt里面的init()方法,无法使用fmt里面的方法。

import ft "fmt"

给fmt包起一个别名:ft,在包名特别长需要使用里面的方法的时候特别有用,可以使用ft.Println()调试输出。

import import . "fmt"

将fmt包所有的方法导入到当前包种,可以直接直接使用fmt里面所有的方法,例如直接使用Println("hello"),但是这种方法不推荐使用,会有函数重名报错的风险。

init 函数

在包被调用时会事先执行。init函数的格式和普通函数一样,功能主要做一些初始化处理,例如收集一些环境变量,加载配置文件等等,例如我们写一个调用的打印输出的init的函数的例子:

package mypkg



import "fmt" 



func init(){

    fmt.Println("mypkg包,环境变量已初始化,配置文件已加载!")

}

十一.Go语言interface万能类型及其类型断言

Go语言接口也叫interface,interface里面主要是定义一些方法名称,前面第二篇讲过,这个高度抽象的类型不理解它很容易忘,而且有一些高级用法需要认真研究才能懂,通常用由结构体(sctuct)来声明方法并实现它们,今天再仔细讲讲它的高级用法

万能类型

Go语言的基础数据类型都实现了interface{},也就是说interface{}这个空接口都能以引用任意的数据类型,例如int,string,float,struct,怎么引用呢?那就是在函数的形参可以使用空接口,实参可以是任意的数据类型,我们来举一个例子:

package main

import "fmt"

func showAny(mytest interface{}){

    fmt.Println(mytest)   //直接打印输出传入的interface

}



func main() {

    type user struct{

        name string

    }

    showAny("string test")

    showAny(123)

    showAny(123.456)

    worker :=user{"土味挖掘机"}

    showAny(worker)

}

go run main.go看看运行结果:

从运行结果可以看的出来,空接口的确可以引用任意的数据类型。这不就是面对对象三大特征特征:抽象、继承、多态中多态的概念吗?

类型断言

断言通俗来说就是判断变量的类型,在Gin框架的文档中也有类似的断言的过程:

binding.Validator.Engine()是第三方包实现的接口,里面的*validator.Validate就是指针传递的验证内容。

现在就实现一个简单的类型断言吧,改一下上面的代码例子,改一下showAny函数的内容,判断接口引用的内容:

package main

import "fmt"



type user struct{

    name string

}

func showAny(mytest interface{}){

    _,ok:=mytest.(string)

    if ok{

        fmt.Printf("%s是:字符串\n",mytest)

    }

    _,ok=mytest.(int)

    if ok{

        fmt.Printf("%d是:整数\n",mytest)

    }

    _,ok=mytest.(float64)

    if ok{

        fmt.Printf("%g是:浮点数\n",mytest)

    }

    _,ok=mytest.(user)

    if ok{

        fmt.Printf("%s是:user结构体\n",mytest)

    }



}



func main() {

    showAny("string test")

    showAny(123)

    showAny(123.456)

    worker :=user{"土味挖掘机"}

    showAny(worker)

}

执行结果:

所以使用mytest.(type)来判断mytest接口的内容,如果返回的第二个参数不为False,那么就是断言成功,判断类型正确,为False就继续执行下面的判断。

总结

之前讲过interface 的普通的用法,可以用任何定义的类型实现任何接口,今天说了一下空接口可以引用任何类型,也可以做类型断言,体现了接口灵活强大,体现了Go语言多态的特征。

十二.Go语言结构体标签与反射

Go语言结构体标签,就是在结构体中的一段字符串,有点像PHP中的注解,有严格的格式要求,通常用于反射包里的方法来访问它,标签用来声明结构体中字段的属性。

我们先举一个例子看结构体标签在json转换中的运用:

package main



import (

    "encoding/json"

    "fmt"

)



type User struct {

    Name   string `json:"username"`

    Age    int    `json:"userage"`

    Salary int    `json:"usersalary"`

}



func main() {

    myself := User{"土味挖掘机", 18, 2000}

    jsondata, err := json.Marshal(myself)

    if err != nil {

        fmt.Println("格式错误")

    } else {

        fmt.Printf("User结构体转json:%s\n", jsondata)

    }

}

输出结果为:

User结构体转json:{"username":"土味挖掘机","userage":18,"usersalary":2000}

可以看出"encoding/json"包的json.Marshal()方法作用就是把结构体转换为json,它读取了User结构体里面的标签,json键值对的键为定义的标签名,结构体的名字起了辅助作用,同时定义了字段数据类型。json.Unmarshal()可以把json字符串转换为结构体,在很多第三方包方法都会读取结构体标签。 注意在标签中的外层符号是键盘Tab键上方的键,不是单引号。

在Gin的文档中可以看到它的身影:

上面的例子中form:"user"表示是form表单的在参数名为user,binding:"required"表示这个参数是必须的。

可以看出Go语言结构体标签中结构体一个字段可以使用多个标签,

结构体标签需要用反射机制来读取。

Go语言反射

反射通俗来讲就是获取已知变量的类型或者获取已知变量的值,使用realect包的两个方法获取变量的类型和值reflect.ValueOf()和reflect.TypeOf(),我们下面用代码可以展示一下

package main



import (

    "fmt"

    "reflect"

)



type User struct {

    Name string `data:"test" test:"haha"`

    Age  string `json:"test"`

}



func main() {

    user := User{"土味", "测试"}

    user_value := reflect.ValueOf(user) //获取变量的值



    user_type := reflect.TypeOf(user) //获取类型

    fmt.Println(user_value, user_type)



    //反射的用法,获取结构体标签的值

    name_data := user_type.Field(0)            //user第一个字段

    name_tag_data := name_data.Tag.Get("data") //获取结构体第一个字段的Tag(标签)里面键为data的值

    fmt.Println("name_data:", name_tag_data)

}

反射

以上代码实现了通过Go语言的反射获取结构标签的值,通过这个例子是不是想到了前面文章实现的ORM模型方法和数据表结构迁移。我们再简单扩展一下关于反射知识的内容,反射本质是在代码运行的时候尝试获取获取对象的类型信息和内存结构,在Go语言中一般定义变量的时候有一个静态类型,比如,int,string,floast,但是这并不是底层的数据类型,string的底层类型是一个struct,Go语言的数据类型分为静态类型和底层类型,例如:

type MyInt int



var i int

var j MyInt

i和j的底层类型都是int,i的静态类型为int,j的静态类型为MyInt,所以他们不能相等,他们的静态类型不同。

所以一个Go语言变量的有值(value)还有它的静态类型(static type)或者底层类型(concrete type),底层类型也可以叫具体类型,静态类型在编译阶段就已经确定了。反射的具体实现是这个变量在赋值和相应类型的时候会向接口(interface)保存自己的类型信息,type和value用接口(interface)进行转换,上面的代码使用了User的结构体实现了这个转换的接口。

十三.Go语言协程

在Go语言中可以说最核心的是协程(goroutine)了,Go语言原生的支持协程让它的效率非常高,说协程之前先回顾一下线程和进程。

进程和线程

在操作系统中,进程是一个非常重要的概念,我现在正在用的是linux,当启动一个软件的时候就会为这个软件启动一个进程,可以使用linux命令top查看计算机启动进程的状态:

第一列就是进程号,command那一列就是软件的名字,一个软件启动通常会同时执行很多不同的任务,比如QQ音乐最少有一个任务显示界面,有一个任务播放音频,还有任务缓存下一首歌等等,所以可以使用ps -T -p 进程号可以查看进程下启动的线程:

可以看到一个进程下面有很多线程,有一个主线程,PID为24167,其他都是子线程,在折腾linux的时候有时候有些软件卡了,或想彻底卸载某个软件通常会使用kill -9 进程号给这个进程发送强制关闭的信号彻底关闭软件,一个进程下面的线程是资源分配的最小单位,同一进程下不同线程间数据共享很容易开销很小,所以运行软件不会启动很多进程而会启动一两个进程,在这些进程里面再启动一些线程,线程消耗的资源会比进程小很多。

并行和并发

现在的计算机核心都很多了,在启动很多软件的时候随时都会有并行和并发的现象发生。并发并行操作表面上都可以看作在同一时间做很多事情。但是并发其实并不算同一时间做同一件事情,如果把这个时间压缩到很小的情况来他们还是在互相等待一一切换交替执行任务的,只是计算机在不同任务中切换得太快了,让人感觉不到等待,经常听说的一个词就IO多路复用就是并发操作。

并发这个词相关的就是并行,并行就是在同一时间点同时互不干扰地执行任务,注意是时间点,很小的时间概念,并发和并行我觉得差别很大的就是并发会互相影响,自己通常在编写程序的时候都是串行执行,按照编写的代码从头执行到尾,遇到多线程操作的代码的时候控制不知道要开启多少个线程操作,因为线程在进程下执行任务会互相争抢计算资源,自己不知道自己写的那些线程执行相同任务哪个任务会最完成,一切交给天意。

协程

进程和线程都是计算机系统来控制切换的,用户写代码不能控制他们的切换,但是协程是用户控制的,Go语言的并发是己调度的,自己决定同时执行多少个goroutine,什么时候执行哪一个,所以在系统命令中是没有查看协程这个命令的,可以把协程看作一个微型的线程,线程会争抢进程向系统申请的资源,例如线程大概分成5种状态,创建,就绪,执行,阻塞,死亡,与其他进程互相竞争或者互相等待经常会改变线程的状态,做一些无用功(对执行的任务来说)。在而协程中就是可以看作是协作式的,协程切换由Go语言自己特有的模型逻辑控制,在这个切换控制逻辑中执行协程任务时默认启用计算机全部的核心,在每个核心在启动分配到若干个线程,再在这些若干个线程中启动很多个协程(goroutine)(其中有个调度的核心点:这些若干个进程每个进程还要维护一个协程任务队列,有的线程里的协程任务做完了会把其他线程的协程任务拿过来执行,保证所有人都有活干,充分榨干计算机性能!)详情可以百度搜索一下GMP 模型(goroutine,Machine,Processor)还有很多调度细节。

简单代码实现

说了那么多怎么实现一个携程呢?Go语言协程代码非常简单,使用go关键字就可以做到,不需要导入包。不像PHP实现携程还经常使用swoole,Python还需要导入asyncio包来实现异步协程,看看Go语言实现协程真实代码:

package main



import (

    "fmt"

    "time"

)



func main() {

    go fmt.Println("我是土味挖掘机创建的协程任务!")

    fmt.Println("我是 主进程")

    time.Sleep(time.Second)

}

运行结果:

非抢占式多任务处理,由协程主动交出控制权

轻量型“线程”

编译器/解释器/虚拟机层面的多任务

多个协程可以在一个或多个线程上运行

任何函数只需要加上go 就可以发送给调度器执行,不需要在定义时区分是否是异步函数

调度器在合适的点进行切换

十四.Go语言通道(channel)

它的作用就是可以让多个协程(goruntine)之间安全的传值。可以使用make关键字创建一个通道:

    test:=make(chan int)

test通道只有两种方法,使用<-接受内容和发送内容,<-在通道左边为接受内容,<-在通道右边为发送内容,写一个例子展示一下通道的接受发送吧:

package main



import "fmt"



func main() {

    ch := make(chan string)



    go func() {



        ch <- "我是协程发送的值"

    }()



    fmt.Println("我是主线程")



    data := <-ch

    fmt.Println(data)

    fmt.Println("程序执行完毕")

}

在这个例子中的通道是一个有无缓冲的通道,发送和接受是同时发生的,通道中没有容量储存任何数据,没有规定容量,不用像上篇例子写协程中在主线程中等待1秒让协程任务先执行输出,在这个例子中如果协程任务不向通道发送内容,那么主程序接受内容的代码永远会等待这个通道发送数据,直到接收到内容才会运行主程序下面的代码。

缓冲通道

与无缓冲通道相关的就是有缓冲通道,定义缓冲通道需要规定通道的容量,内部的元素先进先出,让我想起了堆栈,堆是先进先出,栈是先进后出。有缓冲的通道也会有两种任务阻塞的状态,一种是通道中没有接受的值,接受操作就会阻塞,另一种就是通道塞满了,发送操作就会阻塞。经典使用场景可以使用for 循环读取或接收通道内容,直到通道关闭:

for x := range ch{

    fmt.Println(x)

}

这样只要通道关闭了就停止循环了,不会报错。写一个简单的功能来实现这个功能:

package main



import (

    "fmt"

    "strconv"

)



func main() {

    ch := make(chan string)



    go func() {

        for i := 0; i <= 10; i++ {

            ch <- "发送" + strconv.Itoa(i)

        }

        close(ch)

    }()



    fmt.Println("我是主线程")



    for x := range ch {

        fmt.Println("接受:" + x)

    }

    fmt.Println("程序执行完毕")

}

这里补充一下关闭通道的知识,使用 close()方法关闭通道。简单直白,跟使用go启动一个协程一样那么简单,

总结

在Go语言通道还有很多用法,现在是初学阶段就了解一下它是干嘛的就行了,Go语言推荐不要通过共享内存来通信,而应该通过通信来共享内存”,在传统语言多线程想读取修改资源的时候通过对内存进行加锁和解锁来进行通信保证数据安全,而在Go语言通道这里相当于用通道开辟一个共享内存来声明对某个资源控制权,使用通道来进行消息传递,条理更加清晰可靠。