Golang Advanced Features

面向函数编程

基本概念

变量、参数、返回值可以是函数

实现累加,维持了sum自由变量,函数返回的是整体闭包

g0

package main

import "fmt"

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


func main() {
    // a := adder() is trivial and also works.
    a := adder()
    for i := 0; i < 10; i++ {
        fmt.Printf("0 + 1 + ... + %d = %d\n",i, a(i))
    }
}

斐波那契数列

package fib

// 1, 1, 2, 3, 5, 8, 13, ...
func Fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

为函数实现接口

package main

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

    "imooc.com/ccmouse/learngo/functional/fib"
)

type intGen func() int  //type struct is a function return int

func (g intGen) Read(
    p []byte) (n int, err error) {
    next := g()
    if next > 10000 {
        return 0, io.EOF
    }
    s := fmt.Sprintf("%d\n", next)

    // TODO: incorrect if p is too small!
    return strings.NewReader(s).Read(p)
}

func printFileContents(reader io.Reader) {
    scanner := bufio.NewScanner(reader)

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

func main() {
    var f intGen = fib.Fibonacci()
    printFileContents(f)
}

遍历二叉树

常规遍历,不能改动遍历时的操作

func (node *Node) Traverse() {
    if node == nil {
        return
    }

    node.Left.Traverse()
    node.Print()
    node.Right.Traverse()
}

使用函数式编程遍历

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)
}

错误处理与资源管理

defer调用

确保调用在函数结束时发生,defer本身是栈结构,先进后出,同时确保会执行

package main

import (
    "fmt"
    "os"

    "bufio"

    "imooc.com/ccmouse/learngo/functional/fib"
)

func tryDefer() {
    for i := 0; i < 100; i++ {
        defer fmt.Println(i)
        if i == 30 {
            // Uncomment panic to see
            // how it works with defer
            // panic("printed too many")
        }
    }
}

func writeFile(filename string) {
    file, err := os.OpenFile(filename,
        os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0666)

    if err != nil {
        if pathError, ok := err.(*os.PathError); !ok {
            panic(err)
        } else {
            fmt.Printf("%s, %s, %s\n",
                pathError.Op,
                pathError.Path,
                pathError.Err)
        }
        return
    }
    defer file.Close()

    writer := bufio.NewWriter(file)
    defer writer.Flush()

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

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

参数在defer语句中计算

error会使程序挂掉(panic)

g1

统一错误处理

实现一个简单的webserver

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
    "os"

    "/filelisting"
)

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) {
        // panic
        defer func() {
            if r := recover(); r != nil {
                log.Printf("Panic: %v", r)
                http.Error(writer,
                    http.StatusText(http.StatusInternalServerError),
                    http.StatusInternalServerError)
            }
        }()

        err := handler(writer, request)

        if err != nil {
            log.Printf("Error occurred "+
                "handling request: %s",
                err.Error())

            // user error
            if userErr, ok := err.(userError); ok {
                http.Error(writer,
                    userErr.Message(),
                    http.StatusBadRequest)
                return
            }

            // system error
            code := http.StatusOK
            switch {
            case os.IsNotExist(err):
                code = http.StatusNotFound
            case os.IsPermission(err):
                code = http.StatusForbidden
            default:
                code = http.StatusInternalServerError
            }
            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)
    }
}
package filelisting

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

const prefix = "/list/"

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 {
    fmt.Println()
    if strings.Index(
        request.URL.Path, prefix) != 0 {
        return userError(
            fmt.Sprintf("path %s must start "+
                "with %s",
                request.URL.Path, prefix))
    }
    path := request.URL.Path[len(prefix):]
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close()

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

    writer.Write(all)
    return nil
}

通过errWrapper统一处理

panic&recover

panic致命错误,谨慎使用

停止当前函数执行(如果没有recover),但是会用到defer

recover,仅在defer调用中使用,可以获取panic,如果无法处理,可以重新panic

package main

import (
    "fmt"
)

func tryRecover() {
    defer func() {
        r := recover()
        if r == nil {
            fmt.Println("Nothing to recover. " +
                "Please try uncomment errors " +
                "below.")
            return
        }
        if err, ok := r.(error); ok {
            fmt.Println("Error occurred:", err)
        } else {
            panic(fmt.Sprintf(
                "I don't know what to do: %v", r))
        }
    }()
    // define and use the unkonw func
    // Uncomment each block to see different panic
    // scenarios.
    // Normal error
    //panic(errors.New("this is an error"))

    // Division by zero
    //b := 0
    //a := 5 / b
    //fmt.Println(a)

    // Causes re-panic
    panic(123)
}

func main() {
    tryRecover()
}

测试与性能调优

表格测试

传统测试存在的问题:

  • 测试数据和测试逻辑混在一起
  • 出错信息不明确
  • 一旦一步出错,测试便结束

g2

表格驱动测试:

  • 分离的数据和逻辑
  • 明确的出错信息
  • 可以部分失败
  • 十分适合golang

g3

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, 50000},
    }

    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 .

golang还可以查看代码覆盖率

go test -coverprofile=c.out

go test -bench .

查找性能瓶颈:

go test -bench . -cpuprofile cpu.out

go tool pprof cpu.out

web

g4

httpserver测试demo

package main

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

func errPanic(_ http.ResponseWriter,
    _ *http.Request) error {
    panic(123)
}

type testingUserError string

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

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

func errUserError(_ http.ResponseWriter,
    _ *http.Request) error {
    return testingUserError("user error")
}

func errNotFound(_ http.ResponseWriter,
    _ *http.Request) error {
    return os.ErrNotExist
}

func errNoPermission(_ http.ResponseWriter,
    _ *http.Request) error {
    return os.ErrPermission
}

func errUnknown(_ http.ResponseWriter,
    _ *http.Request) error {
    return errors.New("unknown error")
}

func noError(writer http.ResponseWriter,
    _ *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"},
    {errNoPermission, 403, "Forbidden"},
    {errUnknown, 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.imooc.com", nil)
        f(response, request)

        verifyResponse(response.Result(),
            tt.code, tt.message, t)
    }
}

func TestErrWrapperInServer(t *testing.T) {
    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,
    expectedCode int, expectedMsg string,
    t *testing.T) {
    b, _ := ioutil.ReadAll(resp.Body)
    body := strings.Trim(string(b), "\n")
    if resp.StatusCode != expectedCode ||
        body != expectedMsg {
        t.Errorf("expect (%d, %s); "+
            "got (%d, %s)",
            expectedCode, expectedMsg,
            resp.StatusCode, body)
    }
}

生成文档

go doc

godoc -http :6060

注释没有语法,相应位置随便写即可,自动识别

测试时加入example,注释output可以自动识别

g5

g6

Goroutine

原生支持并发编程

go demo

增加关键字go即可并发执行

package main

import (
    "fmt"
    "time"
)

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

注意:传入参数i较为安全,否择数据冲突

无论线程多少,都可以支持,go原生支持了协程概念(其他语言需要实现NIO等)

协程Coroutine

  • 轻量级线程
  • 非抢占式多任务处理、由协程主动交出控制权
  • 是编译器/解释器/虚拟机层面的多任务,非OS层
  • 多个协程可以在一个或者多个线程上运行,由golang调度

必须手动交出控制权(IO无须),否择死循环

runtime.Gosched()

g7

JVM不支持协程

g8

go关键字会将函数交给goroutine调度器

可以适用go -race查看冲突

goroutine可能的切换点:

  • IO/select
  • runtime.Gosched()
  • channel
  • 函数调用(有时)
  • 等待锁
  • 仅为可能性参考,不保证

Channel

goroutine之间的双向通道为channel

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

g9

chan也可以作为传参和返回值

通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

package main

import "fmt"

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
        // 缓冲区大小为2
        ch := make(chan int, 2)

        // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
        // 而不用立刻需要去同步读取数据
        ch <- 1
        ch <- 2

        // 获取这两个数据
        fmt.Println(<-ch)
        fmt.Println(<-ch)
}

Go 遍历通道与关闭通道

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。

package main

import (
        "fmt"
)

func fibonacci(n int, c chan int) {
        x, y := 0, 1
        for i := 0; i < n; i++ {
                c <- x
                x, y = y, x+y
        }
        close(c)
}

func main() {
        c := make(chan int, 10)
        go fibonacci(cap(c), c)
        // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
        // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
        // 会结束,从而在接收第 11 个数据的时候就阻塞了。
        for i := range c {
                fmt.Println(i)
        }
}

执行输出结果为:

0
1
1
2
3
5
8
13
21
34

Demo:

package main

import (
    "fmt"
    "time"
)

func worker(id int, c chan int) {
    for n := range c {
        fmt.Printf("Worker %d received %c\n",
            id, n)
    }
}
//can only send data to chan
func createWorker(id int) chan<- int {
    c := make(chan int)
    go worker(id, c)
    return c
}

func chanDemo() {
    //only can receive data from chan
    var channels [10]chan<- int
    for i := 0; i < 10; i++ {
        channels[i] = createWorker(i)
    }

    for i := 0; i < 10; i++ {
        channels[i] <- 'a' + i
    }

    for i := 0; i < 10; i++ {
        channels[i] <- 'A' + i
    }

    time.Sleep(time.Millisecond)
}

func bufferedChannel() {
    c := make(chan int, 3)
    //add buffer 3 to chan
    go worker(0, c)
    c <- 'a'
    c <- 'b'
    c <- 'c'
    c <- 'd'
    //over 3 died lock
    time.Sleep(time.Millisecond)
}

func channelClose() {
    c := make(chan int)
    go worker(0, c)
    c <- 'a'
    c <- 'b'
    c <- 'c'
    c <- 'd'
    close(c)
    //can close or not
    time.Sleep(time.Millisecond)
}

func main() {
    fmt.Println("Channel as first-class citizen")
    chanDemo()
    fmt.Println("Buffered channel")
    bufferedChannel()
    fmt.Println("Channel close and range")
    channelClose()
}

传输并发任务是否完成的信号,定义新的chan done,替代sleep

package main

import (
    "fmt"
    "sync"
)

func doWork(id int,
    w worker) {
    for n := range w.in {
        fmt.Printf("Worker %d received %c\n",
            id, n)
        w.done()
    }
}

type worker struct {
    in   chan int
    done func()
}

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

func chanDemo() {
    var wg sync.WaitGroup

    var workers [10]worker
    for i := 0; i < 10; i++ {
        workers[i] = createWorker(i, &wg)
    }

    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()
}

通过通信来共享内存,而非通过共享内存而通信(CSP),使用waitgroup确保任务完成

select调度

优先接受先到达的数据

g10

http库

client demo:

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 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1")

    client := http.Client{
        CheckRedirect: func(
            req *http.Request,
            via []*http.Request) error {
            fmt.Println("Redirect:", req)
            return nil
        },
    }
    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)
}

广度优先遍历与迷宫问题

迷宫问题,1墙0路,两端为出入口,寻找最短路径

g11

g12

终止条件:

  • 到达终点
  • 前进队列为空(死路)
package main

import (
    "fmt"
    "os"
)

func readMaze(filename string) [][]int {
    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
}

var dirs = [4]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) {
    if p.i < 0 || p.i >= len(grid) {
        return 0, false
    }

    if p.j < 0 || p.j >= len(grid[p.i]) {
        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)

            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()
    }

    // TODO: construct path from steps
}
  • Copyrights © 2019-2020 Rex