Go语言编程思想

进入工程化阶段,我们接下来要面对的不仅仅是代码片段, 我们要学习在做工程时,利用go语言做到模块化,如何让系统可配置, 如何测试,如何用go语言进行并发。

讲述函数式编程,接口,goroutine/channel及其模式

接口,接口的值类型

本节理解的内容

type Traversal interface {
    Traverse()
}

func main() {
    traversal := getTraversal()
    traversal.Traverse()
}

`getTraversal()`定义了接口的使用方法,返回一个接口类型

接口的概念

大家背景

  • 强类型语言:熟悉接口的概念 学过强类型语言的人一般对接口概念比较熟悉。java, c++中接口是非常重要的概念
  • 弱类型语言:没(少)有接口概念。 php, python, js使用者可能没有实际用到

接口的概念如果只有文字描述,没有实例讲解是非常难理解的。

一个段子:

  • 小孩才分对错 前面几章基础部分属于小学生的概念
  • 大人只看利弊

范例:下载网页(函数) downloader.go文件

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("http://www.imooc.com")
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    bytes, _ := ioutil.ReadAll(resp.Body)
    fmt.Printf("%s\n", bytes)
}

提取功能:retrieve函数负责功能实现,main函数调用retrieve函数负责输出结果

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func retrieve(url string) string {
    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    bytes, _ := ioutil.ReadAll(resp.Body)
    return string(bytes)
}

func main() {
    fmt.Println(retrieve("http://www.imooc.com"))
}

范例:团队协作,下载网页(接口引入) 目录结构,低偶合

D:.
│  downloader.go   // main函数,使用接口
│  go.mod
│
├─infra      //infra包,基础架构团队,负责网络请求、磁盘读写、数据库等
│       urlretriever.go  // 接口方法的实现
└─testing    //testing包,测试团队
        retriever.go     // 接口方法的实现

#infra/urlretriever.go 文件代码
package infra

import (
    "io/ioutil"
    "net/http"
)

type Retriever struct{}

// 定义方法
func (Retriever) Get(url string) string { // 接收者(Retriever)里什么也没有,这里不需要定义名字,也用不到
    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    bytes, _ := ioutil.ReadAll(resp.Body)
    return string(bytes)
}

#testing/retriever.go 文件代码
package testing

type Retriever struct {}

func (Retriever) Get(url string) string {
    return "fake content" // 返回假数据
}

# downloader.go 文件代码
package main

import (
    "fmt"
    "learngo/testing"
)

// 演变1
// 把retriever绑定到 infra.Retriever上,看上去更像是配置还不像逻辑,这里把它提出来写一个函数
// func main() {
//  retriever := infra.Retriever{}
//  fmt.Println(retriever.Get("http://www.imooc.com"))
// }

// 演变2
// retriever逻辑化 var retriever infra.Retriever = getRetriever()
// 缺点 retriever 一定是 infra.Retriever 类型
// func getRetriever() infra.Retriever {
//  return infra.Retriever{}
// }

// func main() {
//  var retriever infra.Retriever = getRetriever()
//  fmt.Println(retriever.Get("http://www.imooc.com"))
// }

// 演变3
// 换一个retriever,使用测试团队的方法
// 要在原来基础上改很多地方
// func getRetriever() testing.Retriever {
//  return testing.Retriever{}
// }

// func main() {
//  var retriever testing.Retriever = getRetriever()
//  fmt.Println(retriever.Get("http://www.imooc.com"))
// }

// 演变4 接口引入
// go语言是强类型的
// 需要代码和逻辑一致
// var retriever ? = getRetriever() 其中?是一个东西,可以Get(string)的东西
// 这个?问号怎么描述,这就是我们的接口
func getRetriever() retriever {
    return testing.Retriever{} // 测试团队代码通过,可换成正式代码 return infra.Retriever{}
}

// ?Something that can "Get"
type retriever interface { // 这个接口可以Get
    Get(string) string
}
func main() {
    var r retriever = getRetriever() // 这个东西是什么?是getRetriever()
    fmt.Println(r.Get("http://www.imooc.com"))
}

接口是一个抽象的概念,没有具体干什么,只要里面有调用的方法就可以

duck typing的概念

大黄鸭是鸭子吗?

  • 传统类型系统:脊索动物门,脊椎动物亚门,鸟纲雁形目
  • duck typing:是鸭子
    • “像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子” –原话
    • 描述事物的外部行为而非内部结构
    • 严格说go语言属于结构化类型系统,类似duck typing
      • duck typing中说一定要动态绑定,但go语言是编译就绑定的

其它语言中的duck typing

python中的duck typing

def download(retriever):
   return retriver.get("xx.com")

retriver是用来获取资源的,retriver就是duck typing中的对象,download是使用者使用了 duck typing

  • 运行时才知道传入的retriever有没有get
  • 需要注释来说明接口

c++中的duck typing

template <class R>
string dowload(const R& retriever) {
    return retriever.get("xx.com");
}

通过template来支持duck typing

  • 编译时才知道传入的retriever有没有get
    • 打代码时不知道
  • 需要注释来说明接口

java中的类似duck typing的代码

<R extends Retriever>
String dowload(R r) {
    return r.get("xx.com");
}
  • 传入的参数必须实现Retriever接口
    • 没有运行时、编译时错误
  • 不是duck typing
    • 因为必须实现Retriever接口

go语言的duck typing

  • java同时需要Readable, Appendable怎么办?(appche polygence)
  • 同时具有python, c++的duck typing的灵活性
    • 什么类型都可以传,只要实现了get的方法就行
  • 又具有java的类型检查
    • 不想通过注释来说明需要什么类型

接口的定义

讲述接口定义前先理解上面代码范例中download和retriever角色是什么

使用者(download)和实现者(retriever)

  • go语言的接口是*使用者*来定义的

范例:使用者定义接口和实现者实现 定义接口

# 创建retriever/main.go 文件
package main

import "fmt"

type Retriever interface { // 定义接口Retriever,里面有个Get方法
    Get(url string) string
}

func download(r Retriever) string { // 使用者,定义函数download,使用接口r的Get方法
    return r.Get("http://www.imooc.com")
}

func main() {
    var r Retriever // r还不知道是什么,运行会出错的
    fmt.Println(download(r))
}

接下来我们就实现`Retriever interface` 实现接口:mockretriever,实现了接口中的方法就证明实现了接口

# 创建 retriever/mock/mockretriever.go文件
package mock

type Retriever struct {
    Contents string  // 内容
}

func (r Retriever) Get(url string) string { // 实现接口的方法
    return r.Contents
                                          }

# retriever/main.go文件中
func main() {
        var r Retriever
        r = mock.Retriever{"this is a fake imooc.com"} // 引用实现接口的实例
        fmt.Println(download(r))
}
或者
func main() {
    fmt.Println(download(
        mock.Retriever{
            "this is a fake imooc.com"}))
}

实现接口:真实的retriver

# 实现的retriever
# 创建 retriever/real/retriever.go文件
package real

import (
    "net/http"
    "net/http/httputil"
    "time"
)

type Retriever struct {
    UserAgent string
    TimeOut   time.Duration
}

func (r Retriever) Get(url string) string {
    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }

    result, err := httputil.DumpResponse(resp, true)

    resp.Body.Close() // 读完body信息要关闭body。http.Get: Caller should close resp.Body when done reading from it.

    if err != nil {
        panic(err)
    }

    return string(result)
}

#  retriever/main.go文件内容
package main

import (
    "fmt"
    "learngo/retriever/mock"
    "learngo/retriever/real"
)

type Retriever interface { // 定义接口Retriever,里面有个Get方法
    Get(url string) string
}

func download(r Retriever) string { // 使用者,定义函数download,使用接口r的Get方法
    return r.Get("http://www.imooc.com")
}

func main() {
    var r Retriever
    r = mock.Retriever{"this is a fake imooc.com"}
    r = real.Retriever{}
    fmt.Println(download(r))
}

接口的定义

type Retriever interface {
    Get(source string) string
}

func download(retriever Retriever) string {
    retrun retriever.Get("http://www.imooc.com")
}

接口的实现

  • 接口的实现是隐式的
    • 不需要说明它实现了哪个接口
  • 只要实现接口里的方法

接口的值类型

接口里有什么东西呢?

范例:

#  retriever/main.go文件
func main() {
    var r Retriever
    r = mock.Retriever{"this is a fake imooc.com"}
    fmt.Printf("%T %v\n", r, r) // 查看r的类型 mock.Retriever {this is a fake imooc.com}

    r = real.Retriever{
        UserAgent: "Mozilla/5.0",
        TimeOut: time.Minute,
    }
    fmt.Printf("%T %v\n", r, r) // real.Retriever {Mozilla/5.0 1m0s}

    //fmt.Println(download(r))
}


# real中Retriever定义成指针类型,通过指针访问Get
#  retriever/real/retriever.go文件
type Retriever struct {
    UserAgent string
    TimeOut   time.Duration
}

func (r *Retriever) Get(url string) string {

#  retriever/main.go文件
func main() {
    var r Retriever
    r = mock.Retriever{"this is a fake imooc.com"}
    fmt.Printf("%T %v\n", r, r) // 查看r的类型 mock.Retriever {this is a fake imooc.com}

    r = &real.Retriever{             //Retriever中Get是指针接收者
        UserAgent: "Mozilla/5.0",
        TimeOut: time.Minute,

    }
    fmt.Printf("%T %v\n", r, r) // 类型指针*real.Retriever 值也是指针&{Mozilla/5.0 1m0s}

    //fmt.Println(download(r))
}

说明 interface 里有2个东西:类型和值

  • 值可以是真实值也可以是指针。

如果是真实的值它是拷贝的,拷贝到r的肚子里。 接口几乎不会用到接口的指针,因为接口肚子里通常会含有一个指针。

接口类型的判断

取接口类型2种方法 通过switch判断,或者通过type assertion判断

#retriever/main.go文件

func main() {
    var r Retriever
    r = mock.Retriever{"this is a fake imooc.com"}
    inspect(r) // 查看r的类型 mock.Retriever {this is a fake imooc.com}
               // Contents: this is a fake imooc.com

    r = &real.Retriever{
        UserAgent: "Mozilla/5.0",
        TimeOut: time.Minute,
    }
    // 取接口类型,方法1:switch
    inspect(r)  // *real.Retriever &{Mozilla/5.0 1m0s}
               // UserAgent: Mozilla/5.0
    // 取接口类型,方法2:Type assertition
    realRetriever := r.(*real.Retriever)
    fmt.Println(realRetriever.TimeOut)

    if mockRetriever, ok := r.(mock.Retriever); ok { // 更严谨的写法
        fmt.Println(mockRetriever.Contents)
    } else {
        fmt.Println("not a mock retriever")
    }

    //fmt.Println(download(r))
}

func inspect(r Retriever) {
    fmt.Printf("%T %v\n", r, r)
    switch v := r.(type) {  // 弄清楚r的类型是什么
    case mock.Retriever:
        fmt.Println("Contents:", v.Contents)
    case *real.Retriever:
        fmt.Println("UserAgent:", v.UserAgent)
    }
}

接口变量里有什么?

接口变量里有

  • 实现者的类型
  • 实现者的值

或者 接口变量里有

  • 实现者的类型
  • 实现者的指针
    • 指向实现者

总结:

  • 接口变量自带指针 有时候带指针,当然也可以带值,通常来说自己带指针的。
  • 接口变量同样采用值传递,几乎不需要使用接口的指针。
    • 因为接口变量肚子里有一个指针
  • 指针接收者实现只能用指针方式使用;值接收者都可

查看接口变量

  • 表示任何类型:`interface{}`
  • Type Assertion
  • Type switch

范例:`interface{}`代表任何类型

type Queue []interface{}

func (q *Queue) Push(v int) {
    *q = append(*q, v)
}

func (q *Queue) Pop() int {
    head := (*q)[0]
    *q = (*q)[1:]
    return head.(int) // 强制转换成int类型
}

接口的组合

相对使用者来说的

比如: 标准库:io库

// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
    Reader
    Closer
}

// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {
    Writer
    Closer
}

// ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

范例:接口组合

# retriever/main.go
package main

import (
    "fmt"
    "learngo/retriever/mock"
)

type Retriever interface { // 定义接口Retriever,里面有个Get方法
    Get(url string) string
}

type Poster interface {
    Post(url string,
        form map[string]string) string
}

const url = "http://www.imooc.com"
func download(r Retriever) string { // 使用者,定义函数download,使用接口r的Get方法
    return r.Get(url)
}

func post(poster Poster) {
    poster.Post(url,
     map[string]string {
        "name": "xxx",
        "course": "golang",
     })
}

// 接口组合   组合 Retriever和Poster接口
type RetrieverPoster interface {
    Retriever
    Poster
}
func session(s RetrieverPoster) string {
    s.Post(url, map[string]string {
        "contents": "another faked imooc.com",
    })
    return  s.Get(url)
}

func main() {
    retriever := mock.Retriever{"this is a fake imooc.com"}

    fmt.Println("Try a session")
    fmt.Println(session(&retriever))
}

# 实现者
# retriever/mock/mockretriever.go
package mock

type Retriever struct {
    Contents string  // 内容
}

func (r *Retriever) Get(url string) string { // 实现接口的方法
    return r.Contents
}

func (r *Retriever) Post(url string,
    form map[string]string) string {
        r.Contents = form["contents"]
    return "ok"
}

常用系统的接口

fmt库Stringer接口

可以格式化输出 https://pkg.go.dev/fmt#Stringer 只要实现了String方法就可以

type Stringer interface {
    String() string
}

io库的Reader接口和Writer接口

https://pkg.go.dev/io#Reader

type Reader interface {
        Read(p []byte) (n int, err error) // 相当一个文件,实现reader的人是一个文件,它可以Read。从文件里读进东西给byte
}

type Writer interface {
        Write(p []byte) (n int, err error) // 相当一个文件,提供给你这些byte的数据把文件写进去
}
//不只是文件,网络、string,其它的一些都可以用

`os.Open`函数返回File结构,File结构就有对Read接口的实现 `fmt.Fprintf`函数中参数`io.Writer` `fmt.Fscanf`函数中第一个参数是`io.Reader`

所以把底层读写相关的东西做成Reader或者Writer就可以写许多系统函数共用,尤其是printf, scanf。

范例:函数参数多用接口,如io.Reader接口利用

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"
)

func printFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }

    PrintFileContents(file) // 把实现都实例绑定到io.Reader接口
}

func PrintFileContents(read io.Reader) { // 利用io.Reader,可读文件,字符串等
    scanner := bufio.NewScanner(read)

    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

func main() {
    printFile("loop/abc.txt")
    s := `abc"d"
    kkk
    123

    p`
    PrintFileContents(strings.NewReader(s))
    PrintFileContents(bytes.NewReader([]byte(s)))
}

函数式编程

函数式编程

函数式编程不是go语言特有的 go语言对函数式编程的支持主要体现在闭包上面

func adder() func (value int) int {
    sum := 0
    return func(value int) int {
        sum += value
        return sum
    }
}

func main() {
    adder := adder()
    for i := 0; i < 100; i++ {
            fmt.Println(adder(i))
    }
}

adder返回一个累加值

来看看函数式编程的概念

函数式编程 vs 函数指针

最大的区别

  • 函数是一等公民:参数,变量,返回值都可以是函数
    • c++中只有函数指针,java中没有办法把函数传给别人
  • 高阶函数
    • 函数的参数可以是函数
  • 函数 –> 闭包
    • 重点

“正统”函数式编程

  • 不可变性:不能有状态,只有常量和函数
  • 函数只能有一个参数

写出的程序不一定更好。

范例:用go语言写“正统”函数式编程 创建目录functional/adder,在adder目录下创建文件adder.go文件

package main

import "fmt"

func adder() func(int) int { // 累加器
    sum := 0                     // sum为自由变量, 不是函数体func(value init) int里面定义的,是外面的
    return func(value int) int { // value为局部变量 。这个函数返回了一个闭包func(value int) int内整体
        sum += value
        return sum
    }
}

// “正统”函数式编程
// 只有常量和函数
// 不能有状态sum。这里把状态放在一个新的函数中
type iAdder func(int) (int, iAdder) // 返回当前加完的值和下一个值。这是一个递归的定义

func adder2(base int) iAdder {
    return func(v int) (int, iAdder) {
        return base + v, adder2(base + v)
    }
}

func main() {
    a := adder()
    for i := 0; i < 10; i++ {
        fmt.Printf("0 + 1 ... %d = %d\n",
            i, a(i)) // a()函数里存了变量sum,所以会不断累加
    }

    a1 := adder2(0)
    for i := 0; i < 10; i++ {
        var s int
        s, a1 = a1(i)
        fmt.Printf("0 + 1 ... %d = %d\n",
            i, s) // a()函数里存了变量sum,所以会不断累加
    }
}

闭包

  • 函数体内有局部变量和自由变量

    • 自由变量:编译器追踪,不断找连接关系,直到所有连接的变量都连接到
    • 连到所有变量后,我们把这个整体叫闭包。这个函数返回了一个闭包

    其它语言对闭包的支持

python中的闭包
def adder()
    sum = 0

    def f(value):
        nonlocal sum
        sum += value
        return sum

    return f
  • python原生支持闭包
  • 使用`__closure__`来查看闭包内容
c++ 中的闭包

新c++14的语法

auto adder() {
    auto sum = 0;
    return [=] (int value) mutable {
        sum += value;
        return sum;
    };
}
  • 过去:stl或者boost带有类似库
  • c++11及以后:支持闭包
java 中的闭包
Function <Integer, Intege> adder() {
    final Holder<Integer> sum = new Holder<>(0);
    return (Integer value) -> {
        sum.value += value;
        return sum.value;
    };
}
  • 1.8以后:使用Function接口和Lambda表达式来创建函数对象
  • 匿名类或Lambda表达式均支持闭包

go语言闭包的应用

函数式编程范例1 斐波那契数列

package main

import "fmt"

// 1, 1, 2, 3, 5, 8, 13 ...
// 生成器生成斐波那契数列,返回一个无参数的函数
func fibonacci() func() int {
    a, b := 0, 1  // 记录前2个数
    return func() int {
        a, b = b, a + b
        return a
    }
}

func main() {
    f := fibonacci()  //每个数都是前2个数的和
    fmt.Println(f()) // 1
    fmt.Println(f()) // 1
    fmt.Println(f()) // 2
    fmt.Println(f()) // 3
    fmt.Println(f())
    fmt.Println(f())
    fmt.Println(f())
    fmt.Println(f())
}

函数式编程范例2 为函数实现接口

我们发现斐波那契生成的f像一个生成器一样,每次自动调用f就会生成下一个斐波数,这个东西跟文件有点像 我们使用`io.Reader`接口包装一下,把它当成文件一样读

package main

import (
    "bufio"
    "fmt"
    "io"
    "strings"
)


func fibonacci() intGen {
    a, b := 0, 1
    return func() int {
        a, b = b, a + b
        return a
    }
}

type intGen func() int

// 实现io.Reader接口
func (g intGen) Read(p []byte) (n int, err error) { // intGen函数也能实现接口,因为函数是一等公民,可当函数参数,接收者就是函数特殊的参数而已
    next := g() // 取得下一个元素

    // 限制输出个数
    if next > 10000 {
        return 0, io.EOF
    }

    // 将下一个元素写进 p []byte中,返回定写了几个字节和出了什么错误
    // 实现比较底层,用其它库代理一下。
    s := fmt.Sprintf("%d ", next) // 数字换成字符串,将 s 写进 p []byte中
    return strings.NewReader(s).Read(p)
    // TODO:incorrect if p is too small!
}


func printFileContents(reader io.Reader) { // 原本用处是打印文件,fibonacci实现reader接口也能打印
    scanner := bufio.NewScanner(reader)

    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

func main() {
    f := fibonacci()  //每个数都是前2个数的和
    printFileContents(f)
}

程序优化:`p []byte`如果给的比较小一个整数装不下,只能缓存状态了。

函数式编程范例3 使用函数来遍历二叉树

└─tree  // tree包
    │  node.go     // 结构方法
    │  traversal.go // 遍历树方法
    │
    └─treentry // main包
            entry.go

node.go代码

package tree

import "fmt"

type Node struct {
    Value       int
    Left, Right *Node
}

// 定义结构的方法
func (node Node) Print() { //值传递 在函数前面添加了(node Node)接收者,和print(node Node)相当
    fmt.Print(node.Value, " ")
}

func (node *Node) SetValue(Value int) { // 引用传递
    if node == nil {
        fmt.Println("Setting Value to nil node. Ignored.")
        return // 如果node是nil指针,拿内部值是拿不到的,会报错,所以要return返回
    }
    node.Value = Value
}

func CreatNode(Value int) *Node {
    return &Node{Value: Value}
}

traversal.go代码,增加遍历功能

package tree

import "fmt"

func (node *Node) Traverse() {
        node.TraverseFunc(func(n *Node) {
                n.Print()
        })
        fmt.Println()
}

// 遍历时不一定是要打印,可能是其它事
func (node *Node) TraverseFunc(f func(*Node)) {
        if node == nil {
                return
        }

        node.Left.TraverseFunc(f)
        f(node)
        node.Right.TraverseFunc(f)
}

entry.go代码

package main

import (
    "fmt"
    "learngo/tree"
)

type myTreeNode struct {
    node *tree.Node
}

// 定义一个后续遍历的函数
func (myNode *myTreeNode) postOrder() {
    if myNode == nil || myNode.node == nil {
        return
    }

    left := myTreeNode{myNode.node.Left}
    right := myTreeNode{myNode.node.Right}

    left.postOrder()
    right.postOrder()
    myNode.node.Print()
}

func main() {
    // 结构体的创建
    var root tree.Node //{0 <nil> <nil>}

    root = tree.Node{Value: 3}
    root.Left = &tree.Node{}
    root.Right = &tree.Node{5, nil, nil}
    root.Right.Left = new(tree.Node)    // root.right后也可以用"."点号,
    root.Left.Right = tree.CreatNode(2) // 利用自定义工厂函数构造结构
    // 结构方法的使用
    root.Print()                // 3 其中print是个函数,它不是无参的,它有个接收者root
    fmt.Println()
    root.Right.Left.SetValue(4) // setValue函数会把接收者root.right.left的地址做参数传递过去

    root.Traverse()
    fmt.Println()
    myRoot := myTreeNode{&root}
    myRoot.postOrder()

    // 数一数有几个节点
    nodeCount := 0
    root.TraverseFunc(func(n *tree.Node) {
        nodeCount++
    })
    fmt.Println("Node count:", nodeCount)
}
  • 范例1 斐波那契数列 展示go语言的闭包
  • 范例2 为函数实现接口 展示函数是一等公民,不仅可以做为参数、变量、返回值,还可以做为实现接口, 因为go语言中的结构方法是一种特殊的函数而已 为 斐波那契数列 实现了一个Reader接口
  • 范例3 使用函数来遍历二叉树 把中间的打印换成了函数,实现多种用途的遍历

总结:go语言闭包的应用

  • 更为自然,不需要修饰如何访问自由变量
  • 没有Lambda表达式,但是有匿名函数 从语法层面上Lambda表达式和匿名函数做的事一样,没必要再有Lambda语法

出错处理与defer/panic/recover

资源管理:

  • 打开文件要关闭
  • 连接数据库要释放
  • 这些是要成对出现的

    加入出错处理后程序有可能中间跳出来,怎么保证打开的连接关闭掉,这时就需要资源管理 与出错处理。 通过defer调用来实现资源管理的。

defer调用

  • 确保调用在函数结束时发生
  • 参数在defer语句时计算
  • defer列表为先进后出

范例:defer调用及将斐波那契数列写入到文件 创建errhanding/defer/defer.go文件

package main

import (
    "bufio"
    "fmt"
    "learngo/functional/fib"
    "os"
)

func tryDefer() {
    defer fmt.Println(1) // defer里面相当于有一个栈,先进后出的
    defer fmt.Println(2)
    fmt.Println(3)
    // defer的好处是不怕中间有return, 甚至panic
    panic("error occurred")
    fmt.Println(4)
}

func writeFile(filename string) { // 将斐波那契数列写入到文件
    file, err := os.Create(filename)  // 打开一个文件
    if err != nil {
        panic(err)
    }
    defer file.Close() // 使用defer,函数结束后关闭文件

    // file直接写文件比较慢,这里用 bufio 加载到内存方式
    writer := bufio.NewWriter(file)
    defer writer.Flush() // 保存到文件

    f := fib.Fibonacci()
    for i := 0; i < 20; i++ { //写入前20个
        fmt.Fprintln(writer, f())
    }
}

func main() {
    writeFile("fib.txt")
}

创建functional/fib/fib.go文件

package fib

func Fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a + b
        return a
    }
}

何时使用defer调用

  • Open/Close
  • Lock/Unlock
  • PrintHeader/PrintFooter 打印网页头尾

错误处理概念

file, err := os.Open("abc.txt")
if err != nil {
        if pathError, ok := err.(*os.PathError); ok {
               fmt.Println(pathError.Err)
           } else {
               fmt.Println("unknwon error", err)
           }
    }

范例:error的处理 创建errhanding/defer/defer.go文件

package main

import (
    "bufio"
    "errors"
    "fmt"
    "learngo/functional/fib"
    "os"
)

func tryDefer() {
    defer fmt.Println(1) // defer里面相当于有一个栈,先进后出的
    defer fmt.Println(2)
    fmt.Println(3)
    // defer的好处是不怕中间有return, 甚至panic
    panic("error occurred")
    fmt.Println(4)
}

func writeFile(filename string) { // 将斐波那契数列写入到文件
    file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)  // 打开一个文件

    // 自己设置的error
    err = errors.New("this is a custom error")
    // error的处理
    if err != nil {
        // panic(err) // panic比较难看
        // fmt.Println("file already exists")
        // fmt.Println("Error:", err.Error()) // 不加err.Error() 也可以 ,Println会err中的string找到
        // os.OpenFile中返回的error是*PathError。If there is an error, it will be of type *PathError
        if pathError, ok := err.(*os.PathError); !ok {
            panic(err)
        } else {
            fmt.Printf("%s, %s, %s\n", pathError.Op,
            pathError.Path,
            pathError.Err) // open, fib.txt, The file exists.
        }
        return
    }
    defer file.Close() // 使用defer,函数结束后关闭文件

    // file直接写文件比较慢,这里用 bufio 加载到内存方式
    writer := bufio.NewWriter(file)
    defer writer.Flush() // 保存到文件

    f := fib.Fibonacci()
    for i := 0; i < 20; i++ { //写入前20个
        fmt.Fprintln(writer, f())
    }
}

func main() {
    writeFile("fib.txt")
}

服务器统一出错处理1

这里没有新的逻辑,只是定代码的方式

范例: 操作1 创建errhandling/filelistingserver/web.go 文件

// url中显示文件内容
package main

import (
    "io/ioutil"
    "net/http"
    "os"
)

func main() {
    http.HandleFunc("/list/",
        func(writer http.ResponseWriter, request *http.Request) {
            path := request.URL.Path[len("/list/"):] // /list/fib.txt 得到真实文件的路径
            file, err := os.Open(path)
            if err != nil {
                panic(err)
            }
            defer file.Close()

            all, err := ioutil.ReadAll(file)
            if err != nil {
                panic(err)
            }

            writer.Write(all)
        })

    err := http.ListenAndServe(":8888", nil)
    if err != nil {
        panic(err)
    }
}

访问 `localhost:8888/list/fib.txts`可以看到内容,访问一个不存在的文件可以看到程序返回错误。

做一下错误处理

file, err := os.Open(path)
if err != nil {  // 错误处理,返回错误及http状态
    http.Error(writer,
        err.Error(),
        http.StatusInternalServerError)
    return
}

访问错误页面http://localhost:8888/list/fib.txta, 页面返回了`open fib.txta: The system cannot find the file specified.` 缺点:把程序内部的信息暴露出来了,用户看到不安全

操作2: 把error包装成外部的error 把函数本身的业务逻辑提出来 创建errhandling/filelisting/handler.go 文件

package filelisting

import (
    "io/ioutil"
    "net/http"
    "os"
)

func HandleFileList(writer http.ResponseWriter,
    request *http.Request) error {
    path := request.URL.Path[len("/list/"):] // /list/fib.txt 得到真实文件的路径
    file, err := os.Open(path)
    if err != nil { // 错误处理,不需要处理err
        return err
    }
    defer file.Close()

    all, err := ioutil.ReadAll(file)
    if err != nil {
        return err
    }

    writer.Write(all)
    return err
}

errhandling/filelistingserver/web.go 文件

// url中显示文件内容
package main

import (
    "learngo/errhandling/filelistingserver/filelisting"
    "log"
    "net/http"
    "os"
)

// 统一错误处理
type appHandler func(writer http.ResponseWriter, request *http.Request) error

func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值
    return func(writer http.ResponseWriter,
        request *http.Request) {
        err := handler(writer, request)
        if err != nil { // 错误处理
            log.Printf("error handing request: %s",
                err.Error()) // 程序记录错误
            code := http.StatusOK
            switch {
            case os.IsNotExist(err): // 文件不存在的错误
                code = http.StatusNotFound
            case os.IsPermission(err): // 没有权限 403
                code = http.StatusForbidden
            default: // 什么都不知道返回 500
                code = http.StatusInternalServerError
            }
            // 参数的意义,1th: 向谁汇报err,这里为writer, 2th: 页面显示的错误信息, 3th: 状态码
            http.Error(writer,
                http.StatusText(code),
                code)
        }
    }
}

func main() {
    http.HandleFunc("/list/", errWrapper(filelisting.HandleFileList)) // 将函数中业务逻辑提出去

    err := http.ListenAndServe(":8888", nil)
    if err != nil {
        panic(err)
    }
}

访问错误页面http://localhost:8888/list/fib.txta, 页面返回了`Not Found`

服务器统一出错处理2

范例: 不以`/list/`为入口 修改errhandling/filelistingserver/web.go

// url中显示文件内容
package main

import (
    "learngo/errhandling/filelistingserver/filelisting"
    "log"
    "net/http"
    "os"
)

// 统一错误处理
type appHandler func(writer http.ResponseWriter, request *http.Request) error

func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值
    return func(writer http.ResponseWriter,
        request *http.Request) {
        defer func() { // 当服务发现panic时,拦截保持一下
            if r := recover(); r != nil { // 拦截pannic,不中断程序
                log.Printf("Panic: %v", r)
                http.Error(writer,
                    http.StatusText(http.StatusInternalServerError),
                    http.StatusInternalServerError)
            }
        }()

        err := handler(writer, request)

        if err != nil { // 错误处理
            log.Printf("Error handing request: %s",
                err.Error())
            code := http.StatusOK
            switch {
            case os.IsNotExist(err): // 文件不存在的错误
                code = http.StatusNotFound
            case os.IsPermission(err): // 没有权限 403
                code = http.StatusForbidden
            default: // 什么都不知道返回 500
                code = http.StatusInternalServerError
            }
            // 参数的意义,1th: 向谁汇报err,这里为writer, 2th: 页面显示的错误信息, 3th: 状态码
            http.Error(writer,
                http.StatusText(code),
                code)
        }
    }
}

func main() {
    http.HandleFunc("/", errWrapper(filelisting.HandleFileList)) // 响应/路径

    err := http.ListenAndServe(":8888", nil)
    if err != nil {
        panic(err)
    }
}

访问错误页面http://localhost:8888/abc,触发panic 页面返回了`Internal Server Error`,同时程序打印友好的 `Panic: Internal Server Error`

业务逻辑判断url路径不匹配时,返回error 修改rrhandling/filelisting/handler.go 文件

package filelisting

import (
    "errors"
    "io/ioutil"
    "net/http"
    "os"
    "strings"
)

const prefix = "/list/"

func HandleFileList(writer http.ResponseWriter,
    request *http.Request) error {
    if strings.Index(
        request.URL.Path, prefix) != 0 { // 判断路径是否匹配
        return errors.New("path must start with " + prefix)
    }
    path := request.URL.Path[len(prefix):] // /list/fib.txt 得到真实文件的路径
    file, err := os.Open(path)
    if err != nil { // 错误处理,不需要处理err
        return err
    }
    defer file.Close()

    all, err := ioutil.ReadAll(file)
    if err != nil {
        return err
    }

    writer.Write(all)
    return err
}

访问错误页面http://localhost:8888/abc,触发panic 页面返回了`Internal Server Error`,同时程序打印error日志 `Error handing request: path must start with /list/` 缺点:返回信息哪些能给用户看到哪些不让看没有做区分

区分给用户显示的错误信息 修改errhandling/filelistingserver/web.go

// url中显示文件内容
package main

import (
    "learngo/errhandling/filelistingserver/filelisting"
    "log"
    "net/http"
    "os"
)

// 统一错误处理:
type appHandler func(writer http.ResponseWriter, request *http.Request) error

func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值
    return func(writer http.ResponseWriter,
        request *http.Request) {
        defer func() { // 指定路径:当服务发现panic时,拦截保持一下
            if r := recover(); r != nil { // 指定路径:拦截pannic,不中断程序
                log.Printf("Panic: %v", r)
                http.Error(writer,
                    http.StatusText(http.StatusInternalServerError),
                    http.StatusInternalServerError)
            }
        }()

        err := handler(writer, request)

        if err != nil { // 统一错误处理:错误处理
            log.Printf("Error handing request: %s",
                err.Error())

            // 区分用户信息:区分给用户看的错误信息
            if userErr, ok := err.(userError); ok {
                http.Error(writer,
                    userErr.Message(),
                    http.StatusBadRequest)
                return
            }
            code := http.StatusOK
            switch {
            case os.IsNotExist(err): // 文件不存在的错误
                code = http.StatusNotFound
            case os.IsPermission(err): // 没有权限 403
                code = http.StatusForbidden
            default: // 什么都不知道返回 500
                code = http.StatusInternalServerError
            }
            // 参数的意义,1th: 向谁汇报err,这里为writer, 2th: 页面显示的错误信息, 3th: 状态码
            http.Error(writer,
                http.StatusText(code),
                code)
        }
    }
}

// 区分用户信息:定义用户错误接口
type userError interface {
    error            // 系统看的信息
    Message() string // 用户看的信息
}

func main() {
    http.HandleFunc("/", errWrapper(filelisting.HandleFileList)) // 统一错误处理|指定路径:将函数中业务逻辑提出去

    err := http.ListenAndServe(":8888", nil)
    if err != nil {
        panic(err)
    }
}

修改rrhandling/filelisting/handler.go 文件

package filelisting

import (
    "io/ioutil"
    "net/http"
    "os"
    "strings"
)

const prefix = "/list/"

// 区分用户信息:定义userError类型
type userError string

func (e userError) Error() string {
    return e.Message()
}

func (e userError) Message() string {
    return string(e)
}

func HandleFileList(writer http.ResponseWriter,
    request *http.Request) error { // 统一错误处理: 业务逻辑
    if strings.Index(
        request.URL.Path, prefix) != 0 { // 匹配路径:判断路径是否匹配
        return userError("path must start with " + prefix) // 区分用户信息:返回用户错误信息类型值
    }
    path := request.URL.Path[len(prefix):] // 匹配路径: /list/fib.txt 得到真实文件的路径
    file, err := os.Open(path)
    if err != nil { // 错误处理,不需要处理err
        return err
    }
    defer file.Close()

    all, err := ioutil.ReadAll(file)
    if err != nil {
        return err
    }

    writer.Write(all)
    return err
}

访问错误页面http://localhost:8888/list/fib.txta, 页面返回了`Not Found`,同时程序打印error日志`Error handing request: open fib.txta: The system cannot find the file specified.` 给用户隐藏了系统错误信息 访问错误页面http://localhost:8888/abc,触发panic 页面返回了`path must start with /list/`,同时程序打印error日志 `Error handing request: path must start with /list/` 给用户显示了用户错误信息

error vs panic

尽量不用panic

  • 意料之中的:用error。如:文件打不开
  • 意料之外的:用panic。如:数组越界

范例小结

  • defer + panic + recover 做错误处理
  • Type Assertion 判断错误类型
  • 函数式编程的应用 包装函数

    type appHandler func(writer http.ResponseWriter, request *http.Request) error
    errWrapper(handler appHandler) func(
        http.ResponseWriter,
        *http.Request) { // 把输入的函数包装成一个输出的函数。 函数式编程,函数即可做参数,也可做返回值
    

测试与性能调优

测试

希望程序员多做测试而不是多做调试,能否写出一个好的测试对一名成功的软件工程非常重要。

范例:调试

传统测试 vs 表格驱动测试

go语言是使用表格驱动的测试方法

传统测试
@Test public void testAdd() {
    assertEquals(3, add(1, 2)); // 1th: 3 期待值,2th:add(1, 2)算出的值
    assertEquals(2, add(0, 2));
    assertEquals(0, add(0, 0));
    assertEquals(0, add(-1, 1));
    assertEquals(Integer.MIN_VALUE, add(1, Integer.MAX_VALUE));
}

缺点:

  • 测试数据和测试逻辑混在一起
  • 出错信息不明确
  • 一旦一个数据出错测试全部结束
表格驱动测试

go代码示例:

tests := []struct { // 数据
    a, b, c int32
}{
    {1, 2, 3},
    {0, 2, 2},
    {0, 0, 0},
    {-1, 1, 0},
    {math.MaxInt32, 1, math.MinInt32},
}

for _, test := range tests { // 逻辑
        if actual := add(test.a, test.b); actual != test.c {}
    }

优点:

  • 分离的测试数据和测试逻辑
  • 明确的出错信息
  • 可以部分失败
  • go语言的语法使用我们更易实践表格驱动测试
    • java, c++写表格驱动测试比较麻烦

表格驱动测试

  • `testing.T`的使用
    • 测试文件:
      • 文件名`*_test.go`,和被测试代码放在一起
      • 函数名`Test*`开头
  • 运行测试
    • 命令行:`go test .`

范例:triangle三角形

// learngo/basic/basic/basic.go

package main

import (
  "fmt"
  "math"
)

func triangle() {
  var a, b int = 3, 4
  fmt.Println(calcTriangle(a, b))
}

func calcTriangle(a, b int) int {
  var c int
  c = int(math.Sqrt(float64(a*a + b*b)))
  return c
}
func main() {
  triangle()
}


// learngo/basic/basic/triangle_test.go

package main

import "testing"

func TestTriangle(t *testing.T) {
  tests := []struct{ a, b, c int }{
    {3, 4, 5},
    {5, 12, 13},
    {8, 15, 17},
    {12, 35, 37},
    {30000, 40000, 0},
  }

  for _, tt := range tests {
    if actual := calcTriangle(tt.a, tt.b); actual != tt.c {
      t.Errorf("calcTriangle(%d, %d); got %d; expected %d",
        tt.a, tt.b, actual, tt.c)
    }
  }
}

运行测试

#进入到被测目录, 执行go test .
cd basic/basic
PS D:\project\learngo\basic\basic> go test .
--- FAIL: TestTriangle (0.00s)
    triangle_test.go:16: calcTriangle(30000, 40000); got 50000; expected 0
FAIL
FAIL    xxx.com/jasper/learngo/basic/basic      0.311s

范例2:求无重复字符的最长子串

// leedcode/nonrepeatingsubstr/nonrepeating.go

package main

import "fmt"

func lengthOfNonRepeatingSubStr(s string) int {
  lastOccurred := make(map[rune]int) // 每个字母最后出现的位置
  start := 0                         // 当前找到的最长不含有字符的子串开始
  maxLength := 0                     // 重复子串的最大长度
  // 遍历字符串
  for i, ch := range []rune(s) {

    if lastI, ok := lastOccurred[ch]; ok && lastI >= start { // lastOccurred[ch]可能是不存在的它的值是0,0参与下面的运算是不对的
      start = lastOccurred[ch] + 1
    }

    if i-start+1 > maxLength { // 更新maxLength
      maxLength = i - start + 1
    }

    lastOccurred[ch] = i
  }

  return maxLength
}

func main() {
  fmt.Println(
    lengthOfNonRepeatingSubStr("abccabcbb"))
  fmt.Println(
    lengthOfNonRepeatingSubStr("pwwkewwe"))
  fmt.Println(
    lengthOfNonRepeatingSubStr(""))
  fmt.Println(
    lengthOfNonRepeatingSubStr("b"))
  fmt.Println(
    lengthOfNonRepeatingSubStr("一二三二一"))
}


// leedcode/nonrepeatingsubstr/nonrepeating_test.go

package main

import "testing"

func TestSubstr(t *testing.T) {
  tests := []struct {
    s   string
    ans int
  }{
    // Normal cases 一般例子
    {"abccabcbb", 3},
    {"pwwkewwe", 3},

    // Edg cases 特殊例子
    {"", 0},
    {"b", 1},
    {"bbbb", 1},
    {"abcabcabcd", 4},

    // Chinese support
    {"这里是", 3},
    {"一二三二一", 3},
    {"黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 0},
  }

  for _, tt := range tests {
    actual := lengthOfNonRepeatingSubStr(tt.s)
    if actual != tt.ans {
      t.Errorf("got %d for input %s; "+
        "expect %d",
        actual, tt.s, tt.ans)
    }
  }
}

运行测试

PS D:\project\learngo\basic\basic> cd ..\..\leedcode\nonrepeatingsubstr\

PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test .
--- FAIL: TestSubstr (0.00s)
    nonrepeating_test.go:29: got 7 for input 黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发; expect 0
FAIL
FAIL    xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr      0.245s
FAIL

代码覆盖率和性能测试

代码覆盖率

命令行

# 1.生成代码覆盖率文件:
go test -coverprofile="c.out"
# 或者 go test -cover

# 2. 使用go tool 查看,下面以页面方式查看为例
go tool cover -html="c.out"
# 绿色是cover到的,红色是没有cover到的

性能测试

  • 函数名:`Benchmark*`开头
  • 命令行:

    go test -bench .
    

范例:求无重复字符的最长子串

// leedcode/nonrepeatingsubstr/nonrepeating_test.go

package main

import "testing"

func TestSubstr(t *testing.T) {
  tests := []struct {
    s   string
    ans int
  }{
    // Normal cases 一般例子
    {"abccabcbb", 3},
    {"pwwkewwe", 3},

    // Edg cases 特殊例子
    {"", 0},
    {"b", 1},
    {"bbbb", 1},
    {"abcabcabcd", 4},

    // Chinese support
    {"这里是", 3},
    {"一二三二一", 3},
    {"黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 7},
  }

  for _, tt := range tests {
    actual := lengthOfNonRepeatingSubStr(tt.s)
    if actual != tt.ans {
      t.Errorf("got %d for input %s; "+
        "expect %d",
        actual, tt.s, tt.ans)
    }
  }
}

func BenchmarkSubstr(b *testing.B) {
  // 这里选一个比较难的来示例
  s, ans := "黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 7

  for i := 0; i < b.N; i++ { // b.N 系统决定做多少遍
    actual := lengthOfNonRepeatingSubStr(s)
    if actual != ans {
      b.Errorf("got %d for input %s; "+
        "expect %d",
        actual, s, ans)
    }
  }
}

运行性能测试

PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test -bench .
goos: windows
goarch: amd64
pkg: xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
BenchmarkSubstr-8        1488318               815.3 ns/op
PASS
ok      xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr      2.290s

# 跑了1488318次, 每次用时 815.3纳秒

使用pprof进行性能调优

范例:增加求无重复字符的最长子串难度

 // leedcode/nonrepeatingsubstr/nonrepeating_test.go
func BenchmarkSubstr(b *testing.B) {
  // 这里选一个比较难的来示例
  s, ans := "黑化黑灰化肥黑灰会挥发发灰黑化肥黑灰化肥挥发", 7
  for i := 0; i < 13; i++ { // 增加s长度
    s = s + s
  }

  b.Logf("len(s) = %d", len(s)) //查看s的长度 b.Logf()输出日志
  b.ResetTimer() // 重置时间,排除上面准备数据的时间

  for i := 0; i < b.N; i++ { // b.N 系统决定做多少遍
    actual := lengthOfNonRepeatingSubStr(s)
    if actual != ans {
      b.Errorf("got %d for input %s; "+
        "expect %d",
        actual, s, ans)
    }
  }
}

增加字符串长度后,用时

PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test -bench .
goos: windows
goarch: amd64
pkg: xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
BenchmarkSubstr-8            187           6298157 ns/op
--- BENCH: BenchmarkSubstr-8
    nonrepeating_test.go:43: len(s) = 540672
    nonrepeating_test.go:43: len(s) = 540672
    nonrepeating_test.go:43: len(s) = 540672
PASS
ok      xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr      2.121s

# 187次,每次6ms左右

找到运行慢的地方

# 跑性能测试的同时,生成cpu耗时二进制文件
go test -bench . -cpuprofile="cpu.out"

# 使用go tool pprof 查看, 在交互式里生成图片查看
go tool pprof cpu.out
(pprof) help
(pprof) web

使用web生成图片前,提前安装graphviz工具 图片中展示信息:

  • 方框越大越花时间
  • 箭头越粗越花时间

优化花时间多的地方 以上面代码“求不重复的最长子串”为例,需要优化3块地方

  • runtime – stringoslicerune – decoderune string在转成`[]rune`时,中间有decode过程,不可避免,无法优化
  • runtime – mapassign_fast32 `lastOccurred[ch] = i` 可优化
  • runtime – mapaccess2_fast32 `lastoccurred[ch]` 可优化

优化map,用其它的数据结构存

// leedcode/nonrepeatingsubstr/nonrepeating.go

func lengthOfNonRepeatingSubStr(s string) int {
  // lastOccurred := make(map[rune]int) // 每个字母最后出现的位置
  lastOccurred := make([]int, 0xffff) // 对于字符型map优化的方法,用空间换时间,c++
  // lastOccurred['e'] = 1 // lastOccurred[0x65] = 1
  // lastOccurred['课'] = 6 // lastOccurred[0x8BFE] = 6
  for i := range lastOccurred { // 初始值设置为 -1
    lastOccurred[i] = -1
  }

  start := 0
  maxLength := 0

  for i, ch := range []rune(s) {

    if lastI := lastOccurred[ch]; lastI != -1 && lastI >= start {  // 判断
      start = lastOccurred[ch] + 1
    }

    if i-start+1 > maxLength {
      maxLength = i - start + 1
    }

    lastOccurred[ch] = i
  }

  return maxLength
}

运行性能测试

PS D:\project\learngo\leedcode\nonrepeatingsubstr> go test -bench . -cpuprofile="cpu.out"
goos: windows
goarch: amd64
pkg: xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
BenchmarkSubstr-8            435           3457264 ns/op
--- BENCH: BenchmarkSubstr-8
    nonrepeating_test.go:43: len(s) = 540672
    nonrepeating_test.go:43: len(s) = 540672
    nonrepeating_test.go:43: len(s) = 540672
PASS
ok      xxx.com/jasper/learngo/leedcode/nonrepeatingsubstr      2.190s

# 435次,每次3ms左右,快了2倍左右

# 再看一下性能图
go test -bench . -cpuprofile="cpu.out"
go tool pprof cpu.out
(pprof) web

还可以再优化:runtime – makeslice

// 修改 leedcode/nonrepeatingsubstr/nonrepeating.go
var lastOccurred = make([]int, 0xffff) // 提到函数外面

func lengthOfNonRepeatingSubStr(s string) int {

# 运行性能测试
go test -bench . -cpuprofile="cpu.out"
go tool pprof cpu.out
(pprof) web

现在时间都花在 runtime – stringoslicerune – decoderune上了,没有优化空间了

性能调优模式

  • `-cpuprofile`获取性能数据
  • `go tool pprof`查看性能数据
    • web命令可看可视化图形
  • 分析慢在哪里
    • 框最大的慢
  • 优化代码
  • 重复动作

测试http服务器

范例:测试跟业务没有逻辑的出错处理 将出错的返回值接下来

// learngo\errhandling\filelistingserver/errwrapper_test.go

package main

import (
  "errors"
  "fmt"
  "io/ioutil"
  "net/http"
  "net/http/httptest"
  "os"
  "strings"
  "testing"
)

func errPanic(writer http.ResponseWriter, request *http.Request) error { // 输入函数:会panicw信息的函数
  panic(123)
}

// 区分用户信息:定义userError类型
type testingUserError string

func (e testingUserError) Error() string {
  return e.Message()
}

func (e testingUserError) Message() string {
  return string(e)
}
func errUserError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:用户错误信息的函数
  return testingUserError("user error")
}

func errNotFound(writer http.ResponseWriter, request *http.Request) error { // 输入函数:文件不存在错误信息的函数
  return os.ErrNotExist
}

func errPermission(writer http.ResponseWriter, request *http.Request) error { // 输入函数:权限错误信息的函数
  return os.ErrPermission
}

func errUnkown(writer http.ResponseWriter, request *http.Request) error { // 输入函数:500错误信息的函数
  return errors.New("unknown err")
}

func noError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:没有错误信息的函数
  fmt.Fprintln(writer, "no error")
  return nil
}

func TestErrWrapper(t *testing.T) {
  tests := []struct {
    h       appHandler // 输入的是函数
    code    int        // 期望的输出:状态码
    message string     // 期望的输出:信息
  }{
    {errPanic, 500, "Internal Server Error"},
    {errUserError, 400, "user error"},
    {errNotFound, 404, "Not Found"},
    {errPermission, 403, "Forbidden"},
    {errUnkown, 500, "Internal Server Error"},
    {noError, 200, "no error"},
  }

  for _, tt := range tests {
    f := errWrapper(tt.h)
    response := httptest.NewRecorder()
    request := httptest.NewRequest(
      http.MethodGet,
      "http://www.immoc.com",
      nil)
    f(response, request) // 调用出错函数
    b, _ := ioutil.ReadAll(response.Body)
    body := strings.Trim(string(b), "\n") // 把string中的换行去掉
    if response.Code != tt.code ||
      body != tt.message {
      t.Errorf("expect (%d, %s); "+
        "got (%d, %s)",
        tt.code, tt.message,
        response.Code, body)
    }
  }
}


# 查看一下哪些代码没有覆盖到
go test -coverprofile="c.out"
go tool cover -html="c.out"

真实请求

// 修改 learngo\errhandling\filelistingserver/errwrapper_test.go

package main

import (
  "errors"
  "fmt"
  "io/ioutil"
  "net/http"
  "net/http/httptest"
  "os"
  "strings"
  "testing"
)

func errPanic(writer http.ResponseWriter, request *http.Request) error { // 输入函数:会panicw信息的函数
  panic(123)
}

// 区分用户信息:定义userError类型
type testingUserError string

func (e testingUserError) Error() string {
  return e.Message()
}

func (e testingUserError) Message() string {
  return string(e)
}
func errUserError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:用户错误信息的函数
  return testingUserError("user error")
}

func errNotFound(writer http.ResponseWriter, request *http.Request) error { // 输入函数:文件不存在错误信息的函数
  return os.ErrNotExist
}

func errPermission(writer http.ResponseWriter, request *http.Request) error { // 输入函数:权限错误信息的函数
  return os.ErrPermission
}

func errUnkown(writer http.ResponseWriter, request *http.Request) error { // 输入函数:500错误信息的函数
  return errors.New("unknown err")
}

func noError(writer http.ResponseWriter, request *http.Request) error { // 输入函数:没有错误信息的函数
  fmt.Fprintln(writer, "no error")
  return nil
}

var tests = []struct { // 测试数据共用
  h       appHandler // 输入的是函数
  code    int        // 期望的输出:状态码
  message string     // 期望的输出:信息
}{
  {errPanic, 500, "Internal Server Error"},
  {errUserError, 400, "user error"},
  {errNotFound, 404, "Not Found"},
  {errPermission, 403, "Forbidden"},
  {errUnkown, 500, "Internal Server Error"},
  {noError, 200, "no error"},
}

func TestErrWrapper(t *testing.T) { // 假数据

  for _, tt := range tests {
    f := errWrapper(tt.h)
    response := httptest.NewRecorder() // 提供假数据
    request := httptest.NewRequest(
      http.MethodGet,
      "http://www.immoc.com",
      nil)
    f(response, request) // 调用出错函数
    verifyResponse(
      response.Result(), tt.code, tt.message, t)
  }
}

func TestErrWrapperInServer(t *testing.T) {  // 启一个server,真实url访问请求
  for _, tt := range tests {
    f := errWrapper(tt.h)
    server := httptest.NewServer(
      http.HandlerFunc(f))
    resp, _ := http.Get(server.URL)

    verifyResponse(
      resp, tt.code, tt.message, t)
  }
}

func verifyResponse(resp *http.Response,
  expectCode int, expectMsssage string,
  t *testing.T) {
  b, _ := ioutil.ReadAll(resp.Body)
  body := strings.Trim(string(b), "\n") // 把string中的换行去掉
  if resp.StatusCode != expectCode ||
    body != expectMsssage {
    t.Errorf("expect (%d, %s); "+
      "got (%d, %s)",
      expectCode, expectMsssage,
      resp.StatusCode, body)
  }
}


# 运行测试
PS D:\project\learngo\errhandling\filelistingserver> go test -run="TestErrWrapperInServer$"
2021/11/12 22:26:45 Panic: 123
2021/11/12 22:26:45 Error handing request: user error
2021/11/12 22:26:45 Error handing request: file does not exist
2021/11/12 22:26:45 Error handing request: permission denied
2021/11/12 22:26:45 Error handing request: unknown err
PASS
ok      xxx.com/jasper/learngo/errhandling/filelistingserver    0.302s

测试代码就是堆起来的,维护起来比较方便

http测试小结

  • 通过使用假的Request/Response 优点:速度快,测试粒度细,更像单元测试只是测试某个函数
  • 通过起服务器 优点:代码覆盖量大。 缺点:慢

生成文档和示例代码

`go doc` 和 `godoc` 命令

`go doc`查看文档

PS D:\project\learngo\queue> go doc    // 当前目录下的文档
package queue // import "xxx.com/jasper/learngo/queue"

type Queue []int

PS D:\project\learngo\queue> go doc Queue

# 显示自带包中的文档
go doc json.Decoder.Decode
PS D:\project\learngo\queue> go doc fmt.Println
package fmt // import "fmt"

func Println(a ...interface{}) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline is
    appended. It returns the number of bytes written and any write error
    encountered.

`godoc` 生成文档

# 1. godoc安装
go get golang.org/x/tools/cmd/godoc
go install golang.org/x/tools/cmd/godoc
# 将二进制文件放到$GOROOT/bin下

# 2. 页面访问 http://localhost:6060
godoc -http :6060
# 可找到我们自己写的文档

用注释写文档

注释可以自动转成文档

// learngo/queue/queue.go

package queue

// An FIFO queue.
type Queue []int

// Pushes the element into the queue.
//   e.g. q.Push(123) 空格给文字,显示时会框起来
func (q *Queue) Push(v int) {
  *q = append(*q, v)
}

// Pops element from head.
func (q *Queue) Pop() int {
  head := (*q)[0]
  *q = (*q)[1:]
  return head
}

生成示例代码

在测试中加入Example

// learngo/queue/queue_test.go

package queue

import "fmt"

func ExampleQueue_Pop() {
  q := Queue{1}
  q.Push(2)
  q.Push(3)
  fmt.Println(q.Pop())
  fmt.Println(q.Pop())

  // Output:
  // 1
  // 2
}

# 页面中查看示例
godoc -http :6060

goroutine

goroutine

func main() {
  for i := 0; i < 1000; i++ {
    go func(i int) {
      for {
        fmt.Println("Hello %d\n", i)
      }
    }(i)
  }
  time.Sleep(time.Microsecond)
}

加了关键字 go ,其后函数会并发执行。

范例:goroutine

// goroutine/goroutine.go

package main

import (
  "fmt"
  "time"
)

func main() {
  for i := 0; i < 1000; i++ {
    go func(i int) { // i 引用外面的 i 不安全,这里将 i 传进来
      for { // 反复打印
        fmt.Printf("Hello from goroutine %d\n", i) // 证明很多人打,给它设置一个编号
      }
    }(i)
  }
  // 这里的 main 和 for {} 是并发执行的,还来不及打印就执行完了
  // main 结束了,所有的 goroutine就被杀掉了
  // 给一定时间让 for{} 能打出东西
  time.Sleep(time.Millisecond)
}

协程Coroutine

goroutine是一种协程。

  • 轻量级“线程” 都是并发执行东西的
  • 是*非抢占式*多任务处理,由协程主动交出控制权 只要处理其中切换的几个点就可以了,这个对资源的消耗就会少一些
  • 是编译器/解释器/虚拟机层面的多任务
    • 不是操作系统层面的多任务,操作系统层面只有线程没有协程
    • 编译器级别的多任务:编译器会把go 一个function解释成一个协程
    • 在执行上go语言会有一个调度器调度协程
  • 多协程可能在一个或多个线程上运行 这个是由调度器来决定的

什么是“非抢占式多任务” 抢占式:`fmt.Printf`是io的操作,io的操作里会进行切换,因为io操作总会有等待的过程。 范例:`runtime.Gosched()`交出控制权

// goroutine/goroutine.go

package main

import (
  "fmt"
  "runtime"
  "time"
)

func main() {
  var a [10]int
  for i := 0; i < 10; i++ {
    go func(i int) {
      for {
        a[i]++ // 没有机会协程间切换,就会被一个协程抢掉。不主动交出控制权,就会始终在这个协程中
        runtime.Gosched()
      }
    }(i)
  }
  time.Sleep(time.Millisecond)
  fmt.Println(a)
}

#结果
PS D:\project\learngo> go run .\goroutine\goroutine.go
[854 442 626 625 578 564 640 620 537 589]

`go run -race` 检测数据访问的冲突 范例:

func main() {
  var a [10]int
  for i := 0; i < 10; i++ {
    go func() {
      for {
        a[i]++
        runtime.Gosched()
      }
    }() // 解决报错需要固定i值,即把i传进函数中
  }
  time.Sleep(time.Millisecond) // 半包中 i 在这是 i= 10,a[10]是不存在的,无法在闭包中运行
  fmt.Println(a)
}

#检查数据冲突
PS D:\project\learngo> go run  -race .\goroutine\goroutine.go
==================
WARNING: DATA RACE
Read at 0x00c00012c078 by goroutine 7:

go语言调度器

  • 子程序是协程的一个特例

普通函数 vs 协程

普通函数在一个线程内– 线程中有main函数– main函数调用doWork函数,等doWork结束才把 控制权交给main函数

协程也是有main和doWork函数,但他们之间不是单向的箭头,他们中间有个双向的通道。 main和doWork之间数据可以进行双向的流通、控制权也可以双向的流通。就相当于并发执行 的两个线程。 main和doWork可能运行在一个线程中也可能在多个线程中,这个事情不需要管,由调度器负责。

其他语言对协程的支持

其他语言不是原生支持

  • c++ :Boost.Coroutine库
  • java: 不支持;第三方的有
  • python中的协程
    • 使用yield关键字实现协程
    • python 3.5加入async def对协程原生支持

go语言中的协程是怎样的?

go语言的一个进程,程序开启后他下面有一个调度器,调度器负责调度协程。 有些协程是1个协程放在线程中、有些协程是2个协程放一个线程中等,这些是调度器来 控制的。

goroutine的定义

  • 任何函数只需加上go关键字就能送给调度器运行。
  • 好处:不需要在定义时区分是否是异步函数 这是相对于python来讲的
  • 调度器会在合适的点进行切换 虽然是非抢占式的,但还是有个调度器进行切换,这些切换的点我们并不进行完全的 控制。这个也是goroutine和传统意义上协程有一定区别的地方。 传统意义上的协程是非抢占式的,所有切换的点都需要显示地写出来,但goroutine 不一样,就像写普通函数一样,调度器会进行切换。 但它又和线程的切换不一样,它是固定的点切换
  • 使用`-race`来检测数据访问冲突

goroutine可能切换的点

  • I/O或者select i/o:如,向屏幕上打印字
  • channel
  • 等待锁
  • 函数调用(有时) 是一个切换的机会,这个由调度器决定
  • runtime.Gosched() 手动提供一个切换的点,交出控制权
  • 上面的只是参考,不能保证切换,不能保证在其他地方不切换 goroutine协程是非抢占式的,从代码来看可能跟抢占式的有点像,但实际运行的机制 还是非抢占式的

channel

协程之间的双向通道,即channel

channel语法

范例:创建channel

package main

import "fmt"

func chanDemo() {
  // 创建channel
  // var c chan int // c == nil ,只是定义了chan,它的值是nil,nil的channel无法使用,但在select中会用到
  c := make(chan int)
  // 发数据
  c <- 1  // 死锁在向chan发送1
  c <- 2
  // 从chan中收数据
  n := <-c
  fmt.Println(n)
}

func main() {
  chanDemo()
}

# 执行报错,死锁
PS D:\project\learngo> go run .\channel\channel.go
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.chanDemo()
        D:/project/learngo/channel/channel.go:10 +0x37

channel是goroutine和goroutine之间的交互,发了数据没人收就会deadlock! 修改之后代码

func chanDemo() {
  // 创建channel
  // var c chan int // c == nil ,只是定义了chan,它的值是nil,nil的channel无法使用,但在select中会用到
  c := make(chan int)
  go func() {
    for {
      n := <-c
      fmt.Println(n)
    }
  }()
  // 发数据
  c <- 1
  c <- 2
  // 避免来不及打印,main函数先结束了
  time.Sleep(time.Millisecond)
}

func main() {
  chanDemo()
}
channel也是一等公民,也能作为参数、作为返回值

范例:channel当函数参数

package main

import (
  "fmt"
  "time"
)

func worker(id int, c chan int) {
  for {
    fmt.Printf("worker %d received %d\n",
      id, <-c)
  }
}

func chanDemo() {
  // 创建channel
  // var c chan int // c == nil ,只是定义了chan,它的值是nil,nil的channel无法使用,但在select中会用到
  c := make(chan int)
  //go worker(c)
  go worker(0, c)
  // 发数据
  c <- 1
  c <- 2
  // 避免来不及打印,main函数先结束了
  time.Sleep(time.Millisecond)
}

func main() {
  chanDemo()
}

范例:channel作为数组的类型

package main

import (
  "fmt"
  "time"
)

func worker(id int, c chan int) {
  for {
    fmt.Printf("worker %d received %c\n",
      id, <-c)
  }
}

func chanDemo() {

  var channels [10]chan int // channel作为数组的类型
  for i := 0; i < 10; i++ { // 开10个work接收channel数据
    channels[i] = make(chan int)
    go worker(i, channels[i])
  }

  // 向10个channels[i]发数据
  for i := 0; i < 10; i++ {
    channels[i] <- 'a' + i
  }
  for i := 0; i < 10; i++ {
    channels[i] <- 'A' + i
  }

  // 避免来不及打印,main函数先结束了
  time.Sleep(time.Millisecond)
}

func main() {
  chanDemo()
}

# 输出
PS D:\project\learngo> go run .\channel\channel.go
worker 3 received d
worker 0 received a
worker 0 received A
worker 5 received f
worker 4 received e
worker 8 received i
worker 9 received j
worker 2 received c
worker 1 received b
worker 1 received B
worker 7 received h
worker 6 received g
worker 6 received G
worker 2 received C
worker 3 received D

范例:channel作为返回值

package main

import (
  "fmt"
  "time"
)

func createWorker(id int) chan int {
  c := make(chan int)
  go func(){ // 真正做事的work
    for {
      fmt.Printf("worker %d received %c\n",
        id, <-c)
    }
  }()

  return c
}

func chanDemo() {

  var channels [10]chan int // channel作为数组的类型
  for i := 0; i < 10; i++ { // 开10个work接收channel数据
    channels[i] = createWorker(i)
  }

  // 向10个channels[i]发数据
  for i := 0; i < 10; i++ {
    channels[i] <- 'a' + i
  }
  for i := 0; i < 10; i++ {
    channels[i] <- 'A' + i
  }

  // 避免来不及打印,main函数先结束了
  time.Sleep(time.Millisecond)
}

func main() {
  chanDemo()
}

范例:channel的定义上进一步修饰,告知channel的使用方式

func createWorker(id int) chan<- int { // 告知channel是向channel发数据的
  c := make(chan int)
  go func(){ // 真正做事的work
    for {
      fmt.Printf("worker %d received %c\n",
        id, <-c)  // 里面只能从channel中收数据
    }
  }()

  return c
}

func chanDemo() {

  var channels [10]chan<- int //
  for i := 0; i < 10; i++ { // 开10个work接收channel数据
    channels[i] = createWorker(i)
  }

  // 向10个channels[i]发数据
  for i := 0; i < 10; i++ {
    channels[i] <- 'a' + i
  }
  for i := 0; i < 10; i++ {
    channels[i] <- 'A' + i
  }

  // 避免来不及打印,main函数先结束了
  time.Sleep(time.Millisecond)
}

func main() {
  chanDemo()
}
buffer channel

范例:channel有缓冲区

package main

import (
  "fmt"
  "time"
)

func worker(id int, c chan int) {
  for {
    fmt.Printf("worker %d received %c\n",
      id, <-c)  // 里面只能从channel中收数据
  }
}

func bufferChannel() {
  c := make(chan int, 3) // 有3个缓冲
  go worker(0, c)
  c <- 'a'
  c <- 'b'
  c <- 'c'
  c <- 'd'
  time.Sleep(time.Millisecond)
}

func main() {
  bufferChannel()
}
channel close and range

范例:channel可以close,永远是发送方close

package main

import (
  "fmt"
  "time"
)

func worker(id int, c chan int) {
  // for {
  //  n, ok := <-c // 判断channel是否有数据方法1
  //  if !ok {
  //    break
  //  }
  //  fmt.Printf("worker %d received %c\n",
  //    id, n)
  // }

  for n := range c {  // 判断channel是否有数据方法2
    fmt.Printf("worker %d received %c\n",
    id, n)
  }
}

func channelClose() {
  c := make(chan int, 3) // 有3个缓冲
  go worker(0, c)
  c <- 'a'
  c <- 'b'
  c <- 'c'
  c <- 'd'
  close(c) // 告诉接收方数据发完了
  time.Sleep(time.Millisecond)
}

func main() {
  channelClose()
}
小结channel语法
  • channel
  • buffered channel
  • range channel可以close,注意一定是发送方close。接收方有2种判断方法
  • 理论基础:Communication Sequential Process(CSP)模式

channel的实际应用

  • Don't communicate by sharing memory; share memory by communicating.
  • 不要通过共享内存来通信;通过通信来共享内存

使用Channel等待任务结束

基于CSP,通过通信来共享内

范例:channel等待任务结束

// 创建channel/done/done.go

package main

import (
  "fmt"
)

func doWorker(id int, c chan int, done chan bool) {
  for n := range c {
    fmt.Printf("worker %d received %c\n",
      id, n)
    // 通知外部打印完毕,通过通信共享内存
    done <- true
  }
}

type worker struct { // 包装一下2个channel
  in   chan int
  done chan bool
}

func createWorker(id int) worker {
  w := worker{
    in:   make(chan int),
    done: make(chan bool),
  }
  go doWorker(id, w.in, w.done)
  return w
}

func chanDemo() {
  var workers [10]worker    //
  for i := 0; i < 10; i++ { // 开10个work接收channel数据
    workers[i] = createWorker(i)
  }

  // 向10个channel发数据
  for i := 0; i < 10; i++ {
    workers[i].in <- 'a' + i
    <-workers[i].done
  }
  for i := 0; i < 10; i++ {
    workers[i].in <- 'A' + i
    <-workers[i].done
  }

}

func main() {
  chanDemo()
}

#输出,为有顺序打印
PS D:\project\learngo> go run .\channel\done\done.go
worker 0 received a
worker 1 received b
worker 2 received c
worker 3 received d
worker 4 received e
worker 5 received f
worker 6 received g
worker 7 received h
worker 8 received i
worker 9 received j
worker 0 received A
worker 1 received B
worker 2 received C
worker 3 received D
worker 4 received E
worker 5 received F
worker 6 received G
worker 7 received H
worker 8 received I
worker 9 received J

上面代码示例是顺序打印的,不是我们要的,这不是并发执行,一个个打不就行了? 不希望发一个就等待channel结束,要等待全部结束再main函数中退出来

修改,去掉sleep

// channel/done/done.go

package main

import (
  "fmt"
)

func doWorker(id int, c chan int, done chan bool) {
  for n := range c {
    fmt.Printf("worker %d received %c\n",
      id, n)
    // 通知外部打印完毕,通过通信共享内存
    go func() { done <- true }() // 并行的done
  }
}

type worker struct { // 包装一下2个channel
  in   chan int
  done chan bool
}

func createWorker(id int) worker {
  w := worker{
    in:   make(chan int),
    done: make(chan bool),
  }
  go doWorker(id, w.in, w.done)
  return w
}

func chanDemo() {
  var workers [10]worker    //
  for i := 0; i < 10; i++ { // 开10个work接收channel数据
    workers[i] = createWorker(i)
  }

  // 向10个channels[i]发数据
  for i, worker := range workers {
    worker.in <- 'a' + i
  }
  for i, worker := range workers {
    worker.in <- 'A' + i
  }

  // 等待所有
  for _, worker := range workers {
    <-worker.done
    <-worker.done
  }
}

func main() {
  chanDemo()
}
  • 以及WaitGroup的使用

使用WaitGroup等待多人完成任务

范例:使用WaitGroup等待多人完成任务

// channel/done/done.go

package main

import (
  "fmt"
  "sync"
)

func doWorker(
  id int, c chan int, wg *sync.WaitGroup) {
  for n := range c {
    fmt.Printf("worker %d received %c\n",
      id, n)
    // 通知外部打印完毕,通过通信共享内存
    wg.Done()
  }
}

type worker struct { // 包装一下2个channel
  in chan int
  wg *sync.WaitGroup // 使用wg
}

func createWorker(
  id int, wg *sync.WaitGroup) worker {
  w := worker{
    in: make(chan int),
    wg: wg,
  }
  go doWorker(id, w.in, wg)
  return w
}

func chanDemo() {
  var wg sync.WaitGroup
  var workers [10]worker    //
  for i := 0; i < 10; i++ { // 开10个work接收channel数据
    workers[i] = createWorker(i, &wg)
  }

  // 向10个channels[i]发数据
  for i, worker := range workers {
    worker.in <- 'a' + i
    wg.Add(1)
  }
  for i, worker := range workers {
    worker.in <- 'A' + i
    wg.Add(1)
  }

  // 等待所有
  wg.Wait()
}

func main() {
  chanDemo()
}

包装一下waitgroup,抽象程度更高了一点

// channel/done/done.go

package main

import (
  "fmt"
  "sync"
)

func doWorker(
  id int, w worker) {
  for n := range w.in {
    fmt.Printf("worker %d received %c\n",
      id, n)
    // 通知外部打印完毕,通过通信共享内存
    w.done()
  }
}

type worker struct { // 包装一下2个channel
  in   chan int
  done func() // waitgroup包装成函数
}

func createWorker(
  id int, wg *sync.WaitGroup) worker {
  w := worker{
    in:   make(chan int),
    done: func() { wg.Done() },
  }
  go doWorker(id, w)
  return w
}

func chanDemo() {
  var wg sync.WaitGroup
  var workers [10]worker    //
  for i := 0; i < 10; i++ { // 开10个work接收channel数据
    workers[i] = createWorker(i, &wg)
  }

  // 向10个channels[i]发数据
  wg.Add(20)
  for i, worker := range workers {
    worker.in <- 'a' + i
  }
  for i, worker := range workers {
    worker.in <- 'A' + i
  }

  // 等待所有
  wg.Wait()
}

func main() {
  chanDemo()
}

使用Channel进行树的遍历

使用channel显示更序列化一些 范例:使用Channel进行树的遍历 使用

// 修改 tree/treentry/entry.go
func main() {
...

  // channel,统计最大的节点树
  c := root.TraversWithChannel()
  maxNode := 0
  for node := range c {
    if node.Value > maxNode {
      maxNode = node.Value
    }
  }
  fmt.Println("Max node value:", maxNode)
}

实现

// 修改添加 tree/traversal.go

func (node *Node) TraversWithChannel() chan *Node {
  out := make(chan *Node)
  go func() {
    node.TraverseFunc(func(n *Node) {
      out <- n
    })
    close(out)
  }()
  return out
}

使用Select来进行调度

范例:非阻塞式channel 选择select defautl

// 创建select/select.go

package main

import "fmt"

func main() {
  var c1, c2 chan int // c1 and c2 == nil
  // 同时收,谁收的快要谁
  select {  // channel值为nil, 在select中是拿不到数据的,会走default, 这是一个非阻塞式的channel
  case n := <-c1:
    fmt.Println("Received from c1:", n)
  case n := <-c2:
    fmt.Println("Received from c2:", n)
  default:
    fmt.Println("No value received") // No value received
  }
}

将从channel接收来的数据送给channel

// channel/select/select.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func generator() chan int { // 生成channel数据
  out := make(chan int)
  go func() {
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
      out <- i
      i++
    }
  }()
  return out
}

func worker(id int, c chan int) {
  for n := range c { // 判断channel是否有数据方法2
    fmt.Printf("worker %d received %d\n",
      id, n)
  }
}

func createWorker(id int) chan<- int { // 告知channel是向channel发数据的
  c := make(chan int)
  go worker(id, c)
  return c
}

func main() {
  var c1, c2 = generator(), generator()
  // 同时收,谁收的快要谁
  // 将收过来的 c 送给 worker
  var worker = createWorker(0)
  n := 0
  haseValue := false // 告诉是否有值
  for {
    var activeWorker chan<- int
    if haseValue {  // 有值时,将activeWorker激活
      activeWorker = worker
    }

    select {
    case n = <-c1: // 收数, 但不希望收一个数据再送给channel被阻塞住 w <- n
      haseValue = true //收到值,haseValue为true
    case n = <-c2:
      haseValue = true
    case activeWorker <- n: // 发数据, nil的chanel会被阻塞到
      haseValue = false  // 发送完 haseValue变为fasle
    }
  }
  // time.Sleep()
}

上面代码有一个问题:生成数据的channel和消耗数据的channel的速度是不一样的 如果生成的数据太快了,中间有些数据会冲掉

将收到数据排队,之后再让别人消耗它

// channel/select/select.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func generator() chan int { // 生成channel数据
  out := make(chan int)
  go func() {
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
      out <- i
      i++
    }
  }()
  return out
}

func worker(id int, c chan int) {
  for n := range c { // 判断channel是否有数据方法2
    time.Sleep(1 * time.Second) // 等待5秒
    fmt.Printf("worker %d received %d\n",
      id, n)
  }
}

func createWorker(id int) chan<- int { // 告知channel是向channel发数据的
  c := make(chan int)
  go worker(id, c)
  return c
}

func main() {
  var c1, c2 = generator(), generator()
  // 同时收,谁收的快要谁
  // 将收过来的 c 送给 worker
  var worker = createWorker(0)

  // 将收到数据排队,之后再让别人消耗它
  var values  []int

  for {
    var activeWorker chan<- int
    var activeValue int
    if len(values)  >0 {  // 有值时,将activeWorker激活
      activeWorker = worker
      activeValue = values[0]
    }

    select {
    case n := <-c1: // 收数, 但不希望收一个数据再送给channel被阻塞住 w <- n
      values = append(values, n)
    case n := <-c2:
      values = append(values, n)
    case activeWorker <- activeValue: // 发数据, nil的chanel会被阻塞到
      values = values[1:]
    }
  }
  // time.Sleep()

}

计时器的使用

// channel/select/select.go
package main

import (
  "fmt"
  "math/rand"
  "time"
)

func generator() chan int { // 生成channel数据
  out := make(chan int)
  go func() {
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
      out <- i
      i++
    }
  }()
  return out
}

func worker(id int, c chan int) {
  for n := range c { // 判断channel是否有数据方法2
    time.Sleep(1 * time.Second) // 等待5秒
    fmt.Printf("worker %d received %d\n",
      id, n)
  }
}

func createWorker(id int) chan<- int { // 告知channel是向channel发数据的
  c := make(chan int)
  go worker(id, c)
  return c
}

func main() {
  var c1, c2 = generator(), generator()
  // 同时收,谁收的快要谁
  // 将收过来的 c 送给 worker
  var worker = createWorker(0)

  // 将收到数据排队,之后再让别人消耗它
  var values  []int
  tm := time.After(10 * time.Second)
  tick := time.Tick(time.Second) // 定时功能。这里是每秒种送一个值
  for {
    var activeWorker chan<- int
    var activeValue int
    if len(values)  >0 {  // 有值时,将activeWorker激活
      activeWorker = worker
      activeValue = values[0]
    }

    select {
    case n := <-c1: // 收数, 但不希望收一个数据再送给channel被阻塞住 w <- n
      values = append(values, n)
    case n := <-c2:
      values = append(values, n)
    case activeWorker <- activeValue: // 发数据, nil的chanel会被阻塞到
      values = values[1:]
    case <- time.After(800 * time.Millisecond): // 800毫秒间没有数据打印timeout
      fmt.Println("timeout")
    case <- tick: // 定时器,每隔1秒打印value的长度
      fmt.Println("queue len=", len(values))
    case <-tm:    // 10 秒后结束
      fmt.Println("bye")
      return
    }
  }
  // time.Sleep()

}

#输出
PS D:\project\learngo> go run .\channel\select\select.go
queue len= 3
worker 0 received 0
queue len= 5
worker 0 received 0
queue len= 5
worker 0 received 1
queue len= 10
worker 0 received 1
queue len= 10
worker 0 received 2
queue len= 11
worker 0 received 2
queue len= 12
worker 0 received 3
queue len= 13
worker 0 received 3
queue len= 14
worker 0 received 4
bye

select小结

  • select的使用
  • 定时器的使用 3种定时器的使用方法
  • 在select中使用Nil Channel channel数据还没有准备好时,select就不会执行。

传统同步机制

  • WaitGroup 虽然里面是channel实现的,但看起来像传统的同步机制
  • Mutex 互斥量
  • Cond 用法类似

    一般使用channel通信

Mutex互斥量的使用

范例:

// basic/atomic/atomic.go

package main

import (
  "fmt"
  "time"
)

// 实现atomic int
type atomicInt int

func (a *atomicInt) increment() { // 自增长功能
  *a++
}

func (a *atomicInt) get() int {
  return int(*a)
}

func main() {
  // atomic.AddInt32() 系统中自带原子化的int,线程安全的
  var a atomicInt
  a.increment()
  go func() {
    a.increment()
  }()
  time.Sleep(time.Millisecond)
  fmt.Println(a)
}

# 输出 -race 打到数据冲突
# 27行读fmt.Println(a) 和 12行写*a++有冲突,
PS D:\project\learngo> go  run -race .\basic\atomic\atomic.go
==================
WARNING: DATA RACE
Read at 0x00c0000ac078 by main goroutine:
  main.main()
      D:/project/learngo/basic/atomic/atomic.go:27 +0xee

Previous write at 0x00c0000ac078 by goroutine 7:
  main.(*atomicInt).increment()
      D:/project/learngo/basic/atomic/atomic.go:12 +0x45
  main.main.func1()
      D:/project/learngo/basic/atomic/atomic.go:24 +0x2e

加锁

// basic/atomic/atomic.go

package main

import (
  "fmt"
  "sync"
  "time"
)

// 实现atomic int
type atomicInt struct {
  value int
  lock sync.Mutex  // 锁
}

func (a *atomicInt) increment() { // 自增长功能
  // 锁来保护
  a.lock.Lock()
  defer a.lock.Unlock()
  a.value++
}

func (a *atomicInt) get() int {
  a.lock.Lock()
  defer a.lock.Unlock()
  return int(a.value)
}

func main() {
  // atomic.AddInt32() 系统中自带原子化的int,线程安全的
  var a atomicInt
  a.increment()
  go func() {
    a.increment()
  }()
  time.Sleep(time.Millisecond)
  fmt.Println(a.get())
}


# 输出 -race 没有数据冲突了
PS D:\project\learngo> go  run -race .\basic\atomic\atomic.go
2

真正用时还得用系统自带的原子化操作。

代码区中锁保护:使用匿名函数

func (a *atomicInt) increment() { // 自增长功能
  // 在一段代码区,锁来保护.
  func() { // 使用匿名函数
    a.lock.Lock()
    defer a.lock.Unlock()
    a.value++
  }()
}

并发模式

从应用的角度解决类型的问题

channel可以做一个生成器

范例:channel做一个生成器 框架

// channel/pattern/main.go

package main

import "fmt"

func msgGen() chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据

  }()
  return c
}

func main() {
  m := msgGen() // 生成消息
  for {  // 不断获取消息
    fmt.Println(<-m)
  }
}

代码内容

// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen() chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
      c <- fmt.Sprintf("message %d", i)
      i++
    }

  }()
  return c
}

func main() {
  m1 := msgGen() // 生成消息
  m2 := msgGen()
  for {         // 不断获取消息
    fmt.Println(<-m1)
    fmt.Println(<-m2)
  }
}

服务/任务

channel也可以看作是一个服务的句柄handle,我们拿着它就可以跟服务去交互。

// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string) chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
      c <- fmt.Sprintf("service %s: message %d", name, i)
      i++
    }

  }()
  return c
}

func main() {
  m1 := msgGen("service1") // 生成消息
  m2 := msgGen("service2")
  for { // 不断获取消息
    fmt.Println(<-m1)
    fmt.Println(<-m2)
  }
}

# 输出
PS D:\project\learngo> go run -race .\channel\pattern\main.go
service service1: message 0
service service2: message 0
service service1: message 1
service service2: message 1
service service1: message 2

缺点:需要等待第一个m1服务生成后再等第二m2,这样交替进行。实际上我们希望同时去等待他们。

同时等待多个服务:两种方法

同时收到2个服务发的消息:统一收下消息发到第三channel上,再从第三个channel收消息

方法1 不确定有几个服务
// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string) chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
      c <- fmt.Sprintf("service %s: message %d", name, i)
      i++
    }

  }()
  return c
}

func fanIn(c1, c2 chan string) chan string {
  c := make(chan string)
  // 分别将c1 c2 数据取出送给c
  go func() {
    for {
      c <- <-c1
    }
  }()
  go func() {
    for {
      c <- <-c2
    }
  }()
  return c
}

func main() {
  m1 := msgGen("service1") // 生成消息
  m2 := msgGen("service2")
  m := fanIn(m1, m2) // 同时等待多服务消息
  for {              // 不断获取消息
    fmt.Println(<-m)
  }
}

# 输出 谁先有数据打印谁
PS D:\project\learngo> go run -race .\channel\pattern\main.go
service service1: message 0
service service2: message 0
service service1: message 1
service service2: message 1
service service1: message 2
service service1: message 3

不确定有几个服务

// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string) chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
      c <- fmt.Sprintf("service %s: message %d", name, i)
      i++
    }

  }()
  return c
}

func fanIn(chs ...chan string) chan string {
  c := make(chan string)
  // channel 循环变量的坑
  // for _, ch := range chs {
  //  go func() {
  //    for {
  //      c <- <-ch // 闭包中 ch 变量 引用外置变更有坑, ch只有range chs最后一个的值
  //    }
  //  }()
  // }

  // 解决1, 拷贝
  // for _, ch := range chs {
  //  chCopy := ch
  //  go func() {
  //    for {
  //      c <- <-chCopy
  //    }
  //  }()
  // }

  //  解决2,值传递
  for _, ch := range chs {
    go func(ch chan string) {
      for {
        c <- <-ch
      }
    }(ch)
  }

  return c
}

func main() {
  m1 := msgGen("service1") // 生成消息
  m2 := msgGen("service2")
  m := fanIn(m1, m2) // 同时等待多服务消息
  for {              // 不断获取消息
    fmt.Println(<-m)
  }
}
方法2:select 确定有几个服务
// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string) chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
      c <- fmt.Sprintf("service %s: message %d", name, i)
      i++
    }

  }()
  return c
}

func fanInBySelect(c1, c2 chan string) chan string {
  c := make(chan string)
  go func() {
    for {
      select {
      case m := <-c1:
        c <- m
      case m := <-c2:
        c <- m
      }
    }
  }()
  return c
}

func main() {
  m1 := msgGen("service1") // 生成消息
  m2 := msgGen("service2")
  m := fanInBySelect(m1, m2) // 同时等待多服务消息
  for {              // 不断获取消息
    fmt.Println(<-m)
  }
}

并发模式小结

  • channel可以做一个生成器 实践案例:搭建并行处理管道,感受GO语言魅力
  • 服务/任务
  • 同时等待多个服务:两种方法
    • 方法1:不确认有几个服务,range 循环变量坑
    • 方法2:知道有几个服务,select

并发任务的控制

  • 非阻塞等待
  • 超时机制
  • 任务中断/退出
  • 优雅退出

非阻塞等待

范例:

// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string) chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
      c <- fmt.Sprintf("service %s: message %d", name, i)
      i++
    }

  }()
  return c
}

func nonBlockingWait(c chan string) (string, bool) {
  select {
  case m := <-c:
    return m, true
  default:
    return "", false
  }
}

func main() {
  m1 := msgGen("service1") // 生成消息
  m2 := msgGen("service2")
  for {
    fmt.Println(<-m1)
    if m, ok := nonBlockingWait(m2); ok {
      fmt.Println(m)
    } else {
      fmt.Println("no message from service2")
    }
  }
}

# 输出
PS D:\project\learngo> go run -race .\channel\pattern\main.go
service service1: message 0
no message from service2
service service1: message 1
service service2: message 0

超时机制

范例:

// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string) chan string {
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      time.Sleep(time.Duration(rand.Intn(5000)) * time.Millisecond)
      c <- fmt.Sprintf("service %s: message %d", name, i)
      i++
    }

  }()
  return c
}

func timeoutWait(c chan string, timeout time.Duration) (string, bool) {
  select {
  case m := <-c:
    return m, true
  case <-time.After(timeout):
    return "", false
  }
}

func main() {
  m1 := msgGen("service1") // 生成消息
  for {
    if m, ok := timeoutWait(m1, 2 * time.Second); ok {
      fmt.Println(m)
    } else {
      fmt.Println("no message from service2")
    }
  }
}


# 输出
PS D:\project\learngo> go run -race .\channel\pattern\main.go
no message from service2
service service1: message 0
no message from service2
service service1: message 1

任务中断/退出

知道退出了 `done chan bool` 和`done chan struct{}` 都可以,struct{}比bool更省空间

范例:

// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string, done chan struct{}) chan string { //  done chan bool 和 done chan struct{} 都可以,struct{}比bool更省空间
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      select {
      case <- time.After(time.Duration(rand.Intn(5000)) * time.Millisecond):  // 5秒后没有收到done的数据就产生一条数据
        c <- fmt.Sprintf("service %s: message %d", name, i)
      case <-done:
        fmt.Println("cleaning up")
        return
      }
      i++
    }
  }()
  return c
}

func timeoutWait(c chan string, timeout time.Duration) (string, bool) {
  select {
  case m := <-c:
    return m, true
  case <-time.After(timeout):
    return "", false
  }
}

func main() {
  done := make(chan struct{})
  m1 := msgGen("service1", done) // 生成消息
  for i := 0; i < 5; i++ {
    if m, ok := timeoutWait(m1, time.Second); ok {
      fmt.Println(m)
    } else {
      fmt.Println("timeout")
    }
  }
  done <- struct{}{} // 退出;struct{}{}表示空的struct{}结构
  time.Sleep(time.Second)
}

# 输出
PS D:\project\learngo> go run -race .\channel\pattern\main.go
timeout
timeout
timeout
service service1: message 0
timeout
cleaning up

优雅退出

怎么知道清理做完了?有一个channel能告诉我们做完了

// channel/pattern/main.go

package main

import (
  "fmt"
  "math/rand"
  "time"
)

func msgGen(name string, done chan struct{}) chan string { //  done chan bool 和 done chan struct{} 都可以,struct{}比bool更省空间
  c := make(chan string)
  go func() { // 在goroutine中送数据
    i := 0
    for {
      select {
      case <- time.After(time.Duration(rand.Intn(5000)) * time.Millisecond):  // 5秒后没有收到done的数据就产生一条数据
        c <- fmt.Sprintf("service %s: message %d", name, i)
      case <-done:
        fmt.Println("cleaning up")
        time.Sleep(2 * time.Second)
        fmt.Println("cleaning done")
        done <- struct{}{} // 告诉做完了,大部分不会做成双向的
        return
      }
      i++
    }

  }()
  return c
}


func timeoutWait(c chan string, timeout time.Duration) (string, bool) {
  select {
  case m := <-c:
    return m, true
  case <-time.After(timeout):
    return "", false
  }
}

func main() {
  done := make(chan struct{})
  m1 := msgGen("service1", done) // 生成消息
  for i := 0; i < 5; i++ {
    if m, ok := timeoutWait(m1, time.Second); ok {
      fmt.Println(m)
    } else {
      fmt.Println("timeout")
    }
  }
  done <- struct{}{} // 退出;struct{}{}表示空的struct{}结构
  <-done // 收做完了的消息
}


# 输出
PS D:\project\learngo> go run -race .\channel\pattern\main.go
timeout
timeout
timeout
service service1: message 0
timeout
cleaning up
cleaning done

迷宫实现

迷宫算法-广度优先算法

深度优先算法有可能会绕远路

  • 为爬虫实战项目做好准备
  • 应用广泛,综合性强 涉及语言方面广度比较多,一门语言能够顺利的徒手写出广度优先算法走迷宫,说明已经对这门语言掌握比较熟练了。
  • 面试常见

    迷宫描述:

    • 6行5列,1代表墙,0代表路,左上角进,右下角出。
    • 规则:上下左右走,不能对角走
    • 算出最短路线

    思路: 起点为0,探索上下左右四个方向,发现通过一步能走的格子一共4个。 从起点走2步所能走的格子,不能往回走,从上方逆时针(上左下右90度90度转)探索能走的格子。

       3
      323
     32123
    3210123
     32123
      323
       3
    

每个点都有2种状态: 首先是0,发现有这个0但没有探索过的点,探索了那么周围就会有4个1 这个1是已经发现但没有探索过的点,中间的0是我们已经探索过的点 探索上方的1,发现周围有3个2,再探索左面的1,发现有2个2,等1探索完了才轮到2. 因此,每个节点都有:已经发现还未探索、已经发现且探索完成、没发现过,其中 “已经发现还未探索”的点需要排队,等什么时候办到你了再探索,不能直接探索,这 就是广度优先算法,一层一层往外递进,这样能到的点就是最短路径。

应用:

6 5
0 1 0 0 0
0 0 0 1 0
0 1 0 1 0
1 1 1 0 0
0 1 0 0 1
0 1 0 0 0

下面是0到5是下标,起点是0步能走到,其它都是未知的。

  0 1 2 3 4
0 起点0   4 5 6
1 1 2 3   7
2 2   4   8
3       10 9
4     12 11  
5     13 12 终点13

已经发现还没探索过的`(0,0)`放到队列中

  • 探索`(0,0)`,往下是能走的出现一个1,发现点`(1,0)`放进队列中;
  • 探索`(1,0)`,从列队中拿出来,2个2,这俩个节点通过2步能走到,将`(2,0),(1,1)`加入队列
  • 探索`(2,0)`,从列队中拿出来,左右都是墙且不能往回走,因此是条死路
  • 探索`(1,1)`,从列队中拿出来队列空掉了,可以往右走发现1个3,将点`(1,2)`加入队列
  • 探索`(1,2)`,从列队中拿出来队列空掉了,可以往上往下走发现2个4,将`(0,2),(2,2)`加入队列
  • 以此类似推,这条路就被画出来了,从终点倒过来就能画出最短路,13周围只有一个12等等等

广度优先算法结束条件:

  • 到终点结束
  • 走到死路,队列为空结束。

迷宫代码实现

创建迷宫图文件 maze/maze.in 6行5列

6 5
0 1 0 0 0
0 0 0 1 0
0 1 0 1 0
1 1 1 0 0
0 1 0 0 1
0 1 0 0 0

在vscode中,可以修改状态栏右下角的CRLF,改为LF。

读进迷宫文件

// maze/maze.go

package main

import (
    "fmt"
    "os"
)

func readMaze(filename string) [][]int { // 返回2维slice
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }

    var row, col int
    fmt.Fscanf(file, "%d %d", &row, &col) // 读进迷宫行和列

    maze := make([][]int, row) // 创建迷宫
    for i := range maze {
        maze[i] = make([]int, col)
        for j := range maze[i] {
            fmt.Fscanf(file, "%d", &maze[i][j]) // 每个点坐标位置
        }
    }
    return maze
}

func main() {
    m := readMaze("maze/maze.in")
    for _, row := range m {
        for _, col := range row {
            fmt.Printf("%d ", col)
        }
        fmt.Println()
    }
}

# 输出
PS D:\project\learngo> go run -race .\maze\maze.go
0 1 0 0 0
0 0 0 1 0
0 1 0 1 0
1 1 1 0 0
0 1 0 0 1
0 1 0 0 0

走迷宫

type point struct { // 走的点
    i, j int
}

func walk(maze [][]int, start, end point) {

}

walk(maze, point{0, 0}, point{len(maze) - 1, len(maze[0]) - 1})

代码:

// maze/maze.go

package main

import (
    "fmt"
    "os"
)

func readMaze(filename string) [][]int { // 返回2维slice
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }

    var row, col int
    fmt.Fscanf(file, "%d %d", &row, &col) // 读进迷宫行和列

    maze := make([][]int, row) // 创建迷宫
    for i := range maze {
        maze[i] = make([]int, col)
        for j := range maze[i] {
            fmt.Fscanf(file, "%d", &maze[i][j]) // 每个点坐标位置
        }
    }
    return maze
}

type point struct { // 走的点
    i, j int // i 纵坐标, j 横坐标
}

var dirs = []point{ // 定义 上左下右方向
    {-1, 0}, {0, -1}, {1, 0}, {0, 1}}

func (p point) add(r point) point {
    return point{p.i + r.i, p.j + r.j}
}

func (p point) at(grid [][]int) (int, bool) { // 同时看point有没有越界
    if p.i < 0 || p.i >= len(grid) { // i 行条件
        return 0, false
    }

    if p.j < 0 || p.j >= len(grid[p.i]) { // j 列条件
        return 0, false
    }

    return grid[p.i][p.j], true
}

func walk(maze [][]int,
    start, end point) [][]int {
    steps := make([][]int, len(maze))
    for i := range steps {
        steps[i] = make([]int, len(maze[i]))
    }

    Q := []point{start} // 起点加进队列中

    for len(Q) > 0 { // 退出条件,队列不空时
        // 探索点
        cur := Q[0]
        Q = Q[1:]

        if cur == end { // 是终点就退出
            break
        }

        for _, dir := range dirs { // 上左下右方向
            next := cur.add(dir) // 下一个节点坐标

            // 探索下一个节点,要保证下一个节点能走过才能探索 maze at next is 0
            // 并且保证下一个节点step值得有 and steps at next is 0
            // 不能回到起点 and next != start
            val, ok := next.at(maze)
            if !ok || val == 1 { // 值越界了或者撞墙了,断续看下一个点
                continue
            }

            val, ok = next.at(steps)
            if !ok || val != 0 {
                continue
            }

            if next == start {
                continue
            }

            curSteps, _ := cur.at(steps)
            steps[next.i][next.j] = curSteps + 1

            Q = append(Q, next)
        }
    }

    return steps
}

func main() {
    maze := readMaze("maze/maze.in")

    steps := walk(maze, point{0, 0}, point{len(maze) - 1, len(maze[0]) - 1})
    for _, row := range steps {
        for _, val := range row {
            fmt.Printf("%3d ", val)
        }
        fmt.Println()
    }
}

# 输出
PS D:\project\learngo> go run -race .\maze\maze.go
  0   0   4   5   6
  1   2   3   0   7
  2   0   4   0   8
  0   0   0  10   9
  0   0  12  11   0
  0   0  13  12  13

广度优先探索走迷宫小结

  • 用循环创建二维slice
  • 使用slice来实现队列
  • 用Fscanf读取文件
  • 对Point的抽象

标准库

http库

除了做服务器还可以做client

  • 使用http客户端发送请求
  • 使用http.Client控制请求头部等
  • 使用httputil简化工作

使用http客户端发送请求

// http/client.go

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    resp, err := http.Get("http://www.imooc.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    s, err := httputil.DumpResponse(resp, true)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%s\n", s)
}

使用http.Client控制请求头部等

访问手机版本

// http/client.go

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
    request.Header.Add("User-Agent",
        "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")

    client := http.Client{
        CheckRedirect: func(
            req *http.Request,
            via []*http.Request) error {
            fmt.Println("Redirect:", req)
            return nil
        },
    }
    // resp, err := http.DefaultClient.Do(request)
    resp, err := client.Do(request)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    s, err := httputil.DumpResponse(resp, true)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%s\n", s)
}


# 输出 可以看到重定向到https://m.imooc.com
PS D:\project\learngo> go run -race .\http\client.go
Redirect: &{GET https://www.imooc.com  0 0 map[Referer:[http://www.imooc.com] User-Agent:[Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1]] <nil> <nil> 0 [] false  map[] map[] <nil> map[]   <nil> <nil> 0xc000206000 0xc0000140c8}
Redirect: &{GET https://m.imooc.com  0 0 map[Referer:[https://www.imooc.com] User-Agent:[Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1]] <nil> <nil> 0 [] false  map[] map[] <nil> map[]   <nil> <nil> 0xc00012a000 0xc0000140c8}
HTTP/1.1 200 OK
Transfer-Encoding: chunked

使用httputil简化工作

使用`httputil.DumpResponse(resp, true)` 来简化工作,打印所有内容

http 服务器性能分析

  • `import _ "net/http/ppro"`
  • 访问 /debug/pprof
  • 使用`go tool pprof`分析性能
//修改 errhanding/filelistinserver/web.go
package main

import (
    "log"
    "net/http"
    _ "net/http/pprof" // 加载一些帮助程序
    "os"

    "xxx.com/jasper/learngo/errhandling/filelistingserver/filelisting"
)

func main() {
    http.HandleFunc("/", errWrapper(filelisting.HandleFileList)) // 统一错误处理|指定路径:将函数中业务逻辑提出去

访问url查看性能:http://localhost:8888/debug/pprof/ 30秒cpu性能输入命令:`go tool pprof http://localhost:8888/debug/pprof/profile` 再输入`web`从浏览器上用时 30秒内存使用:`go tool ppro http://localhost:6060/debug/pprof/heap`, 再输入`web`从浏览器上用时 可对服务进行相应优化

json库

JSON的解析

  • JSON数据格式
  • 结构体的tag 处理字段名问题
  • JSON Marshal与Unmarshal,数据类型
范例:转JSON数据格式`json.Marshal(o)`
// lang/json/main.go

package main

import "fmt"

type Order struct { // 订单
    ID         string
    Name       string
    Quantity   int
    TotalPrice float64
}

func main() {
    o := Order{
        ID:         "1234",
        Name:       "learn go",
        Quantity:   3,
        TotalPrice: 30,
    }

    fmt.Printf("%+v\n", o) // {ID:1234 Name:learn go Quantity:3 TotalPrice:30} 缺少引号
}

使用json打印

// lang/json/main.go

package main

import (
    "encoding/json"
    "fmt"
)

type Order struct { // 订单
    ID         string
    Name       string
    Quantity   int
    TotalPrice float64
}

func main() {
    o := Order{
        ID:         "1234",
        Name:       "learn go",
        Quantity:   3,
        TotalPrice: 30,
    }

    b, err := json.Marshal(o) // Marsha 将内存结构序列化成在网络上传输的字节
    if err != nil {
        panic(err)
    }

    fmt.Printf("%s\n", b) // 虽然 b 是字符byte的切片[]byte,但我们知道它里面是一个字符串,可以用%s来打印
}

# 输出
PS D:\project\learngo> go run  .\lang\json\main.go
{"ID":"1234","Name":"learn go","Quantity":3,"TotalPrice":30}

json在网络上传输是跨语言的,应用有自定义字段 给每个字段打上json的tag

// lang/json/main.go

type Order struct { // 订单
    ID         string  `json:"id"`
    Name       string  `json:"name,omitempty"` // 空的不省略,不打印
    Quantity   int     `json:"quantity"`
    TotalPrice float64 `json:"totalprice"`
}

func main() {
    o := Order{
        ID:         "1234",
        Quantity:   3,
        TotalPrice: 30,
    }

# 输出
PS D:\project\learngo> go run  .\lang\json\main.go
{"id":"1234","quantity":3,"totalprice":30}

更复杂的结构

// lang/json/main.go

# 可以是一个大的对象
type OrderItem struct {
    ID    string  `json:"id"`
    Name  string  `json:"name,omitempty"` // 空的不省略,不打印
    Price float64 `json:"price"`
}

type Order struct { // 订单
    ID         string    `json:"id"`
    Item       OrderItem `json:"Item"`
    Quantity   int       `json:"quantity"`
    TotalPrice float64   `json:"totalprice"`
}

func main() {
    o := Order{
        ID:         "1234",
        Quantity: 3,
        TotalPrice: 30,
        Item: OrderItem{
            ID: "item_1",
            Name: "learn go",
            Price: 15,
        },
    }

# 输出
PS D:\project\learngo> go run  .\lang\json\main.go
{"id":"1234","Item":{"id":"item_1","name":"learn go","price":15},"quantity":3,"totalprice":30}

# 可以是嵌入指针
type OrderItem struct {
    ID    string  `json:"id"`
    Name  string  `json:"name,omitempty"` // 空的不省略,不打印
    Price float64 `json:"price"`
}

type Order struct { // 订单
    ID         string    `json:"id"`
    Item       *OrderItem `json:"Item"` // 指向缓存的OrderItem
    Quantity   int       `json:"quantity"`
    TotalPrice float64   `json:"totalprice"`
}

func main() {
    o := Order{
        ID:         "1234",
        Quantity: 3,
        TotalPrice: 30,
        Item: &OrderItem{
            ID: "item_1",
            Name: "learn go",
            Price: 15,
        },
    }

# 输出
PS D:\project\learngo> go run  .\lang\json\main.go
{"id":"1234","Item":{"id":"item_1","name":"learn go","price":15},"quantity":3,"totalprice":30}

# 可以是数组
type Order struct { // 订单
    ID         string      `json:"id"`
    Items      []OrderItem `json:"Item"` // 指向缓存的OrderItem
    TotalPrice float64     `json:"totalprice"`
}

func main() {
    o := Order{
        ID:         "1234",
        TotalPrice: 20,
        Items: []OrderItem{
            {
                ID: "item_1",
                Name:  "learn go",
                Price: 15,
            },
            {
                ID: "item_2",
                Name:  "interview",
                Price: 10,
            },
        },
    }

# 输出
PS D:\project\learngo> go run  .\lang\json\main.go
{"id":"1234","Item":[{"id":"item_1","name":"learn go","price":15},{"id":"item_2","name":"interview","price":10}],"totalprice":20}

# 可以是指针数组
type Order struct { // 订单
    ID         string      `json:"id"`
    Items      []*OrderItem `json:"Item"` // 指向缓存的OrderItem
    TotalPrice float64     `json:"totalprice"`
}

func main() {
    o := Order{
        ID:         "1234",
        TotalPrice: 20,
        Items: []*OrderItem{
            {
                ID: "item_1",
                Name:  "learn go",
                Price: 15,
            },
            {
                ID: "item_2",
                Name:  "interview",
                Price: 10,
            },
        },
    }
# 输出结果是一样的
PS D:\project\learngo> go run  .\lang\json\main.go
{"id":"1234","Item":[{"id":"item_1","name":"learn go","price":15},{"id":"item_2","name":"interview","price":10}],"totalprice":20}
json解析

范例:生成JSON数据格式`json.Marshal(o)`

// lang/json/main.go

package main

import (
    "encoding/json"
    "fmt"
)

type OrderItem struct {
    ID    string  `json:"id"`
    Name  string  `json:"name,omitempty"` // 空的不省略,不打印
    Price float64 `json:"price"`
}

type Order struct { // 订单
    ID         string       `json:"id"`
    Items      []OrderItem `json:"Item"` // 指向缓存的OrderItem
    TotalPrice float64      `json:"totalprice"`
}

func main() {
    unmarshal()
}

func unmarshal() {
    s := `{"id":"1234","Item":[{"id":"item_1","name":"learn go","price":15},{"id":"item_2","name":"interview","price":10}],"totalprice":20}`
    var o Order // 定义一个order来接收
    err := json.Unmarshal([]byte(s), &o)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", o)
}

# 输出结果是一样的
PS D:\project\learngo> go run  .\lang\json\main.go
{ID:1234 Items:[{ID:item_1 Name:learn go Price:15} {ID:item_2 Name:interview Price:10}] TotalPrice:20}

第三方API数据格式的解析技巧

范例

res := `{
"data": [
    {
        "synonym":"",
        "weight":"0.6",
        "word": "真丝",
        "tag":"材质"
    },
    {
        "synonym":"",
        "weight":"0.8",
        "word": "韩都衣舍",
        "tag":"品牌"
    },
    {
        "synonym":"连身裙;联衣裙",
        "weight":"1.0",
        "word": "连衣裙",
        "tag":"品类"
    }
]
}`

使用map接收

// json解析
m := make(map[string]interface{})
err := json.Unmarshal([]byte(res), &m)
if err != nil {
        panic(err)
}

fmt.Printf("%+v\n", m) // map[data:[map[synonym: tag:材质 weight:0.6 word:真丝] map[synonym: tag:品牌 weight:0.8 word:韩都衣舍] map[synonym:连身裙;联衣裙 tag:品类 weight:1.0 word:连衣裙]]]

// 取出 "连身裙;联衣裙"
fmt.Printf("%+v\n",m["data"].([]interface{})[2].(map[string]interface{})["synonym"]   ) // 使用type assertion 告诉每个元素的类型

缺点:处理map时代码难懂

定义结构

// json解析
m := struct{
        Data []struct{
                Synonym string `json:"synonym"`
        } `json:"data"`
}{}
err := json.Unmarshal([]byte(res), &m)
if err != nil {
        panic(err)
}

fmt.Printf("%+v\n", m.Data[2].Synonym)

gin框架

go语言框架的设计思路基本都是一样的

  • `gin-gonic/gin`
  • middleware的使用 注册一些函数,让所有的请求都从middleware走过
  • context的使用 包括用户请求、反馈是什么样的、设置自己的key value

`gin-gonic/gin`

go get -u github.com/gin-gonic/gin

范例:简单演示

// lang/http/gindemo/ginserver.go

package main

import "github.com/gin-gonic/gin"

func main() {
        r := gin.Default()  // 返回一个gin的Engine
        r.GET("/ping", func(c *gin.Context) { // Engine有个Get方法,它注册到ping上面, 返回了个json
                c.JSON(200, gin.H{
                        "message": "pong",
                })
        })
        r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

# 请求 http://localhost:8080/ping
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2021/11/24 - 00:34:44 |?[97;42m 200 ?[0m|            0s |             ::1 |?[97;44m GET     ?[0m "/ping"

middleware的使用

不论访问哪个路径,一律进入到r.use中

范例:使用middleware统一的写日志

# 安装写日志的库zap
go get -u go.uber.org/zap

输出path

// lang/http/gindemo/ginserver.go

package main

import (
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

func main() {
    r := gin.Default()
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }

    r.Use(func(c *gin.Context) {
        // path, log latency, response code
        logger.Info("incoming request",
            zap.String("path", c.Request.URL.Path))

        c.Next()
    })

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })

    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "hello")
    })

    r.Run()
}

# 访求输出。请问http://localhost:8080/ping, http://localhost:8080/hello, http://localhost:8080/
{"level":"info","ts":1637686464.1075187,"caller":"gindemo/ginserver.go:17","msg":"incoming request","path":"/ping"}
[GIN] 2021/11/24 - 00:54:24 |?[97;42m 200 ?[0m|       994.2µs |             ::1 |?[97;44m GET     ?[0m "/ping"
{"level":"info","ts":1637686478.7833898,"caller":"gindemo/ginserver.go:17","msg":"incoming request","path":"/hello"}
[GIN] 2021/11/24 - 00:54:38 |?[97;42m 200 ?[0m|       290.7µs |             ::1 |?[97;44m GET     ?[0m "/hello"
{"level":"info","ts":1637686482.1059425,"caller":"gindemo/ginserver.go:17","msg":"incoming request","path":"/"}
[GIN] 2021/11/24 - 00:54:42 |?[90;43m 404 ?[0m|         788µs |             ::1 |?[97;44m GET     ?[0m "/"

耗时,状态

// lang/http/gindemo/ginserver.go

package main

import (
    "time"

    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

func main() {
    r := gin.Default()
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }

    r.Use(func(c *gin.Context) {
        s := time.Now() // 记录开始时间

        c.Next() // 它之后就可以 response code

        // path, response code, log latency
        logger.Info("incoming request",
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("elapsed", time.Now().Sub(s))) // 将结束时间记录进去
    })

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })

    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "hello")
    })

    r.Run()
}

# 访求输出。请问http://localhost:8080/ping, http://localhost:8080/hello, http://localhost:8080/
{"level":"info","ts":1637687286.6143937,"caller":"gindemo/ginserver.go:23","msg":"incoming request","path":"/","status":404,"elapsed":0}
[GIN] 2021/11/24 - 01:08:06 |?[90;43m 404 ?[0m|       998.5µs |             ::1 |?[97;44m GET     ?[0m "/"
{"level":"info","ts":1637687290.1554174,"caller":"gindemo/ginserver.go:23","msg":"incoming request","path":"/hello","status":200,"elapsed":0.0005609}
[GIN] 2021/11/24 - 01:08:10 |?[97;42m 200 ?[0m|       560.9µs |             ::1 |?[97;44m GET     ?[0m "/hello"
{"level":"info","ts":1637687294.1163,"caller":"gindemo/ginserver.go:23","msg":"incoming request","path":"/ping","status":200,"elapsed":0.000952}
[GIN] 2021/11/24 - 01:08:14 |?[97;42m 200 ?[0m|         952µs |             ::1 |?[97;44m GET     ?[0m "/ping"

context的使用

gin.Context实现了context 库Context接口

范例:将数字插入到每一个request中生成requestId

// lang/http/gindemo/ginserver.go

package main

import (
    "math/rand"
    "time"

    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

func main() {
    r := gin.Default()
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }

    r.Use(func(c *gin.Context) {
        s := time.Now() // 记录开始时间

        c.Next() // 它之后就可以 response code

        // path, response code, log latency
        logger.Info("incoming request",
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("elapsed", time.Now().Sub(s))) // 将结束时间记录进去
    }, func(c *gin.Context) {
        c.Set("requestId", rand.Int())
        c.Next()
    })

    r.GET("/ping", func(c *gin.Context) {

        h := gin.H{
            "message": "pong",
        }
        if rid, exists := c.Get("requestId"); exists { // 判断requestId在不在
            h["requestId"] = rid
        }
        c.JSON(200, h)
    })

    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "hello")
    })

    r.Run()
}

# 访求输出。浏览器请问http://localhost:8080/ping
{"message":"pong","requestId":3916589616287113937}

requestId常量

// lang/http/gindemo/ginserver.go

package main

import (
    "math/rand"
    "time"

    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

const keyRequestId = "requestId"

func main() {
    r := gin.Default()
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }

    r.Use(func(c *gin.Context) {
        s := time.Now() // 记录开始时间

        c.Next() // 它之后就可以 response code

        // path, response code, log latency
        logger.Info("incoming request",
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("elapsed", time.Now().Sub(s))) // 将结束时间记录进去
    }, func(c *gin.Context) {
        c.Set(keyRequestId, rand.Int())
        c.Next()
    })

    r.GET("/ping", func(c *gin.Context) {

        h := gin.H{
            "message": "pong",
        }
        if rid, exists := c.Get(keyRequestId); exists { // 判断requestId在不在
            h[keyRequestId] = rid
        }
        c.JSON(200, h)
    })

    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "hello")
    })

    r.Run()
}

# 访求输出。浏览器请问http://localhost:8080/ping
{"message":"pong","requestId":3916589616287113937}