我要偷偷的学Go语言,然后惊呆所有人(第三天)

标题无意冒犯,就是觉得这个广告挺好玩的
上面这张思维导图喜欢就拿走

目录

基本的程序结构

众所周知的基本程序结构有顺序分支循环

顺序就不再赘述,接下来讲解基本常见的分支和循环的书写和常用模式。

5.1 常见的分支程序结构和模式:

1. if-else

这是所有程序员都逃不开的结构,这实在是太重要了。if-else背后所潜藏的思想是非常深刻的。除了True,就是False。也就是对于全集取子集和补集,众所周知:世界是连续的,而计算机是离散的。而if-else背后的集合和真值的思想正是离散数学中的概念。

结论:if-else将连续的世界离散化的重要发明

普通的if-else长这个样子:

if condition_1 { // condition_1的值一定是布尔类型才可以,Go语言的if语句条件非常苛刻,不接受任何布尔类型之外的值
    // ...some code here
} else if condition_2 {
    // ...some code here
} else {
    // ...some code here
}

由于Go语言if语句的严苛,我们不可以这么写:

if 1 {
} else {
}     // 错

if "string" {
} else {
}     // 错

if !nil {
} else {
}     // 错

多个返回值的(或者需要预处理语句的)if-else是这个样子的:(之前的类型map介绍中已经提及过了)

if result, statu := function(); statu { // 在条件语句中的子语句是生效的,但是作用域和生命周期仅限于本语句的作用域
    // ...some code here, using result  // 使用分号``;``来分割判定的表达式和产生表达式值的语句
} else {
    // ...some code here
}
// can not use result

在Go语言的规范中,值是否存在,调用是否产生错误,产生的值都是习惯性的放在返回结果的最后一个,因此判断时的习惯也是一样的。

2. switch-case

为了避免众多的if-else if-else来进行大量的值的判断。人们发明了开关语句,也就是经典的switch-case-default语句。

大多数语言的switch-case语法都是相同的,但是在使用Go语言的switch-case语句时,需要小心,因为Go语言的switch-case语句是默认每个case后面自带break的,只是隐藏了而已。

switch value {
case value_1:
    // ...some code here
    // 这里有一个隐藏的break,也就是说不会继续执行下一个case
case value_2:
    // ...some code here
    break // 但是这里就算加了break也不会有问题
case value_3:
    // ...some code here
    fallthrough // fallthrough语句可以突破默认的break,可以继续进入下个case
default:
    // ...some code here
}

在Go语言中,大家都乐于使用在类型上没那么严格的接口,但是回过头来又要使用对应的强类型,这就要用到类型断言,在别的语言中也很常见的一种语法。而在Go语言这样常见的环境下,更是有对应的switch-type模式:

switch value := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

3. select-case

select-case是Go语言特有的语法,用于选择不同的通信操作。哪个通信操作最先不阻塞(收到结果),则最先进入哪个case,相比较switch-case而言,select-case的判断是无序的。语法如下:

select {
case communicationClause1:
    // ...some code here
case communicationClause2:
    // ...some code here
default:
    // ...some code here
}

在真实的使用场景中,常常和for一起组合为for-select使用,这些将会在后面介绍。

5.2 常见的循环程序结构和模式:

在Go语言中,表示循环的关键字就只有for。而for中可以有三种循环的模式:

正常的for循环

正常的for循环遵循以下格式:

for initial; condition; after { // 和基本所有语言中的模式都是一样的
    // ...some code here
}

for i:=0; i<10; i++ {}          // 比如这样的

while形式的for

Go语言中的while循环也使用for关键字来实现:

for condition {
    // ...some code here
}

死循环

永真循环,也就是死循环,你可以使用while循环形式来实现,也可以用普通的形式来实现。当然也可以用Go语言提供的方式来实现。

for ;true; {} // 使用普通的for循环形式
for true {}   // 使用while循环的形式实现
for {}        // 使用Go语言提供的死循环的形式

扩展阅读:


开始简单的并发

如果你是一个已经有了一定开发经验的程序员,那么你在接触Go语言之前会觉得编写并发代码是一件非常麻烦的事情:线程的管理,线程之间的通信等等,由于没有语言原语的支持,编写并发代码都是建立在大神编写的并发框架的代码之上的,正常情况下无法触碰到底层的具体逻辑和实现。

但是既然你现在开始接触神奇的Go语言了,你大可放下这方面的顾虑:因为Go语言提供了大量的并发原语来支持从零开始的并发代码开发。接下来就使用最简单的例子开始一个并发代码的编写:

package main

import "fmt"

func main() {
    go func() { // 使用go关键字来开启一个goroutine
        fmt.Println("Hello~Goroutine~")
    }()
}

上面这段代码已经是一个可以并发的代码了:如果你运行它,你会发现有时候并没有办法输出"HelloGoroutine"这段文字,这是因为打印文字和主函数已经是并发运行的了。

在Go语言中,包括主函数的程序运行在名为goroutine轻量级进程上,而使用go关键字可以开启一个新的goroutine,从而达到并行的效果。因此在上面的一段代码中,主goroutine和匿名函数所在的goroutine是并行的,因此会存在两种情况:①主goroutine先结束 ②文字被先打印出来。

Goroutine是轻量级的,因此在Go语言的代码中你可以开启成千上万的goroutine,这在其他语言中是很难做到的。而进程之间的通信就交给Go语言的channel类型来完成(前面有介绍)。

package main

import "fmt"

func main() {
    done := make(chan struct{}) // 定义一个空接口体类型的通道
    go func() {                 // 开启一个goroutine
        fmt.Println("Hello~Goroutine~")
        done<- struct{}{}       // 告诉done已经完成了工作
    }()                         // 匿名函数声明的同时进行调用
    <-done                      // 在通道内没有值之前,会堵塞在这里,因此程序不会在打印前结束
    close(done)                 // 记得关闭通道
}

现在使用了通道在goroutine之间进行通信,我们可以很好的来进行goroutine之间的同步了。

使用通信来进行goroutine之间的同步操作是基于CSP(通信顺序进程)的思想。但是很多人会更加习惯于去使用来进行同步,但是我认为锁是面向资源的,并不适合用于同步不同的线程或goroutine。

// 使用锁的例子
package main

import (
    "fmt"
    "sync"
)

func main() {
    lock := new(sync.Mutex) // 声明一个锁
    lock.Lock()             // 锁上
    go func() {
        fmt.Println("Hello~Goroutine~")
        lock.Unlock()       // 开锁
    }()
    lock.Lock()             // 锁上,但是在锁是打开的之前也会阻塞
}

但是在同步goroutine的方面,还是使用通道来进行通信是比较推荐的方式。

扩展阅读:


Go语言中的函数

7.1 函数声明

函数的组成部分有名字, 形参列表, 可选的返回列表, 函数体

func functionName(parameter-list) (result-list) {
    // body
}

我相信在前面写过的不少示例中,大家已经熟悉了这种声明方式。这是最普通,也是最基础的声明方法。但是Go语言中的函数还有和其他语言不同的地方。

当形参列表中的几个连续类型都相同时,可以选择只写一个类型标识符。

func min(x, y int) int
func min(x int, y int) int // 二者是完全相同的

使用4种方式来声明具有同样函数签名的函数:

func add(x int, y int) int { return x+y }      // 正常的声明
func sub(x, y int) (z int) { z = x-y; return } // 在返回值列表写好变量名,在返回时会自动返回该变量
func first(x int, _ int)   { return x }        // 下划线强调未使用
func zero(int, int)        { return 0 }        // 不使用参数的情况

Go语言中的函数声明是非常灵活的。

7.2 递归

和所有的语言一样。Go语言也支持函数的递归调用,也就是自身调用自身。

func dfs(node *TreeNode) { // 一个常见的对于二叉树的中序遍历的例子
    if node.Left != nil {
        dfs(node.Left) 
    } // 调用自身
    // use this node
    if node.Right != nil { 
        dfs(node.Right) 
    }
}

整个例子,自己实现一下是很有趣的:

package main

import "fmt"

type TreeNode struct {
    Val    int
    Left   *TreeNode
    Right  *TreeNode
}

func dfs(node *TreeNode) {
    if node.Left != nil { 
        dfs(node.Left) 
    }
    fmt.Println(node.Val)
    if node.Right != nil { 
        dfs(node.Right) 
    }
}

func main() {
    // construct a tree here
    dfs(root)
}
7.3 多个返回值

现在能够支持多个返回值的语言已经非常普遍了,如Python等等。Go语言也是支持多个返回值的,尤其是在错误处理方面,不像其他语言的try-except或者try-catch模式,而是直接在返回值中加入err的值。

下面是摘自strconv包的几个函数原型:

func ParseBool(str string) (bool, error)
func ParseFloat(s string, bitSize int) (float64, error)
func ParseInt(s string, base int, bitSize int) (i int64, err error)
func ParseUint(s string, base int, bitSize int) (uint64, error)

可以很明显的看出来,由于从字符串解析不同的类型时,经常会出现无法解析,或者解析失败的情况,开发者们在内置函数的最后一个返回值放上了错误信息。而判定的方式也是非常简单的,因为error类型可以和nil进行比较:

_, err := strconv.Atoi("&&")
if err != nil {
    // error handle
}

所以当我们编写自己的函数的时候也要注意返回错误:

func yourHandler(str string) (int, error) {
    val, err := strconv.Atoi(str)
    if err != nil {
        return 0, err // 将错误返回上层
    }
    return val, nil   // 没有错误时返回nil
}
7.4 函数变量

函数变量在前面的派生类型中已经介绍过了,这里只是简单写一下用法实例。

函数变量的零值nil,因此函数变量可以和nil进行比较,但是函数变量之间本身是不可以互相比较的!

var f func(int) int  // 变量f为nil
f(4)                 // 调用一个为零值的函数会出错宕机

if f != nil {
    f(4)             // 判断函数是否为空
}

现在在Go语言中,由于现在还不支持泛型程序设计,导致函数变量的地位还不是很高。但是在Go2出现泛型之后,函数变量的地位一定会飙升。

7.5 匿名函数

在Go语言中,匿名函数是非常常见的,除了方便撰写小规模的函数之外,还因为Go语言中的go关键字后面仅允许接函数,这就导致了匿名函数的大量使用。

普通的使用匿名函数:

x, y := 10, 20
fmt.Println(func(a, b int) int {
    if a > b {
        return a
    }
    return b
}(x, y))

结合闭包的特性使用匿名函数:

func squares() func() int {
    var x int                 // 在返回的匿名函数的眼中,这是一个全局变量,这是由于闭包的性质决定的
    return func() int {
        x++
        return x*x
    }
}

func main() {
    f := squares()
    fmt.Println(f(), f(), f(), f()) // 1, 4, 9, 16
}
7.6 变长函数

变长函数被调用的时候可以有可变的参数个数。最常见的就是fmt.Println(),那么如何声明一个变长函数呢?

在参数列表的,最后的类型名称之前使用省略号...,表示声明一个变长函数,调用这个函数的时候可以传递该类型任意数目的参数。

func sum(vals ...int) int {
    tot := 0
    for _, val := range vals { // 在变长函数中,以slice的形式组织多个相同类型的参数
        tot += val
    }
    return tot
}
7.7 延迟函数调用

所谓延迟调用,其实就是讲解一下defer关键字的作用。

有的时候,一些操作是需要成对进行的,比如内存的声明和释放(malloc、new和free),文件的打开和关闭(fopen和fclose),数据库的连接和释放等等。

在编写代码的时候,忘记,或者配对的语句失效的情况是很常见的:比如程序直接panic了,那么接下来的语句也不会执行了。如果你学习过Python语言,那么你一定知道with语句和上下文的概念(Python使用这种方式来确保能够让程序正常运行),而在Go语言中,使用了defer关键字来达到这种目的。

defer关键字的作用就是延迟目标函数的调用,在该语句的作用域退出前再进行执行。

比如:(对之前的示例进行改造)

func main() {
    ch := make(chan struct{})
    defer close(ch)           // 相比之前的结构,我们现在可以使用更舒适的逻辑来编写程序
    
    go func() {
        fmt.Println("Hello, golang!")
        ch<- struct{}{}
    }
    <-ch
    // close(ch)实际会在这个位置执行
}

为了证明即使宕机了也可以执行defer,我们做个实验:

func main() {
    defer fmt.Println("I'am defer.")
    
    f := func(int) int
    f(4)
}

多个defer语句执行的顺序是从后到前的,因为是嘛。

7.8 宕机和恢复

宕机和恢复,其实是讲panic()recover()这两个Go语言的内置函数。

1. panic

当你的程序遇到不可操控的错误时,你的程序会宕机,也就是panic,当然我们可以自己去panic,这样往往比Go语言自动的panic更好也更可控,也更利于trouble-shooting,使用了panic函数之后你的程序会立刻退出。

func main() {
    // ...do something
    if err != nil { // error can not handle
        panic(err)  //
    }
    // normal logic
}

2. recover

退出程序通常是正常处理宕机的方式,但是也有例外,在一定情况下是可以进行恢复的,比如在web开发时,一个具体的handler内部出现了足以让程序宕机的错误,但是这个时候应该做的是返回一个500的状态,而不是一直阻塞在那里(程序都崩溃了怎么给response啊)。

如果内置的recover函数在延迟函数的内部调用,而且这个包含defer语句的函数发生了宕机,recover会终止当前的宕机状态并且返回宕机产生的值。函数不会在宕机的位置继续运行,而是正常返回。

例如:

func main() {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("Internal Error: %v", p)
        }
    }()
    // ...some code here
    // 如果这里发生了宕机,会在defer语句中执行错误的输出
}

扩展阅读:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小夕Coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值