zmqNut

明知会散落, 仍不惧盛开

0%

GO和C++语法上的主要差别

1576218212309

概要

GO语言是由Google在2012年正式发布的编程语言,在语法和特性上,GO和C++有很多相似之处,但也有很多不同点。本文就GO语言在语法上和C++的一些主要的差异点进行简单的描述。

Go 是一门简单、紧凑且通用的语言。而 C++ 是一门快速且复杂的通用编程语言。Go 和 C++ 都是静态类型语言且都有强大的社区。C++ 广泛用于各种应用,而 Go 主要用于 Web 后端。

Go 是专为现代多核处理器而设计的。Go 语言支持并发编程,这意味着它可以使用不同的线程同时运行多个处理过程,而不是同一时刻只运行一个任务。它还具有延迟垃圾回收功能,可以进行内存管理以防止内存泄漏。

  • 编译

都可以静态编译,直接编译成二进制文件。目前,许多语言(比如Java,C#)都是基于运行时,能静态编译语言的不多,Go算一个。同时,都可以跨平台。

  • 内存管理

在C++中,需要自己使用new和delete管理内存,尽管C++已经有了智能指针。但是有一些坑,不是那么好用。

Go虽是静态语言,但是自己管理内存,减轻了程序员的心智负担。这一点,非常重要。因为C++程序的崩溃,大多数时候都是内存问题,比如内存泄漏,非常难以解决。

  • 标准库

一门语言开发效率高不高,它的标准库起着关键的作用。Go的标准库十分强大,常用的库几乎都有,开箱即用,十分顺手。C++的标准库里面的工具并不多,很多时候只能下载第三方库使用。不过,boost是一个著名的C++库,包含了大量的常用库。

  • 性能

Go 相当快。其编译、静态类型以及高效的垃圾回收使其变得极其快。Go 也擅长内存管理;它有指针但没有引用。在速度方面,和 C++ 非常接近(以及 C 语言)。所有的时间都花在编码和编译上。因为 C++ 难编码,中级语言,更接近于机器码:当编译时,他更适合于机器码的嵌套。

C++ 也缺少那些让编码更容易但会给生成的程序增加阻力的特性。当提及运行时,C++ 轻量,精简且快速。

Go 配备了所有的能让你在编码过程中使生活更容易的零件和部件,因此,在运行时会慢一些。其中最大的一块是他的虽然很好但很慢的垃圾收集器。

  • 并发编程

并发编程是Go语言的一大特色,可以轻松实现高并发,在语言层面就支持。C++只能调动系统API开启线程实现并发,语言层面并不支持。

  • 开发和调试难度

C++的内存问题,很难排查和调试,比如内存越界,从程序崩溃的栈信息上可能就看不出来是什么问题,定位问题很难。Go进程中会启动Go自己的运行时,抛出的异常会指明错误信息,很容易能排查出问题。

代码格式

GO语言要求代码的花括号{}必须采用统一的风格,即左花括号{必须放在行尾而不能另起一行。

GO不需要在语句的最后增加分号;以表示语句结束。

1
2
3
if a == b {      //花括号{必须放在上一行的最后面
a += 1 //语句后面不需要增加;号
}

常量和变量

常量&变量声明与赋值

GO在进行变量声明时,需要在变量前加var关键字;但GO中的变量可以不定义直接赋值,此时GO会自己根据:=右侧的表达式计算出变量的类型。

同时,GO还支持多重赋值。

1
2
3
4
var i int
i = 10
str := "Hello World!"
i1, str1 := 10, "Hello World!" //多重赋值

在定义常量常量时,和C++一样只需要在前面增加const关键字,并同时给常量赋以编译期可确定的值。

1
2
3
4
5
const i int = 2 << 5  //=号右侧的表达式必须可以在编译期确定
const {
i1 int = 15
str = "Hello World!"
}

GO语言中有三个预定义的常量:truefalseitoatrue/false很好理解,itoa是一个很特殊和常量,它在一个const作用范围中,第一次出现时为0,后续每次出现都比上次的值大1,在GO中通常用于“枚举”(GO不支持枚举)定义。

1
2
3
4
5
6
7
const {
Monday = itoa //Monday = 0
Tuesday = itoa //Tuesday = 1
Wednesday = itoa //Wedesday = 2
}

const x = itoa //下一个const作用域,x = 0

可见性

GO语言中没有private/protected/public/friend关键字,可见性是通过成员的首字母是否大写决定的,并且仅支持包间的可见性定义,不支持类的可见性。

包中所有首字母大写开头的成员(对象、函数),表示包间可见,可以被其它包中的代码访问。但对于首字母小写开头的成员,则仅能被本包中的成员所访问(这相当于包内的所有成员间都加了friend声明)

条件、选择和循环

条件语句

GO语言的条件语句中,条件不需要加括号(),并且支持初始化语句:

1
2
3
if a := b; a == 1 {
//......
}

选择语句

GO的选择语句中,不支持break关键字,因为GO的选择语句中,遇到符合条件的分支会自动跳出;如果想要继续向下执行,需要在case分支中增加fallthrough关键字。

1
2
3
4
5
6
7
8
switch i {
case 0, 1:
fmt.Printf("0") //i等于0或1时,执行完此打印语句后会退出
case 2:
fallthrough //i等于2时,会继续向下执行
default:
fmt.Printf("")
}

在GO的switch后面可以不带表达式,此时在case中需要增加条件:

1
2
3
4
switch {
case i == 10:
return
}

循环语句

GO不支持whiledo...while形式的循环,仅支持for循环。在GO的for循环中可以通过多重赋值方式为多个变量赋值。对于多重循环,可以在最外层定义循环标签,并在内层循环中通过break关键字直接指定标签名,直接跳出外层循环。

1
2
3
4
5
6
7
8
9
10
OutLoop:    //定义循环标签
//死循环
for {
//注意,在for循环中只能使用多重赋值
for a, b := 1, 5; a < b; a, b = a + 2, b + 1 {
if a == 2 {
break OutLoop //直接跳出循环标签所定义的外层循环
}
}
}

函数

在Golang语言中不支持函数的重载

Golang不允许同一个文件里函数名不同

在Go中,函数本身也是一种数据类型,可以赋值给一个变量,该变量就是函数类型的变量,通过该变量实现对函数类型的调用

函数定义

Go 语言函数定义格式如下:

1
2
3
func function_name( [parameter list] ) [return_types] {
函数体
}
  • func:函数由 func 开始声明
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。可以有返回值,也可以没有
  • 函数体:函数定义的代码集合。

return语句

  1. Go 函数可以返回多个值,和python相似
  2. 如果返回多个值时,在接受时,希望忽略某个返回值,则使用_表示忽略
  3. 如果返回值只有一个,返回值类型列表可以不写()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func swap(x, y string) (string, string) { //两个返回值,此时一定要用()括起来
return y, x
}

func main() {
a, b := swap("Google", "Runoob")
fmt.Println(a, b)
}

//可以忽略返回值
/*
func swap(x, y string) (string, string) { //两个返回值,此时一定要用()括起来
return y, x
}
func main() {
_, b := swap("Google", "Runoob") //注意看细节
fmt.Println(a, b)
}
*/

普通函数

在定义GO函数时,需要在前面增加func关键字,func后面紧随函数名,然后定义参数列表;GO语言是支持多返回值的,因此最后需要定义返回值列表。下面是两个int类型的除法函数示例:

1
2
3
4
5
6
7
8
9
10
11
//参数或返回值列表中,多个相同类型的可以通过逗号合并声明
func Divide(a, b int) (ret int, err error) {
//返回值会自动定义,在函数体内可直接赋值
err = errors.New("Cannot divide zero!")
if b == 0 {
return //在不指定时返回值时,将返回自动定义的对象(ret, err)
}

ret = a / b
return ret, nil
}

可变参数

GO和C++一样也支持可变参数,同样需要将可变参数放在所有参数的最后。GO的可变参数可以指定类型,也可以通过interface{}支持任意类型。

1
2
3
4
5
6
7
8
func varFunc1(argv ...int){
} //只可以接受int类型的0到多个参数

func sum(n1 int, args...int) sum int{
} //可以接受int类型的1到多个参数

func varFunc1(argv ...interface{}){
} //可接受任意类型的可变参数

说明:

  1. args是slice切片,通过args[index]可以访问到各个值

  2. 切片是一个动态的数组

  3. 如果一个函数的形参列表中有可变参数,那么把可变参数放在最后

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

import "fmt"

func sum(n1 int, args ...int) int {
sum := n1
//遍历args
for i := 0; i < len(args); i++ {
sum += args[i]
}
return sum
}

func main() {
a := sum(10, 234, 3, 4, 34543, 5, 3, 45, 3, 5, 56)
fmt.Println("a = ", a)
}

匿名函数

Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

GO语言支持匿名函数,这一点是C++所不具备的。

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
package main

import "fmt"

func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}

func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()

/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())

/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}

接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。把所有方法全部实现了,叫做实现了接口

接口类型可以定义一组方法,但是这些不需要实现,而且Interface不能包含任何的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
/* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}

案例:

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
37
38
package main

import (
"fmt"
)

type Phone interface {
call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}

func main() {
var phone Phone

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()

}
/*
I am Nokia, I can call you!
I am iPhone, I can call you!
*/

错误处理

GO语言通过error类型以及deferpanicrecover三个关键字,相对C++提供了更方便的错误处理。

  • error

GO的error实际是一个定义了func Error() string函数的接口。所有的error类型都必须实现Error()函数。

1
2
3
type error interface {
Error() string
}

因为GO支持多个返回值,因此可以通过在返回值中增加error类型,更方便开发人员判断调用是否成功,比如前面定义的Devide函数的调用可以按如下方式写:

1
2
3
4
ret, err := Devide(a, b)
if err != nil {
fmt.Println(err.Error())
}

而在C++中要实现同样的功能,则需要函数抛出一个DevideZero异常,并在调用处捕获异常再处理;或是定义一个返回值并通过输出参数返回结果。而通过error进行处理则简单很多。

  • defer

使用C++的开发人员对C++中的内存、文件句柄等资源的释放的处理一定有很深的映像。在使用C++开发过程中,我们必须保证在函数退出或对象析构时进行正确的资源释放动作,而这在复杂的代码逻辑中往往是非常容易出错的地方。

GO语言通过defer关键字解决了开发人员的这个难题。开发人员只需要在申请了资源后立即通过defer关键字编写资源释放的代码,GO会保证在正确的时机释放资源(有点类似C++的finally)。比如下面的例子中,GO会保证在TestResource函数在退出前,调用到defer后面的代码以保证文件被关闭:

1
2
3
4
5
6
7
8
func TestResource() {
file, err := os.Open(fn)
if err != nil {
return
}
defer file.Close()
//.......
}

注意:如果一个作用域内有多个defer,则在退出时会按先进后出的方式进行调用。因此在使用时应该尽量避免一个作用域内多个defer中的代码存在顺序要求,以避免出错。

  • panic&recover

panic和recover的函数原型如下:

1
2
func panic(interface{}) 
func recover() interface{}

有点类似于C++中的抛出异常,在函数的执行过程中,如果panic被调用到则当前函数执行中止,此时将调用函数中走到过的defer代码,然后再依次向上级函数返回并调用这些函数的panic函数,直至当前goroutine中所有函数中止。

而recover函数通常用于在defer中用于判断函数是否是panic导致的退出:

1
2
3
4
5
defer func() {
if r := recover(); r != nil {
//说明是panic的情况,进行相应的处理
}
}

更丰富的内建类型

C++仅支持布尔、字符、数值内建类型,如果需要使用字符串、列表、字典,必须引入stl或其它库。

GO则将字符串、切片(slice)、字典、复数作为了原子类型支持。下面我们通过示例看一下GO对各种类型的支持:

golang的数组可以像python一样使用range遍历

1
2
3
for index,value := range array{
....
}
  1. index : 数组下标
  2. value 下标对应位置
  3. array:数组名
  4. 都是仅在for循环内部可见的局部变量
  5. 遍历数组时,如果不想使用index,可以使用_代替

字符串(string)

1
2
3
var s string
str := "Hello World!"
s = str

切片(slice)

GO语言中的切片类似于stl中的vector,它实际包括了一个指向数组的指针,数组元素的个数,以及对应的内存存储空间。

  1. 切片是数组的一个引用,那么切片是一个引用类型,这和数组是不一样的,函数中改变的会改变其值

  2. 切片的长度是可以变化的

  3. 切片的使用类似于数组,遍历和访问都是和数组一样的

  4. 切片的定义基本语法:

    1
    2
    3
    4
    5
    var slicename [] type
    /*
    slicename:切片名
    type :类型
    */

下面的示例展示了如何通过一个数组创建slice。

1
2
3
4
myArray := [5]int{1, 2, 3, 4, 5}
mySlice := myArray[:3]
//通过len和cap函数,可以获取slice的长度和容量
l, c := len(mySlice), cap(mySlice)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

func main() {
var intArr [5]int = [...]int{11, 22, 33, 44, 55} //数组
slice := intArr[1:3]
fmt.Println("intarr=", intArr)
fmt.Println("intarr的容量是 ", len(intArr))
fmt.Println("slice 的元素是 ", slice)
fmt.Println("slice 的容量是", cap(slice))
fmt.Println("slice 的元素个数为", len(slice))
}
/*
intarr= [11 22 33 44 55]
intarr的容量是 5
slice 的元素是 [22 33]
slice 的容量是 4
slice 的元素个数为 2
*/

也可以通过make函数创建指定类型、长度和容量的slice:

1
2
//创建一个int类型的slice,初始长度为3,但容量为5
myArray := make([]int, 3, 5)

注意:

  1. slice 是切片名称
  2. intArr[1:3] 表示slice引用数组第二个元素到下标
  3. 引用intArr数组的起始下标为1,终止下标为3,但是不包含3
  4. 切片的容量cap是可变的,这样可以节约空间
  5. 此时改变数组的值,slice 的值也会发生变化(引用)

切片在内存中形式

在内存里,可以理解为slic是由三个部分组成的

  1. 第一个位置记录的是数组的地址,是引用类型
  2. 第二个记录了slic本身的长度
  3. 第三个记录的是slic容量的大小

可以理解为slic是一个引用类型(本身也是有个地址)slic从底层来说其实就是一个数据结构,是struct结构体

1
2
3
4
5
type slice struct{
ptr *[2]int
len int
cap int
}

append() 和 copy() 函数

  • append动态追加
1
2
3
4
var slice []int = []int{100, 200, 300}
slice3 = append(slice, 400, 500)
/*slice4则是一个新的空间,slice3被回收,如果是slice3,则在原来的空间扩容*/
fmt.Println("slice", slice)
  1. 如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
  2. 使用append时Go底层创建一个新的数组newArr安装扩容后大小
  3. 将slice原来包含的元素拷贝到新的数组,newArr是在底层维护的,程序员不可见
  • copy内置函数拷贝
1
2
3
4
5
6
var slice4 []int = []int{1, 2, 3, 4, 5}
var slice5 = make([]int, 10)
copy(slice5, slice4) //将切片slice4拷贝为slice5

fmt.Println(slice4) //1,2,3,4,5
fmt.Println(slice5) //1,2,3,4,5,0,0,0,0,0
  1. 如果修改slice5的值,slice4不变,他们之间的数据空间是独立的
  2. 默认情况下,使用make后,多余的空间默认为0

当使用拷贝的时候,如果当前切片容量不够怎么办,会报错吗?

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

import "fmt"

func main() {
a := []int{1, 2, 3, 4, 5}
slice := make([]int, 1)
fmt.Println("a=", a)
fmt.Println(slice) //0
copy(slice, a)
fmt.Println(slice) //不会报错,而且赋予的是第一个元素的值
}
/*
a= [1 2 3 4 5]
[0]
[1]
*/

字典(map)

在Go中定义字典类型以map[KeyType]ValueType的形式定义,示例如下:

1
2
3
4
var dict map[string]int
dict := make(map[string]int, 100) //构造了下string:int类型的map,容量为100
dict["int_1"] = 1
v, ok := dict["int_1"] //通过判断第2个返回值是否为true,可以判断数据是否存在

复数(complex64/complex128)

除了以上常见的数据类型,Go还支持复数类型。

1
2
3
4
var z complex64
z = 10 + 20i //即10 + 20i
a = real(z) //real获取复数的实部
b = imag(z) //imag获取复数的虚部

除了以上类型,GO还支持管道channel、unicode字符rune、错误类型error、指针pointer。


类型系统

除了内建类型之后,GO也支持用户自定义类型,比如struct、interface,但在语法上有一些差异,比如下面的示例是GO的struct和interface的定义:

1
2
3
4
5
6
7
type Rect struct {
X, Y int
}

type TestItf interface {
f()
}

但GO的struct和interface与C++存在几点显著的不同:

  1. 不支持继承,只能通过组合实现继承
  2. 可以给任意类型增加方法
  3. 类不需要显式指定实现interface
  4. 对象可以在多个具有相同接口的interface间转换

通过以下的示例可以更好的理解以上几点差异:

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
//将内建类型int定义Integer
type Integer int
//为Integer增加Add方法
func (a Integer)Add(b Integer) Integer{
return a + b
}

type IReader interface {
Read() string
}

type Father struct {
I Integer
}
//Father并没有显式实现IReader接口,但通过实现了Read函数就相当于是IReader的实例了
func (f *Father)Read() string {
fmt.Println("Father::I = ", f.I)
return "Father's Read"
}

type Child struct {
Father //通过匿名组合了Father,Child自动“继承”了I成员和Func方法
}

type IStream interface {
Read() string
}

var obj IReader
var is IStream
c := &Child{}
obj = c //Child是IReader的实例,因为c通过组合继承了Father实现的Read方法
is = obj //IReader因为和IStream有相同的接口定义,因此可以互相转换

Go并发

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

goroutine 语法格式:

1
go 函数名( 参数列表 )

Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间

案例

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

import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行。


通道

概念

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

1
2
3
ch <- v    // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

1
ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:

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
package main

import "fmt"

func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收

fmt.Println(x, y, x+y)
}
/*
-5 17 12
*/

通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

1
ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

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

import "fmt"

func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)

// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2

// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
/*
1
2
*/

Go 遍历通道与关闭通道

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

1
v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。

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
37
38
package main

import (
"fmt"
)

func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}
/*
0
1
1
2
3
5
8
13
21
34
*/

要点

  1. goroutine 是 golang 中在语言级别实现的轻量级线程,仅仅利用 go 就能立刻起一个新线程。多线程会引入线程之间的同步问题,在 golang 中可以使用 channel 作为同步的工具。

    通过 channel 可以实现两个 goroutine 之间的通信。

    向 channel 传入数据, CHAN <- DATA , CHAN 指的是目的 channel 即收集数据的一方, DATA 则是要传的数据。

    从 channel 读取数据, DATA := <-CHAN ,和向 channel 传入数据相反,在数据输送箭头的右侧的是 channel,形象地展现了数据从隧道流出到变量里。

  2. Channel 是可以控制读写权限的 具体如下:

    1
    2
    3
    4
    5
    6
    7
    8
    //定义只读的channel
    read_only := make (<-chan int)

    //定义只写的channel
    write_only := make (chan<- int)

    //可同时读写
    read_write := make (chan int)

    定义只读和只写的channel意义不大,一般用于在参数传递中

    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
    package main

    import (
    "fmt"
    "time"
    )

    func main() {
    c := make(chan int)
    go send(c)
    go recv(c)
    time.Sleep(3 * time.Second)
    }

    //只能向chan里写数据
    func send(c chan<- int) {
    for i := 0; i < 10; i++ {
    c <- i
    }
    }

    //只能取channel中的数据
    func recv(c <-chan int) {
    for i := range c {
    fmt.Println(i)
    }
    }
---------- End~~ 撒花ฅ>ω<*ฅ花撒 ----------