转载:https://www.luozhiyun.com/archives/213

人是否会进步以及进步得有多快,依赖的恰恰就是对自我的否定,这包括否定的深刻与否,以及否定自我的频率如何。这其实就是“不破不立”这个词表达的含义。

数组和切片的range子句遍历

切片

先看例子:

	numbers1 := []int{1, 2, 3, 4, 5, 6}
for i := range numbers1 {
if i == 3 {
numbers1[i] += i
}
}
fmt.Println(numbers1)

在上面这个例子中,会返回:

[1 2 3 7 5 6]

由于numbers1是⼀个 切⽚,那么迭代变量就可以有两个,右边的迭代变量代表当次迭代对应的某⼀个元素值,⽽左边的迭代变量则代表该元素值在切⽚中的索引值。

如果像上面这样只有一个迭代变量只会返回对应的索引值,这里和java很不一样,在java中使用foreach迭代只会返回元素值。

如果像如下这样又会返回什么呢:

	numbers3 := []int{1, 2, 3, 4, 5, 6}
maxIndex3 := len(numbers2) - 1
for i, e := range numbers3 {
if i == maxIndex3 {
numbers3[0] += e
} else {
numbers3[i+1] += e
}
}
fmt.Println(numbers3)

在这个例子中,迭代返回了两个参数,i表示的是索引值,e表示的是切片中的元素。

这里打印的结果是:

[22 3 6 10 15 21]

因为切片是引用类型的,所以传入for循环中的是一个指针,所以每次遍历都会修改原来的值。

数组

	numbers2 := [...]int{1, 2, 3, 4, 5, 6}
maxIndex2 := len(numbers2) - 1
for i, e := range numbers2 {
if i == maxIndex2 {
numbers2[0] += e
} else {
numbers2[i+1] += e
}
}
fmt.Println(numbers2)

像上面这样遍历一个数组,那么会返回什么?

[7 3 5 7 9 11]

因为数组是值类型的,所以range表达式的求值结果会被复制,也就是说,被迭代的对象是range表达式结果值的副本⽽不是原值。

在第⼀次迭代时,我改变的是numbers2的第⼆个元素的值,新值为3,也就是1和2之和。

但是,被迭代的对象的第⼆个元素却没有任何改变,毕竟它与numbers2已经是毫不相关的两个数组了。因此,在第⼆次迭代 时,我会把numbers2的第三个元素的值修改为5,即被迭代对象的第⼆个元素值2和第三个元素值3的和。

异常处理

panic

panic类似java里面的Exception,当引发一个panic的时候会将调用栈打印出来,并引发程序的崩溃终止。

panic详情会在控制权传播的过程中,被逐渐地积累和完善,并且,控制权会一级一级地沿着调用栈的反方向传播至顶端。

因此,在针对某个 goroutine 的代码执行信息中,调用栈底端的信息会先出现,然后是上一级调用的信息,以此类推,最后才是此调用栈顶端的信息。

例如下面这个例子:

main函数调用了caller1函数,而caller1函数又调用了caller2函数,那么caller2函数中代码的执行信息会先出现,然后是caller1函数中代码的执行信息,最后才是main函数的信息。

recover函数和defer语句

Go 语言的内建函数recover专用于恢复 panic,或者说平息运行时恐慌。recover函数无需任何参数,并且会返回一个空接口类型的值。

我们在使用recover 函数的时候不能将其放置在panic之后或之前。

放置之后panic 一旦发生,控制权就会讯速地沿着调用栈的反方向传播。所以,在panic函数调用之后的代码,根本就没有执行的机会;把调用recover函数的代码提前,那时未发生 panic,那么该函数就不会做任何事情,并且只会返回一个nil。

所以我们需要将recover函数和defer语句配合使用,defer语句相当于java里面的try -finally,它会让其携带的defer函数的调用延迟执行,并且会延迟到该defer语句所属的函数即将结束执行的那一刻。

利用recover和defer防止由panic引起的程序崩溃

func main() {
fmt.Println("Enter function main.")
defer func(){
fmt.Println("Enter defer function.")
if p := recover(); p != nil {
fmt.Printf("panic: %s\n", p)
}
fmt.Println("Exit defer function.")
}()
// 引发panic。
panic(errors.New("something wrong"))
fmt.Println("Exit function main.")
}

在上面这个代码中会返回:

Enter function main.
Enter defer function.
panic: something wrong
Exit defer function.

在defer语句中,通过recover函数将错误给成功捕获,避免了程序的崩溃。

测试

  • 对于功能测试函数来说,其名称必须以Test为前缀,并且参数列表中只应有一个*testing.T类型的参数声明。
  • 对于性能测试函数来说,其名称必须以Benchmark为前缀,并且唯一参数的类型必须是*testing.B类型的。

功能测试

$ go test lesson2/article10/main
ok lesson2/article10/main 0.006s

如上,我们执行一个功能测试,会返回一个ok,表示测试成功,并且会显示在0.006s完成了测试。

如果我们再次的执行

$ go test lesson2/article10/main
ok lesson2/article10/main (cached)

会发现多了个(cached),这表明我们的测试结果被缓存了,但是如果我们的程序有任何变动,缓存数据就会失效,go 命令就会再次真正地执行操作。所以我们并不用担心打印出的缓存数据不是实时的结果。

go 命令会定期地删除最近未使用的缓存数据,但是,如果你想手动删除所有的缓存数据,运行一下go clean -cache命令就好了。

对于测试成功的结果,go 命令也是会缓存的。运行go clean -testcache将会删除所有的测试结果缓存。不过,这样做肯定不会删除任何构建结果缓存。

性能测试

性能测试这个在java中是没有中,只能依靠其他的手段实现。所以我们来看看Go中性能测试是如何做的。

我们写一个测试函数:

func BenchmarkGetPrimes(b *testing.B) {
for i := 0; i < b.N; i++ {
GetPrimes(1000)
}
}

在这个函数中调用了GetPrimes方法,并传入一个1000的值。

我们再运行上面的测试函数如下:

$ go test -bench=. -run=^$  lesson2/article10/main
goos: darwin
goarch: amd64
pkg: lesson2/article10/main
BenchmarkGetPrimes-8 312973 3307 ns/op
PASS
ok lesson2/article10/main 1.082s

我们在上面的命令中加入了两个参数:

-bench=.,只有有了这个标记,命令才会进行性能测试,.表示需要执行任意名称的性能测试函数。

-run=$,这个标记用于表明需要执行哪些功能测试函数,值$意味着:只执行名称为空的功能测试函数,换句话说,不执行任何功能测试函数。

在运行完命令后的结果中:

BenchmarkGetPrimes-8被称为单个性能测试的名称,它表示命令执行了性能测试函数BenchmarkGetPrimes,并且当时所用的最大 P 数量为8。最大 P 数量相当于可以同时运行 goroutine 的逻辑 CPU 的最大个数。

在写测试函数的时候会调用传入参数b *testing.B的b.N。go test命令会先尝试把b.N设置为1,然后执行测试函数。

如果测试函数的执行时间没有超过上限,此上限默认为 1 秒,那么命令就会改大b.N的值,然后再次执行测试函数,如此往复,直到这个时间大于或等于上限为止。

所以b.N就是指的上面结果中的312973。3307 ns/op表明单次执行GetPrimes函数的平均耗时为3307纳秒。

放上一个示例图看一下:

条件变量sync.Cond

sync.Cond类似于java里面的Condition,初始化的时候也是需要互斥锁初始化。

条件变量提供的方法有三个:等待通知(wait)、单发通知(signal)和广播通知(broadcast)。

等待通知的时候需要在互斥锁保护下进行,但是在signal和broadcast却是需要在对应的互斥锁解锁之后再做这两种操作。

sync.Cond类型并不是开箱即用的。我们只能利用sync.NewCond函数创建它的指针值。这个函数需要一个sync.Locker类型的参数值。

因此我们通常需要这样进行初始化:

	var lock sync.Mutex
cond := sync.NewCond(&lock)

在为sendCond变量做初始化的时候,把基于lock变量的指针值传给了sync.NewCond函数。

我们下面来看一个例子,比较长,需要点耐心:

func main() {
// mailbox 代表信箱。
// 0代表信箱是空的,1代表信箱是满的。
var mailbox uint8
// lock 代表信箱上的锁。
var lock sync.Mutex
// sendCond 代表专用于发信的条件变量。
sendCond := sync.NewCond(&lock)
// recvCond 代表专用于收信的条件变量。
recvCond := sync.NewCond(&lock) // send 代表用于发信的函数。
send := func(id, index int) {
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
log.Printf("sender [%d-%d]: the mailbox is empty.",
id, index)
mailbox = 1
log.Printf("sender [%d-%d]: the letter has been sent.",
id, index)
lock.Unlock()
recvCond.Broadcast()
} // recv 代表用于收信的函数。
recv := func(id, index int) {
lock.Lock()
for mailbox == 0 {
recvCond.Wait()
}
log.Printf("receiver [%d-%d]: the mailbox is full.",
id, index)
mailbox = 0
log.Printf("receiver [%d-%d]: the letter has been received.",
id, index)
lock.Unlock()
sendCond.Signal() // 确定只会有一个发信的goroutine。
} // sign 用于传递演示完成的信号。
sign := make(chan struct{}, 3)
max := 6
go func(id, max int) { // 用于发信。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500)
send(id, i)
}
}(0, max)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= max; j++ {
time.Sleep(time.Millisecond * 200)
recv(id, j)
}
}(1, max/2)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for k := 1; k <= max; k++ {
time.Sleep(time.Millisecond * 200)
recv(id, k)
}
}(2, max/2) <-sign
<-sign
<-sign
}

在上面的例子中,使用了两个condition,sendCond和recvCond。send函数用于发信的函数,因为我在这里收信方设置了两个goroutine,所以在发送方使用了Broadcast,而接收方使用了Signal。

看完了上面的代码,在看看下面的几个注意事项

先加锁再调用Wait

我们下面来看看Wait方法做了什么:

  1. 把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。
  2. 解锁当前的条件变量基于的那个互斥锁。
  3. 让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。
  4. 如果通知到来并且决定唤醒这个 goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的 goroutine 就会继续执行后面的代码了。

所以Wait方法实际上经历了:

入队 -> 解锁 -> 等待 -> 唤醒 -> 重新加锁 -> 继续执行

所以在阻塞当前的goroutine之前会解锁,所以如果不先进行锁定的话会引发一个不可恢复的 panic。

如果当前的 goroutine 无法解锁,别的 goroutine 也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?

要用for语句来包裹调用其Wait方法的表达式

这主要是为了保险起见。如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。

还有就是为了防止:

  1. 有多个 goroutine 在等待共享资源的同一种状态。被唤醒的时候无法保证每次的共享资源的状态都是正确的,所以需要做一个检查
  2. 共享资源可能有的状态可能有很多。所以,在设计合理的前提下,单一的结果一定不可能满足所有 goroutine 的条件。那些未被满足的 goroutine 显然还需要继续等待和检查。
  3. 在一些多 CPU 核心的计算机系统中,即使没有收到条件变量的通知,调用其Wait方法的 goroutine 也是有可能被唤醒的。这是由计算机硬件层面决定的。

Signal方法和Broadcast方法

条件变量的Wait方法总会把当前的 goroutine 添加到通知队列的队尾,而它的Signal方法总会从通知队列的队首开始,查找可被唤醒的 goroutine。所以,因Signal方法的通知,而被唤醒的 goroutine 一般都是最早等待的那一个。

所以如果能确定只有一个goroutine在等待通知,或者只需要唤醒任意一个goroutine就可以满足要求,那么可以使用Signal,其他情况最好使用Broadcast

sync.WaitGroup

WaitGroup其实有点像java的CountDownLatch,可以用来控制goroutine之间协作流程。

WaitGroup类型拥有三个指针方法:Add、Done和Wait。

例如:

func coordinateWithWaitGroup() {
var wg sync.WaitGroup
wg.Add(2)
num := int32(0)
fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
max := int32(10)
go addNum(&num, 3, max, wg.Done)
go addNum(&num, 4, max, wg.Done)
wg.Wait()
} // addNum 用于原子地增加numP所指的变量的值。
func addNum(numP *int32, id, max int32, deferFunc func()) {
defer func() {
deferFunc()
}()
for i := 0; ; i++ {
currNum := atomic.LoadInt32(numP)
if currNum >= max {
break
}
newNum := currNum + 2
time.Sleep(time.Millisecond * 200)
if atomic.CompareAndSwapInt32(numP, currNum, newNum) {
fmt.Printf("The number: %d [%d-%d]\n", newNum, id, i)
} else {
fmt.Printf("The CAS operation failed. [%d-%d]\n", id, i)
}
}
}

coordinateWithWaitGroup函数会等待两个addNum函数运行完wg.Done方法后wg.Wait()才会继续放行。

由于主goroutine需要等待其他的goroutine运行完毕后才能继续往下执行,所以可以在addNum的defer函数中才调用wg.Done函数。

需要注意的是,和java的CountDownLatch不一样的是CountDownLatch的countDown方法是可以重复调用,即使计数小于零也不会报错,但是WaitGroup的计数器的值不能小于0,是因为这样会引发一个 panic。

sync.Once

该方法的功能是只执行“首次被调用时传入的”那个函数,并且之后不会再执行任何参数函数。

举例:

	var counter uint32
var once sync.Once
once.Do(func() {
atomic.AddUint32(&counter, 2)
})
fmt.Printf("The counter: %d\n", counter)
once.Do(func() {
atomic.AddUint32(&counter, 1)
}) fmt.Printf("The counter: %d\n", counter)

在上面这个例子中,once.Do里面修饰的函数只会执行第一个,第二个函数并不会得到执行。

因为,Once类型中有一个名叫done的uint32类型的字段。它的作用是记录其所属值的Do方法被调用的次数。不过,该字段的值只可能是0或者1。一旦Do方法的首次调用完成,它的值就会从0变为1。

所以每次在执行once.Do的时候都会判断done是不是为1,并且在这个条件判断之后,Do方法会立即锁定其所属值中的那个sync.Mutex类型的字段m。然后,它会在临界区中再次检查done字段的值,并且仅在条件满足时,才会去调用参数函数,以及用原子操作把done的值变为1。

以上保证了sync.Once只会调用一次。

需要注意的是:

  1. 由于Do方法里面加了互斥锁,所以如果有多个goroutine同时执行,并且里面参数函数需要执行很长时间,那么其他goroutine会一起阻塞
  2. Do方法在参数函数执行结束后,对done字段的赋值用的是原子操作,并且,这一操作是被挂在defer语句中的。因此,不论参数函数的执行即使报错,done字段的值都会变为1。

Java程序员学习Go指南(三)的更多相关文章

  1. 【Python】Java程序员学习Python(三)— 基础入门

    一闪一闪亮晶晶,满天都是小星星,挂在天上放光明,好像许多小眼睛.不要问我为什么喜欢这首歌,我不会告诉你是因为有人用口琴吹给我听. 一.Python学习文档与资料 一般来说文档的资料总是最权威,最全面的 ...

  2. Java程序员学习Go指南(终)

    我的博客:https://www.luozhiyun.com/archives/215 context.Context类型 Context类型可以提供一类代表上下文的值.此类值是并发安全的,也就是说它 ...

  3. Java程序员学习Go指南(二)

    摘抄:https://www.luozhiyun.com/archives/211 Go中的结构体 构建结构体 如下: type AnimalCategory struct { kingdom str ...

  4. Java程序员学习之路

    1. Java语言基础 谈到Java语 言基础学习的书籍,大家肯定会推荐Bruce Eckel的<Thinking in Java>.它是一本写的相当深刻的技术书籍,Java语言基础部分基 ...

  5. 聊聊阿里社招面试,谈谈“野生”Java程序员学习的道路

    引言 很尴尬的是,这个类型的文章其实之前笔者就写过,原文章里,笔者自称LZ(也就是楼主,有人说是老子的简写,笔者只想说,这位同学你站出来,保证不打死你,-_-),原文章名称叫做<回答阿里社招面试 ...

  6. 【Python】Java程序员学习Python(五)— 函数的定义和使用

    不想做一个待宰的羔羊!!!!要自己变得强大.... 函数的定义和使用放在最前边还是有原因的,现在语言趋于通用,基本类型基本都是那些,重点还是学习对象的使用方法,而最根本的还是方法的使用,因此优先介绍, ...

  7. 【Python】Java程序员学习Python(二)— 开发环境搭建

    巧妇难为无米之炊,我最爱的还是鸡蛋羹,因为我和鸡蛋羹有段不能说的秘密. 不管学啥,都要有环境,对于程序员来说搭建个开发环境应该不是什么难题.按顺序一步步来就可以,我也只是记录我的安装过程,你也可以滴. ...

  8. 如何准备阿里社招面试,顺谈 Java 程序员学习中各阶段的建议

    引言 其实本来真的没打算写这篇文章,主要是LZ得记忆力不是很好,不像一些记忆力强的人,面试完以后,几乎能把自己和面试官的对话都给记下来.LZ自己当初面试完以后,除了记住一些聊过的知识点以外,具体的内容 ...

  9. 写给自己的Java程序员学习路线图

    恩,做开发的工作已经三年多了,说起来实在是惭愧,自己的知识树还像一棵小草一样,工作中使用到了许多的知识和技术,不过系统性不够.根基不牢.并且不够深入!当然,慢慢的我也更加的清楚,我需要学习一些什么样的 ...

随机推荐

  1. December 21st, Week 51st Saturday, 2019

    May the odds be ever in your favor. 愿好运永远眷顾你. From The Hunger Games. May we all have good luck, and ...

  2. 微信小程序 列表倒计时

    最近要实现一个列表倒计时的功能,写了个demo 展示图 <view class="center colu"> <view class="time&quo ...

  3. PTA的Python练习题(四)

    从 第3章-1 3-1.大于身高的平均值 开始 1. 引入列表的概念 a=list(map(int,input().split())) b=sum(a) c=len(a) d=b/c for i in ...

  4. xshell编码设置-----支持中文

    1. 点击 设置 图标 2. 选择 UTF-8 3. 重启xshell即可

  5. Codeforces 599D:Spongebob and Squares

    D. Spongebob and Squares time limit per test 2 seconds memory limit per test 256 megabytes input sta ...

  6. spring-cloud-服务调用流程

  7. Charles + Android 抓取Https数据包 (适用于Android 6.0及以下)

    通过Charles代理,我们能很轻易的抓取手机的Http请求,因为Http属于明文传输,所以我们能直接获取到我们要抓取的内容.但是Https内容本身就是加密的,这时我们会发现内容是加密的了.本文我们来 ...

  8. SpringMVC 注解配置

    使用注解配置spring  mvc (1)spring mvc的配置文件 <?xml version="1.0" encoding="UTF-8"?> ...

  9. ASP.NET MVC Web项目中使用Log4Net记录日志,并按照日志类型分文件存储

    1.创建MvcLog4Net项目 2.创建 空的MVC项目 3.项目创建完成的效果 4.选择项目,点击鼠标右键,在弹出菜单中选择“管理解决方案的 NuGet 程序包” 5. 在NuGet浏览界面: 点 ...

  10. SQL注入个人理解及思路(包括payload和绕过的一些方式)

    首先本文主要是把我对SQL注入的一些坑和最早学习SQL注入的时候的一些不理解的地方做一个梳理. (本文仅为个人的一点皮毛理解,如有错误还望指出,转载请说明出处,大佬勿喷=.=) 什么是SQL注入呢? ...