读《Go并发编程实战》第4章 流程控制方式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读《Go并发编程实战》第4章 流程控制方式相关的知识,希望对你有一定的参考价值。

       说实话,该书前面讲的枯燥冗长,看的有点打瞌睡,而我自己又是有一个有强迫症的人,喜欢一个字一个字地抠,最终结果是一看就困,然后转天再看再困,依次循环......。

       这就总会让我自己有点遐想,自己也写一本关于Go的书算了,但因为平时真的太忙了,稍有时间时又贡献给我女儿。我想后面我录一些视频,帮助那些想学习编程的人快速入门,打消非计算机专业人员的入门门槛。

       好了,废话有点多,还是耐着性子把作者郝林的这本书看完。


       Go语言与其它编程语言类似,也有if语句、switch语句、for循环语句、goto语句,但不同之处在于Go语言的循环语句只有for,没有平时我们见的while、do-while;在其它编程语言中我们经常听到尽量避免使用goto语句,而读过谢孟军的beego代码的人应该能看到谢大神使用了不少goto语句;此外,Go语言还有defer语句、针对异常处理引入了panic和recover语句等,本章就基本围绕着这些展开。


4.1 基本流程控制

首先看一个源代码:

package main

import (
    "fmt"
)

func main(){
    v := []int{1, 2, 3}
    if v != nil{
         var v int = 123
         fmt.Printf("%v\n", v)
    }
    fmt.Printf("%v\n", v)
}

       第一次看Go代码的人可能会有点不舒服,不过没有关系,看的多了就舒服了,掌握编程语言最大的诀窍就是多写,随便解释一下:

  • package和import不用多说,属于工程化思想中的体系部分

  • v := []int{1,2,3},上看下看左看右看,也没有看到v的定义呢?其实这里:=就是定义加赋值

       好,偏离原文太多了,原文在这里主要说代码块和作用域。


4.1.1 代码块和作用域

       所谓代码块就是一个由花括号“{”和“}”括起来的若干表达式和语句的序列。当然,代码块中也可以不包含任何代码 ,即为空代码块。就上面源代码为例,代码块就是main()函数中的内容。

       那么这个程序运行结果是什么呢?结果如下:

       123

       [1  2  3]

       之所以两次打印内容不同,就是由于作用域的原因,先采用作者的原话。

       “我们可以在某个代码块中对一个已经在包含它的外层代码块中声明过的标识符进行重声明。并且,当我们在内层代码块中使用这个标识符的时候,它代表的总是它在内层代码块中被重声明时与它绑定在一起的那个程序实体。也就是说,在这种情况下,在外层代码中声明的那个同名标识符被屏蔽了。

       看懂了吗?是不是有点绕死了?脑子稍短路一下就死机了,用人话来说是这样的:   

       main()函数是一个大的代码块,它里面又包括了一个小的代码块(对应的if语句),这相当于一间大房子(main)里面有一个小卧室(if{}),大房子客厅里有一个叫郝林的人,小卧室中也有一个叫郝林的人,且房子隔音效果非常好。当你进入小卧室时轻轻地喊一声“郝林”,那么是小卧室的郝林会回应你,因为大房子客厅中的那个郝林听不到;同样地,当你进入大房子客厅时,你轻轻地喊一声“郝林”,那么是大房子客厅的郝林会回应你,因为小房子中的那个郝林听不到。

       如果您学过程序编译原理的话,就很容易明白其中的原理,因为在编译时,这根本就是两个不同的变量,此处不再展开,所以有时候想想视频有市场是应当的,如果是视频两句话就能交待清楚,但文字就得啰嗦很多。


if 100 < number{
     number++
}

这个是一个if语句,当然也符合代码块的定义,由花括号括起来的若干表达式和语句的序列。

{
}

这个也是一个代码块,尽快没有任何内容,由花括号括起来的若干表达式和语句的序列,这里的若干包括0。

for i :=0; i <100; i++ {
    i = i + 4
}

同样,这也是一个代码块。


作者又说,在3.3.3节讲过,基本数据类型的值都无法与空值nil进行判等,而上面源代码if v != nil { }就没有编译错误,是因为这里的v代表的是一个切片类型而不是一个string类型值。这里稍有点要讲解的内容,一般语言是没有切片的,这是一个特别的类型,回头我做成一个演示动画一看就很清楚明白。


4.1.2 if语句

      “Go语言的if语句总是以关键字if开始,在这之后,可以跟一条简单语句(这里可以的意思是说,也可以没有),然后是一个作为条件判断的布尔类型的表达式以及一个用花括号“{”和“}”括起来的代码块”。

       上面这句话比较常,简单地理解,就是if语句必须是这种形式“if +条件判断 + {}”,举两个粟子:

var i int = 0

if i < 10 {
     i++
}

这个if语句就是典型的if+条件判断+{ },其中条件判断为i<10。当然这个if语句还可以这样写:

if i:=0; i <10 {
     i++
}

这个if语句就是上面蓝色字体中说的,在if之后可以跟一条简单语句i:=0,即变量i的定义和赋值语句。当然这个if语句还可以改写为:

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

}

这里把i++也放到了if和“{”之间。引用一下原书内容:

常用的简单语句包括短变量声明、赋值语句和表达式语句。除了特殊的内建函数和代码包unsafe中的函数,针对其他函数和方法的调用表达式和针对通道类型值的接收表达式都可以出现在语句上下文中。换名话说,它们都可以称为表达式语句。在必要时,我们还可以使用圆括号“(”和“)”将它们括起来。其他的简单语句还包括发送语句、自增/自减语句和空语句”。

看懂没有?若没有看懂就算了,本书作者定义严谨,造成没有接触过的人很难理解 :)


       下面引用作者的一个例子:

if 100 < number{
    number++
} else {
    number--
}

       可能没有接触过的编程语言的小伙伴会问,怎么还有else呀?这还算不算if语句,哥告诉你,这才是正宗的,别无二家,回到原书内容。“可能读者已经注意到了,其中的条件表达式100<number并没有被圆括号括起来。实际上,这也是Go语言的流程控制语句的特点之一。另外,跟在条件表达式和else关键之后的两个代码块必须由“{”和“}”括起来。这一点是强制的,不论代码包含几条语句以及是否包含语句都是如此”。

       这些东西没有必要特别记忆,多写几个例子,自然而然您就知道什么是正确的什么是错误的。

       “上面示例中,有两个特殊符号:++和--。它们分别代表了自增语句和自减语句。注意它们并不是操作符。++的作用是把它左边的标识符代表的值与无类型常量1相加并将结果再赋给左边的标识符,而--的作用则是把它左边的标识符代表的值与无类型常量1相减并将结果再赋给左边的标识符。也就是说,自增语句number++与赋值语句number=number+1具有相同的语义,而自减语句number--则与赋值语句number=number-1具有相同的语义。另外,在++和--左边的并不仅限于标识符,还可以是任何可被赋值的表达式,比如应用在切片类型值或字典类型值之上的索引表达式"。


【更多惯用法】:

这有点类似英语常用对话300句。

func Open(name string) (file *File, err error)

       这个就是常见的函数惯用用法,该函数来自标准库中os包。

       由于在Go语言中一个函数可以返回多个结果,因此我们常常会把函数执行的错误也作为返回结果之一,该函数表达意思是说,您指定一个文件路径让Go语言帮您把文件内容读出来,为了读出文件内容,该函数返回您文件的句柄(即第一个参数),同时也返回一个错误(即第二个参数),用以表达在打开文件时是否发生了错误。具体怎么用呢?

f, err := os.Open(name)

if err != nil {
     return err
}

绕了半天后,还是回到if语句上。“总之,if语句常被用来常规错误”。

       “在通常情况下,我们应该先云检查变量err的值是否为nil,如果变量err的值不为nil,那么就说明os.Open函数在被执行过程中发生了错误。这时的f变量的值肯定是不可用的。这已经是一个约定俗成的规则了”。

        “另外,if语句常被作为卫述语句。卫述语句是指被用来检查关键的先决条件的合法性并在检查未通过的情况下立即终止当前代码块的执行的语句。其实,在上一个示例中的if语句就是卫述语句中的一种。它在有错误发生的时候立即终止了当前代码块的执行并将错误返回给外层代码块”。

       通过理解一下这段蓝色的文字,通常我们写程序是这样的:

func update(id int, deptment string) bool {
     if id <=0 {
          return false
     }
     // 省略若干行
     return true
}

这个没毛病,update函数开始处的那个if语句就是卫述语句。该函数可以稍改造一下:

func update(id int, deptment string) error {
     if id <=0 {
         return errors.New("The id is INVALID!")
     }
     // 省略若干行
     return nil
}

下面这个update返回结果不再是bool值,而是error值,它可以表示在函数执行期间是否发生了错误,而且还可以体现出错误的具体描述。


4.1.3 switch语句

       switch语句与if语句类似,都是一种多分支执行语句,刚接触编程的人可能有疑惑,为什么要提供两种呢?我是否只用一个就可以了?

       当然,您只用其中之一就足够了,为什么要提供两种呢?简单理解还是惯用习惯吧。

§1.  组成和编写方法

    “switch可以使用表达式或者类型说明符作为case判定方法。因此switch语句也就可以分为两类:表达式switch语句和类型switch语句。在表达式switch语句中,每一个case携带的表达式都会与switch语句要判定的那个表达式(也称为switch表达式)相比较。而在类型switch语句中,每个case所携带的不是表达式而是类型字面量,并且switch语句要判定的目标也变成了一个特殊的表达式。这个特殊表达式的结果是一个类型而不是一个类型值。

       在女儿的哭声中我读这句话真的好吃力,静下心来也不难理解,看例子就好:


§2.  表达式switch语句

switch 2*3+5{
    default:
         fmt.Println("运算错误!")
    case 5 + 5:
         fmt.Println("结果为10.")
    case 5 + 6:
         fmt.Println("结果为11.")
}

       快看,快看,switch后面的2*3+5是一个表达式,第一个case后面的5+5也是一个表达式,第二个case后面的5+6也是一个表达式,所以这是一个典型的表达式switch语句。

       在表达式switch语句中,switch表达式和case携带的表达式都会被求值。

       程序运行时,先找计算第一个case后面的表达式得到10,然后与switch的表达式值11进行比较,发现10≠11,接着计算第二个case后面的表达式得到11,然后与switch的表达式值11进行比较,发现两者相同,打印出“结果为11.”后就退出该代码块。

switch content {
    default:
        fmt.Println("Unknown Language.")
    case "Python":
        fmt.Println("An Interpreted Language.")
    case "Go":
        fmt.Println("A Compiled Language.")
}

       这也是一个表达式switch语句,您可能会想这都没有计算,怎么是一个表达式呢?

       姐,表达式不仅仅是数学运算,字符串也是哟,如果您实在感觉不顺眼,改造一下:

switch content := getContent(); content {
    default:
        fmt.Println("Unknown Language.")
    case "Python":
        fmt.Println("An Interpreted Language.")
    case "Go":
        fmt.Println("A Compiled Language.")
}

      在这个示例中,switch语句先调用getConent()函数,并且把它的结果赋给了新声明的变量content,后面紧接着的就是对content的值进行判定。看着像是表达式switch吗?


     “现在来看case语句。一条case语句由一个case表达式和一个语句列表组成,并且这两者之间需要用冒号“:”分隔,在上例的switch语句中,一共有3个case语句,注意default case是一种特殊的case语句。

     “一个case表达式由一个case关键字和一个表达式列表组成。注意,这里说的是一个表达式列表,而不是一个表达式。这意味着,一个case表达式中可以包含多个表达式。现在,我们利用这一特性来改造一下上面的switch语句:

switch content := getContent(); content {
    default:
        fmt.Println("Unknown Language.")
    case "Python", "Ruby":
        fmt.Println("An Interpreted Language.")
    case "Go", "Java", "C":
        fmt.Println("A Compiled Language.")
}

其中"Python"和"Ruby"形成一个表达式列表放到了case后面,同理"Go"、"Java"和"C"也形成一个表达式列表放到了另一个case后面。


由于Go语言有一个fallthrough关键字,所以上面示例可改造如下:

switch content := getContent(); content {
    default:
        fmt.Println("Unknown Language.")
    case "Python":
        fallthrough
    case "Ruby":
        fmt.Println("An Interpreted Language.")
    case "Go", "Java", "C":
        fmt.Println("A Compiled Language.")
}

当content内容为"Python"时,尽管会匹配"Python"对应的case语句,但由于fallthrough关键字的存在,它让程序穿越它而向下执行,所以会打印“An Interpreted Language.”,但一定要记住的是fallthrough只能穿越一次。


§3.  类型switch语句

先看个示例吧:

switch v.(type){
    case string:
        fmt.Printf("The string is ‘%s‘.\n", v.(string))
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
        fmt.Printf("The integer is %d.\n", v)
    default:
        fmt.Printf("Unsupported value. (type=%T)\n", v)
}

仔细看case后面的表达式,都是string, int, int8, int16等Go语言的类型,所以所谓类型switch语句就是对类型进行判定,而不是对值进行判定,其他方面与表达式switch一般无二。


把这个代码跑通,需要补充点关于v的内容:

var v interface{} = "aaabbb"

注意这里是把v定义为接口,而非string,即var v string = "aaabbb",如果真的这样定义了v,那么Go的编译器就会抛个异常给你看,并说:“我靠,你都知道是什么类型了,还让switch判断,这是耍我玩呀!”。


现在我们来具体分析这段示例代码,这个switch语句共包含了3条case语句。

> 第一条case语句的表达式包含了类型string字面量,这意味着如果v的类型是string类型,那么该分支就会被执行。在这个分支中,我们使用类型断言表达式v.(string)把v的值转换成了string类型的值,并以特定格式打印出来;

> 第二条case语句中的类型字面量有多个,包括了所有的整数类型,这就意味着只要v的类型属于整数类型,该分支就会被执行。在这个分支中,我们并没有使用类型断言表达式把v的值转换成任何一个整数类型的值,而是利用fmt.Printf函数直接打印出了v所表示的整数值;

> 如果v的类型既不是string类型也不是整数类型,那么default case的分支将会被执行,并使用标准输出打印v的动态类型(%T)。


需要特别注意的是:fallthrough语句不允许出现在类型switch语句中的任何case语句的语句列表中


最后,值得特别提出的是,类型switch语句的switch表达式还有一种变形写法。

var v interface{} = "aaabbb"
switch i := v.(type) {
    case string:
        fmt.Printf("The string is ‘%s‘.\n", i)
    case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
        fmt.Printf("The integer is %d.\n", i)
    default:
        fmt.Printf("Unsupport value. (type=%T)\n", i)
}

请注意switch表达式位置上的i := v.(type),这实际上是一个短变量声明,当存在这这种形式的switch表达式的时候,就相当于这个变量i被声明在了每个case语名的语句列表的开始处。在每个case语句中,变量i的类型都是不同的,它的类型会和与它处于同一个case语句的case表达式包含的类型字面量所代表的那个类型相等。例如,上面的示例中第一个case语句相当于:

case string:

        i := v.(string)

        fmt.Printf("The string is ‘%s‘.\n", i)


是不是再次被作者的富有九曲十折的表达方式折服?其实作者想表达的意思,简单来说是这样的:

switch v.(type){

     case string:

            fmt.Printf("The string is ‘%s‘.\n", v.(string))

}

如果switch表达式只是取变量v的类型,那么在case语句中必须把变量v进行强制类型转换;


switch i := v.(type){

     case string:

            fmt.Printf("The string is ‘%s‘.\n", i)

}

如果switch表达式中有对变量v的类型赋值给i,那么当进入某个case语句中时,相当于变量i在每个case语句中都有一次具体的类型转换,switch那么可以把它理解为模板。


好吧,如果越说越胡涂,请您暂时记住这两种用法就好,随着代码写的越来越多,就会逐步明白的。


§4. 更多惯用法

好了,又到了常用英语300句了 :)

在不少情况下switch表达式是缺省掉的,即:

switch{
     case number < 100:
          number++
     case number < 200:
          number--
     default:
          number -= 2
}

看这里的switch表达式消失了,此种情况下该switch语句的判定目标被视为布尔值true,也就是说,所有case表达式的结果值都应该是布尔类型,所以才有switch代码块中每个case语句都是number在与数值进行比较,以获得布尔值。

本文出自 “青客” 博客,转载请与作者联系!

以上是关于读《Go并发编程实战》第4章 流程控制方式的主要内容,如果未能解决你的问题,请参考以下文章

全流程开发 GO实战电商网站高并发秒杀系统

《Go并发编程实战》第2版 紧跟Go的1.8版本

《Go并发编程实战》第2版 紧跟Go的1.8版本号

[读书笔记]Java编程思想

Java并发编程实战 04死锁了怎么办?

Java并发编程实战 04死锁了怎么办?