Go学习笔记二

Go语言中的面向对象编程

  • 可见性控制
    publlic 常量、变量、类型、接口、结构、函数等的名称大写
    private 非大写就只能在包内使用
  • 继承
    通过组合实现,内嵌一个或多个struct
  • 多态
    通过接口实现,通过接口定义方法集,编写多套实现

json 编解码

  • json.Unmarshal():从string转换至struct
  • json.Marshal():从struct转换至string
func unmarshal2Struct(humanStr string) Human {
        h := Human{}
        err := json.Unmarshal([]byte(humanStr), &h)
        if err != nil {
            println(err)
        }

        return h
}

func marshal2JsonString(h Human) string {
        h.Age = 30
        updatedBytes, err := json.Marshal(&h)
        if err != nil {
              println(err)
        }
        return string(updatedBytes)
}
  • json包使用map[string]interface{} 和 []interface{} 类型保存任意对象
  • 可通过如下逻辑解析人意json
var obj interface{}
err := json.Unmarshal([]byte(humanStr), &obj)
objMap, ok := obj.(map[string]interface{})
for k, v := range objMap {
        switch value := v.(type) {
              case string:
                        fmt.Printf("type of %s is string, value is %v\n, k, value)
              case interface{}:
                        fmt.Printf("type of %s is interface{}, value is %v\n, k, value)
              default:
                        fmt.Printf("type of %s is wrong, value is %v\n, k, value)
        }
}

错误处理

  • Go 语言无内置 exception 机制,只提供error接口定义错误
type error interface {
        Error() string
}
  • 可通过 errors.New 或 fmt.Errorf 创建新的 error
var errorNotFound error = errors.New("NotFound")
  • 通常应用程序对error的处理大部分是判断 error 是否为 nil

如需将error归类,通常交给应用程序自定义,比如kubernetes自定义了与apiserver交互的不同类型错误

type StatusError struct {
        ErrStatus metav1.Status
}
var _error = &StatusError{}

// Error implements the Error interface
func (e *StatusError) Error() string {
          return e.ErrStatus.Message
}

defer

  • 函数返回之前执行某个语句或函数,等同于java和c#的finally
func main() {
        defer fmt.Println("1")
        defer fmt.Println("2")
        defer fmt.Println("3")
        time.Sleep(time.Second)
}
// 321 类似于一个栈,反序执行
  • 常见的defer使用场景:记得关闭你打开的资源
    defer file.Close()
    defer mu.Unlock()
    defer println("")
  • 循环死锁
lock := sync.Mutex{}
for i := 0; i < 3; i++ {
      lock.Lock() // 第一次循环之后,defer压栈,第二次循环时锁没有释放
      defer lock.Unlock()
      fmt.Println("loopFunc:", i)
}
  • 解决循环死锁
lock := sync.Mutex{}
for i := 0; i < 3; i++ {
      go func(i int) { // 使用闭包,让函数推出,释放锁
              lock.Lock()
              defer lock.Unlock()  // 如果不用defer,而该做lock.Unlock()时,一旦逻辑出现错误,程序会报错,lock.Unlock()不会执行,锁资源就不会释放,所以defer可以用来确保锁资源一定会被释放掉
              fmt.Println("loopFunc:", i)
      }(i)
}
// 321

panic 和 recover

  • panic可在系统出现不可恢复错误时主动调用panic,panic会使当前线程crash
  • defer保证执行并把控制权交还给接收到panic的函数调用者
  • recover,函数从panic或错误场景中恢复
defer func() {
        fmt.Println("defer func is called")
        if err := recover(); err != nil {
                fmt.Println(err)
        }()
}
panic("a panic is triggered")

defer一定是和recover配合使用,defer用来保证panic以后,关联的代码仍然可执行,防止线程的crash导致整个进程crash,recover保证从当前的panic状态恢复过来。一般来说,panic错误需要recover。

多线程

并发和并行

  • 并发:两个或多个事件在同一时间段内交替发生(多个线程在同一CPU上来回切换、交替执行)
  • 并行:两个或多个事件在同一时刻发生(多个CPU同时运行多个线程)

协程

  • 进程:分配系统资源(CPU时间、内存等)的基本单位;有独立的内存空间,切换开销大
  • 线程:线程的一个执行流,是CPU调度并能独立运行的基本单位;多线程通信方便;从内核层面来看线程其实也是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共享了地址空间和信号处理函数;
  • 协程:Go语言中的轻量级线程实现;Golang在runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前的goroutine的CPU(P)转让出去,让其他的goroutine能被调度并执行,也就是Golang从语言层面支持了协程;

备注: 进程是系统分配资源的基本单位,线程是CPU调度的基本单位,一个进程可以启动多个线程、从而使用多个CPU进行计算;进程和线程都在操作系统上体现的;

CSP

  • Go语言多线程的模型叫做CSP(communicating sequential process),是描述两个(多个)独立的并发实体通过共享的通讯channel进行通信的并发模型
  • Go 协程 goroutine 是一种轻量级线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度;是一种绿色线程,微线程,它与coroutine协程也有区别,能够在发现堵塞后启动新的微线程
  • 通道channel,类似Unix 的 Pipe,用于协程之间的通讯和同步;协程之间虽然解耦,但是它们和channel有着耦合

线程和协程的差异

  • 每个goroutine(协程)默认占用内存远比Java、C的线程少 • goroutine:2KB 线程:8MB
  • 线程goroutine切换开销方面,goroutine远比线程小;线程涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新;goroutine:只有三个寄存器的值修改 - PC / SP / DX;
  • GOMAXPROCS:控制并行线程数量

协程示例

func main() {
        loopFunc()
        time.Sleep(time.Second)
}

func loopFunc() {
        for i := 0; i < 3; i++ {
                // 如果不加goroutine,是在一个CPU上处理的,
                // 输出012,加了goroutine,就在多个CPU上处理,
                // 并且谁先执行,谁后执行就说不好了,输出结果会乱掉,
                // 可能是102
                go fmt.Println("loopFunc:", i)  // 加了go,就是告诉go语言,这段程序需要启动一个新的线程
        }
}

channel 多线程通信

  • channel是多个协程之间通信的管道;一端发送数据,一端接收数据;同一时间只有一个协程可以访问数据,无共享内存模式可能出现的内存竞争问题;可以协调协程执行的顺序;
// 示例
ch := make(chan int)
go func() {
        fmt.Println("hello from goroutine")
        ch <- 0 // 数据写入channel
}()
i := <-ch // 从channel中取数据并赋值
  • Go语言可以理解成自带生产者和消费者的支持

通道缓冲

  • 基于channel的通信是同步的,当前缓冲区满时,数据的发送是阻塞的;通过make关键字创建通道时可定义缓冲区容量,默认缓冲区容量为0
ch := make(chan int)   // 如果没有接收,会阻塞,因为默认缓冲区为0
ch := make(chan int, 1)
  • 使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道
  • 只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量
  • 当向通道中发送完数据时,我们可以通过close函数来关闭通道
  • 通道无需每次都关闭
  • 关闭的作用是告诉接收者该通道再无新数据发送
  • 只有发送方需要关闭通道
  • 通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的
  • 关闭后的通道有以下特点:
    1. 对一个关闭的通道再发送值就会导致panic。
    2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
    3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
    4. 关闭一个已经关闭的通道会导致panic。
    5. 只有发送方需要关闭通道
ch := make(chan int)
defer close(ch) // 重点关注这里
if v, notClosed := <-ch; notClosed {
        fmt.Println(v)
}
  • 判断通道关闭的两种方式:
// 方式一
go func() {
    for {
        i, ok := <-ch1 // 通道关闭后再取值ok=false
        if !ok {
            break
        }
        ch2 <- i * i
    }
    close(ch2)
}()
// 方式二
for i := range ch2 { // 通道关闭后会退出for range循环
    fmt.Println(i)
}

// 通常使用的是for range的方式。使用for range遍历通道,当通道被关闭的时候就会退出for range
  • 单向通道:有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收;Go语言中提供了单向通道来处理这种情况
// chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
// <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
// 在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}
  • 生产者-消费者示例
var c = make(chan int)
go prod(c)
go consume(c)

func prod(ch chan<- int) { // 把双向通道转换成单向通道
        for {ch<-}
}

func consume(ch <-chan int) {// 把双向通道转换成单向通道
        for {<-ch}
}

select

-当多个协程同时运行时,可通过select轮询多个通道,如果所有通道都堵塞则等待,如定义了default则执行default;如果多个通道就绪则随机选择;

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

for{
    // 尝试从ch1接收值
    data, ok := <-ch1
    // 尝试从ch2接收值
    data, ok := <-ch2
    …
}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

select{
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        默认操作
}

举个小例子来演示下select的使用:

func main() {
    ch := make(chan int, 1)
    for i := 0; i < 10; i++ {
        select {
        case x := <-ch:
            fmt.Println(x)
        case ch <- i:
        }
    }
}

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有case的select{}会一直等待,可用于阻塞main函数。

定时器 Timer

  • time.Ticker 以指定的时间间隔重复的向通道C发送时间值;使用场景:为协程设定超时时间
timer := time.NewTimer(time.Second)
select {
        case <-ch:
                fmt.Println("received from ch")
        case <- timer.C
                fmt.Println("timeout waiting from channel ch")
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • interface 类似于c++多态,简单的说,interface是一组method签名的组合,我们通过inter...
    明明就_c565阅读 259评论 0 0
  • 将两个(或更多)语句放在一行书写,它们 必须用分号 (’;’) 分隔。一般情况下,你不需要分号。 init函数和m...
    涵仔睡觉阅读 3,781评论 0 8
  • go面试题学习笔记[https://mp.weixin.qq.com/mp/homepage?__biz=MzAx...
    张清柏阅读 521评论 0 2
  • 1.for range结合指针如下写法输出的*v都是m的最后一个valuefor k,v := range m {...
    javid阅读 559评论 0 0
  • 格式化 gofmt是一个cli程序,会优先读取标准输入,如果传入了文件路径的话,会格式化这个文件,如果传入一个目录...
    半亩房顶阅读 256评论 0 2