• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

Go语言语法说明 - kexinxin

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

Go语言语法说明

Go语言语法说明

go语言中的go func(){}() 表示以并发的方式调用匿名函数func

 

 

深入讲解Go语言中函数new与make的使用和区别

前言

本文主要给大家介绍了Go语言中函数new与make的使用和区别,关于Go语言中new和make是内建的两个函数,主要用来创建分配类型内存。在我们定义生成变量的时候,可能会觉得有点迷惑,其实他们的规则很简单,下面我们就通过一些示例说明他们的区别和使用,话不多说了,来一起看看详细的介绍吧。

变量的声明

var i int

var s string

变量的声明我们可以通过var关键字,然后就可以在程序中使用。当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,string类型的零值是"",引用类型的零值是nil。

对于例子中的两种类型的声明,我们可以直接使用,对其进行赋值输出。但是如果我们换成引用类型呢?

package main

import (

"fmt"

)

func main() {

var i *int

*i=10

fmt.Println(*i)

}

这个例子会打印出什么?0还是10?。以上全错,运行的时候会painc,原因如下:

panic: runtime error: invalid memory address or nil pointer dereference

从这个提示中可以看出,对于引用类型的变量,我们不光要声明它,还要为它分配内容空间,否则我们的值放在哪里去呢?这就是上面错误提示的原因。

对于值类型的声明不需要,是因为已经默认帮我们分配好了。

要分配内存,就引出来今天的new和make。

new

对于上面的问题我们如何解决呢?既然我们知道了没有为其分配内存,那么我们使用new分配一个吧。

func main() {

var i *int

i=new(int)

*i=10

fmt.Println(*i)

}

现在再运行程序,完美PASS,打印10。现在让我们看下new这个内置的函数。

// The new built-in function allocates memory. The first argument is a type,

// not a value, and the value returned is a pointer to a newly

// allocated zero value of that type.

func new(Type) *Type

它只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零,也就是类型的零值。

我们的例子中,如果没有*i=10,那么打印的就是0。这里体现不出来new函数这种内存置为零的好处,我们再看一个例子。

func main() {

u:=new(user)

u.lock.Lock()

u.name = "张三"

u.lock.Unlock()

fmt.Println(u)

}

type user struct {

lock sync.Mutex

name string

age int

}

示例中的user类型中的lock字段我不用初始化,直接可以拿来用,不会有无效内存引用异常,因为它已经被零值了。

这就是new,它返回的永远是类型的指针,指向分配类型的内存地址。

make

make也是用于内存分配的,但是和new不同,它只用于chan、map以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。

func make(t Type, size ...IntegerType) Type

从函数声明中可以看到,返回的还是该类型。

二者异同

所以从这里可以看的很明白了,二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。

make返回的还是这三个引用类型本身;而new返回的是指向类型的指针。

其实new不常用

所以有new这个内置函数,可以给我们分配一块内存让我们使用,但是现实的编码中,它是不常用的。我们通常都是采用短语句声明以及结构体的字面量达到我们的目的,比如:

i:=0

u:=user{}

这样更简洁方便,而且不会涉及到指针这种比麻烦的操作。

make函数是无可替代的,我们在使用slice、map以及channel的时候,还是要使用make进行初始化,然后才才可以对他们进行操作。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对极客世界的支持。

 

 

Golang select的使用及典型用法

select是Go中的一个控制结构,类似于switch语句,用于处理异步IO操作。select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。

select中的case语句必须是一个channel操作

select中的default子句总是可运行的。

  1. 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行。
  2. 如果没有可运行的case语句,且有default语句,那么就会执行default的动作。
  3. 如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行

 

 

Go语言的goroutines、信道和死锁

goroutine

Go语言中有个概念叫做goroutine, 这类似我们熟知的线程,但是更轻。

以下的程序,我们串行地去执行两次loop函数:

func loop() {

					for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}

 

 

func main() {
    loop()
    loop()
}

毫无疑问,输出会是这样的:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

下面我们把一个loop放在一个goroutine里跑,我们可以使用关键字go来定义并启动一个goroutine:

func main() {

						go loop() // 启动一个goroutine
    loop()
}

这次的输出变成了:

0 1 2 3 4 5 6 7 8 9

可是为什么只输出了一趟呢?明明我们主线跑了一趟,也开了一个goroutine来跑一趟啊。

原来,在goroutine还没来得及跑loop的时候,主函数已经退出了。

main函数退出地太快了,我们要想办法阻止它过早地退出,一个办法是让main等待一下:

func main() {

					go loop()
    loop()
    time.Sleep(time.Second) // 停顿一秒
}

这次确实输出了两趟,目的达到了。

可是采用等待的办法并不好,如果goroutine在结束的时候,告诉下主线说"Hey, 我要跑完了!"就好了,即所谓阻塞主线的办法,回忆下我们Python里面等待所有线程执行完毕的写法:

for thread in threads:
    thread.join()

是的,我们也需要一个类似join的东西来阻塞住主线。那就是信道

 

信道

信道是什么?简单说,是goroutine之间互相通讯的东西。类似我们Unix上的管道(可以在进程间传递消息),用来goroutine之间发消息和接收消息。其实,就是在做goroutine之间的内存共享。

使用make来建立一个信道:

var channel chan
								int = make(chan
														int)
// 
channel := make(chan
									int)

那如何向信道存消息和取消息呢?一个例子:

func main() {

					var messages chan
									string = make(chan
															string)

					go
							func(message string) {
        messages <- message // 存消息
    }("Ping!")

 

    fmt.Println(<-messages) // 取消息
}

默认的,信道的存消息和取消息都是阻塞的 (叫做无缓冲的信道,不过缓冲这个概念稍后了解,先说阻塞的问题)

也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。

比如以下的main函数和foo函数:

var ch chan
								int = make(chan
														int)

 

func foo() {
    ch <- 0
								// ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走
}

 

func main() {

					go foo()
    <- ch // ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止
}

那既然信道可以阻塞当前的goroutine, 那么回到上一部分「goroutine」所遇到的问题「如何让goroutine告诉主线我执行完毕了」的问题来, 使用一个信道来告诉主线即可:

var complete chan
								int = make(chan
														int)

 

func loop() {

					for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }

 

    complete <- 0
								// 执行完毕了,发个消息
}

 

 

func main() {

					go loop()
    <- complete // 直到线程跑完, 取到消息. main在此阻塞住
}

如果不用信道来阻塞主线的话,主线就会过早跑完,loop线都没有机会执行、、、

其实,无缓冲的信道永远不会存储数据,只负责数据的流通,为什么这么讲呢?

  • 从无缓冲信道取数据,必须要有数据流进来才可以,否则当前线阻塞
  • 数据流入无缓冲信道, 如果没有其他goroutine来拿走这个数据,那么当前线阻塞

所以,你可以测试下,无论如何,我们测试到的无缓冲信道的大小都是0 (len(channel))

如果信道正有数据在流动,我们还要加入数据,或者信道干涩,我们一直向无数据流入的空信道取数据呢?就会引起死锁

 

死锁

一个死锁的例子:

func main() {
    ch := make(chan
									int)
    <- ch // 阻塞main goroutine, 信道c被锁
}

执行这个程序你会看到Go报这样的错误:

fatal error: all goroutines are asleep - deadlock!

何谓死锁操作系统有讲过的,所有的线程或进程都在等待资源的释放。如上的程序中, 只有一个goroutine, 所以当你向里面加数据或者存数据的话,都会锁死信道,并且阻塞当前 goroutine, 也就是所有的goroutine(其实就main线一个)都在等待信道的开放(没人拿走数据信道是不会开放的),也就是死锁咯。

我发现死锁是一个很有意思的话题,这里有几个死锁的例子:

  1. 只在单一的goroutine里操作无缓冲信道,一定死锁。比如你只在main函数里操作信道:
  2. func main() {
    
  3.     ch := make(chan
    											int)
    
  4.     ch <- 1
    										// 1流入信道,堵塞当前线, 没人取走数据信道不会打开
  5.     fmt.Println("This line code wont run") //在此行执行之前Go就会报死锁
  6. }
    
  7. 如下也是一个死锁的例子:
  8. var ch1 chan
    										int = make(chan
    																int)
    
  9. var ch2 chan
    										int = make(chan
    																int)
    
  10.  
  11. func say(s string) {
    
  12.     fmt.Println(s)
    
  13.     ch1 <- <- ch2 // ch1 等待 ch2流出的数据
  14. }
    
  15.  
  16. func main() {
    
  17. 
    							go say("hello")
    
  18.     <- ch1  // 堵塞主线
  19. }
    

    其中主线等ch1中的数据流出,ch1ch2的数据流出,但是ch2等待数据流入,两个goroutine都在等,也就是死锁。

  20. 其实,总结来看,为什么会死锁?非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。或者这样理解 Go启动的所有goroutine里的非缓冲信道一定要一个线里存数据,一个线里取数据,要成对才行 。所以下面的示例一定死锁:
  21. c, quit := make(chan
    											int), make(chan
    																	int)
    
  22.  
  23. go
    								func() {
    
  24.    c <- 1
    										// c通道的数据没有被其他goroutine读取走,堵塞当前goroutine
  25.    quit <- 0
    										// quit始终没有办法写入数据
  26. }()
    
  27.  
  28. <- quit // quit 等待数据的写

    仔细分析的话,是由于:主线等待quit信道的数据流出,quit等待数据写入,而funcc通道堵塞,所有goroutine都在等,所以死锁。

    简单来看的话,一共两个线,func线中流入c通道的数据并没有在main线中流出,肯定死锁。

但是,是否果真 所有不成对向信道存取数据的情况都是死锁?

如下是个反例:

func main() {
    c := make(chan
									int)

 


					go
							func() {
       c <- 1
    }()
}

程序正常退出了,很简单,并不是我们那个总结不起作用了,还是因为一个让人很囧的原因,main又没等待其它goroutine,自己先跑完了,所以没有数据流入c信道,一共执行了一个goroutine, 并且没有发生阻塞,所以没有死锁错误。

那么死锁的解决办法呢?

最简单的,把没取走的数据取走,没放入的数据放入,因为无缓冲信道不能承载数据,那么就赶紧拿走!

具体来讲,就死锁例子3中的情况,可以这么避免死锁:

c, quit := make(chan
									int), make(chan
															int)

 

go
						func() {
    c <- 1
    quit <- 0
}()

 

<- c // 取走c的数据!
<-quit

另一个解决办法是缓冲信道, 即设置c有一个数据的缓冲大小:

c := make(chan
									int, 1)

这样的话,c可以缓存一个数据。也就是说,放入一个数据,c并不会挂起当前线, 再放一个才会挂起当前线直到第一个数据被其他goroutine取走, 也就是只阻塞在容量一定的时候,不达容量不阻塞。

这十分类似我们python中的队列Queue不是吗?

 

无缓冲信道的数据进出顺序

我们已经知道,无缓冲信道从不存储数据,流入的数据必须要流出才可以。

观察以下的程序:

var ch chan
								int = make(chan
														int)

 

func foo(id int) { //id: 这个routine的标号
    ch <- id
}

 

func main() {

						// 开启5routine

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

					go foo(i)
    }

 


						
                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
Go语言简介(上)— 语法发布时间:2022-07-10
下一篇:
go语言基本语法 - sosogengdongni发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap