likes
comments
collection
share

Golang 语言进阶指南

作者站长头像
站长
· 阅读数 9

背景

当前领域各成员的开发水平参差不齐,大部分同学没有深度使用过 golang,为了让大家对 golang 有更深入的了解,写出更高质量的代码,故在此结合平时的自动化 review 和代码指导经验,下面从以下四大模块给大家分享一下 golang 的进阶使用场景。

本文档提供的程序可以在 Playground 进行调试 go.dev/play

函数&方法

在 Go 语言中,函数和方法有明确的区分,函数是指不属于任何结构体、类型的方法;也就是说,函数是没有接收者的,而方法是有接收者的,一般方法的接收者是一个结构体类型。

// 函数
func hello(c *Client) {

}
// 方法
func (c *Client) hello() {

}

可变参数

可变参数放在参数列表最后一个,且只支持一个可变参数

// 使用 ... (三个点)就可以实现可变参数
func funcName(arg ...type){

}

参数传递

值传递:

值传递实际上就是一份拷贝,函数内部对该值的修改,不会影响函数外部的值

func main(){
    x := 16
    fmt.Println("修改前 x=", x)  //16
    // 调用外部修改函数
    changeX(x)
    fmt.Println("修改后 x=", x)  //16,没有改变
}

func changeX(x int) {
    x = 100
    fmt.Println("修改时 x=", x) //100
}

引用传递:

引用传递本质上也是值传递,只不过这份值是一个指针(地址)。 所以我们在函数内对这份值的修改,其实不是改这个值,而是去修改这个值所指向的数据,所以是会影响到函数外部的值的。

func main(){
    x := 16
    fmt.Println("修改前 x=", x)  //16
    // 调用外部修改函数
    changeX(&x)
    fmt.Println("修改后 x=", x)  //100,被修改了
}

func changeX(x *int) {
    *x = 100
    fmt.Println("修改时 x=", x) //100
}

传指针使得多个函数能操作同一个对象

传指针比较轻量级(8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次 copy 上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。

Go语言中 slice ,map 和 channel 这三种类型的实现机制类似指针, 所以可以直接传递,而不用取地址后传递指针。

返回值

返回值可以起别名直接初始化,避免初始化多个返回值

func hello(x, y string)(res string, err error){
    return
}

延迟函数

可以在函数中添加多个 defer 语句。当函数执行到最后时,这些 defer 语句会按照逆序执行,最后该函数返回。

特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。

func (p *TradeAPI) NewGetBalanceStatus(c context.Context, _req *GetBalanceStatusReq) (*NewGetBalanceStatusResp, error) {
    var buf bytes.Buffer
    if err := json.NewEncoder(&buf).Encode(_req); err != nil {
       return nil, err
    }

    req, err := http.NewRequest(http.MethodPost, tradeBalanceHost+"status", &_body)
    if err != nil {
       return nil, err
    }
    resp, err := p.balanceClient.Do(req)
    if err != nil {
       return nil, err
    }
    defer func() {
       _ = resp.Body.Close()
    }()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
       return nil, err
    }

    var statusResp *NewGetBalanceStatusResp
    if err := json.Unmarshal(body, &statusResp); err != nil {
       return nil, err
    }

    return statusResp, nil
}

匿名函数

Go 语言支持函数式编程:

  • 将匿名函数作为另一个函数的参数:回调函数
  • 将匿名函数作为另一个函数的返回值:闭包
func main() {
    // 匿名函数
    func (){
        fmt.Println("我是一个匿名函数")
    }()
    
    // 用一个变量来接收一个匿名函数,就可以再它的作用域内多次调用该函数
    fun1 := func(){
        fmt.Println("我也是一个匿名函数。。")
    }
    fun1()
    fun1()
    
    // 定义带参数的匿名函数
    func(a, b int) {
        fmt.Println(a, b)
    }(1, 2)
    
    // 定义带返回值的匿名函数
    res1 := func(a, b int) int {
        return a + b
    }(10, 20) //匿名函数调用了,将执行结果给res1
    fmt.Println(res1)
}
balanceReq := &trade_balance.BalanceKeepAcctReq{
    ReqSeqNo: fmt.Sprintf("nubela%s%s", time.Now().Format("20060102150405"), util.RandString(12)),
    ReqTime:  time.Now().Format(proto.TimeFormat),
    BizType: func() int32 {
       if amount < 0 {
          return 5
       }
       return 2
    }(),
    Operator:    "paastob_qa_autotest", // 操作人
    ReqDetailList: []*trade_balance.BalanceReqDetailIn{
       {
          Type: func() string {
             if amount < 0 {
                return "O"
             }
             return "I"
          }(),
          Amount: func() string {
             if amount < 0 { // 负数转正数
                return req.RechargeAmount[1:]
             }
             return req.RechargeAmount
          }(),
          PayChannel: "BALANCE",
       },
    },
}

回调函数

回调函数:callback,就是将一个函数 func2 作为函数 func1 的一个参数。那么 func2 叫做回调函数,func1 叫做高阶函数。

type Callback func(x, y int) int

// 根据传入的回调函数进行算术运算
func oper(a, b int, callbackFunc Callback) int {
    res := callbackFunc(a, b)
    return res
}

// 加法运算回调函数
func add(a, b int) int {
    return a + b
}

// 减法运算回调函数
func sub(a, b int) int {
    return a - b
}

func main() {
    // 将 add 作为回调函数传入 oper
    res1 := oper(10, 20, add)
    fmt.Println(res1)

    // 将 sub 作为回调函数传入 oper
    res2 := oper(5, 2, sub)
    fmt.Println(res2)
}
func main() {
    strs := []string{"apple", "orange", "banana", "pear"}
    sort.Slice(strs, func(i, j int) bool {
        return strs[i] < strs[j]
    })
    fmt.Println(strs) // [apple banana orange pear]

    sort.Slice(strs, func(i, j int) bool {
        return strs[i] > strs[j]
    })
    fmt.Println(strs) // [pear orange banana apple]
}

许多官方库就使用了回调函数的思想,把灵活的处理逻辑交给用户自身来实现,这样代码的可定制化大大增强;比如上面的排序函数,由于每个业务场景都有不同的排序诉求,所以把具体的排序实现交给用户,让用户去实现回调函数逻辑。

闭包

闭包:一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量,并且该外层函数的返回值就是这个内层函数。那么这个内层函数和外层函数的局部变量,统称为闭包结构。

闭包 = 函数 + 引用环境


func increment() func() int { //外层函数
    //1. 定义了一个局部变量
    i := 0
    //2. 定义了一个匿名函数,给变量自增并返回
    fun := func() int { //内层函数
        i++
        return i
    }
    //3. 返回该匿名函数
    return fun
}

func main() {
    res1 := increment()      // res1 = fun
    fmt.Printf("%T\n", res1) // func() int
    
    //  带括号表示自执行函数 res1,得到返回结果 v1
    v1 := res1()
    fmt.Println(v1) // 1
    //  再次执行 res1,得到返回结果 v2
    v2 := res1()
    fmt.Println(v2)     // 2
    fmt.Println(res1()) // 3
    fmt.Println(res1()) // 4
    fmt.Println(res1()) // 5
    fmt.Println(res1()) // 6

    // 用一个新的变量来接收 increment() 的返回结果
    // 这个时候 increment 函数又重新执行了一遍
    res2 := increment()
    fmt.Printf("%T\n", res2) // func() int
    // 执行 res2
    v3 := res2()
    fmt.Println(v3)     // 1
    fmt.Println(res2()) // 2

    // res1 和 res2 没什么关系
    fmt.Println(res1()) // 7
}

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在。

隔离数据:假设你想创建一个函数,该函数可以访问即使在函数退出后仍然存在的数据。举个例子,如果你想统计函数被调用的次数,但不希望其他任何人访问该数据(这样他们就不会意外更改它),你就可以用闭包来实现它

// 函数计数器
func counter(f func()) func() int {
    n := 0
    return func() int {
        f()
        n += 1
        return n
    }
}

// 测试的调用函数
func foo() {
    fmt.Println("call foo")
}

func main() {
    cnt := counter(foo)
    cnt()
    cnt()
    cnt()
    fmt.Println(cnt())
}

还可以通过闭包的记忆效应来实现设计模式中工厂模式的生成器

// 定义一个bytedancer生成器,输入名称,返回新的用户数据
func genBytedancer(name string) func() (string, int) {
    // 定义字节范分数
    style := 100
    // 返回闭包
    return func() (string, int) {
        // 引用了外部的 style 变量, 形成了闭包
        return name, style
    }
}

func main() {
    // 创建一个bytedancer生成器
    generator := genBytedancer("bytedance001")

    // 返回新创建bytedancer的姓名, 字节范分数
    name, style := generator()
    fmt.Println(name, style)
}

闭包具有面向对象语言的特性 —— 封装****性,变量 style 无法从外部直接访问和修改。

并发编程

背景知识

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

Goroutine 是官方实现的超级“线程池”,每个实例占用4~5kb的栈空间且极少的创建销毁开销是go高并发的根本原因。

并发是通过切换时间片来实现“并行”运行,go可以设置使用核心数*runtime.GOMAXPROCS*,发挥多核主机能力。

初探并发

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

func hello() {
    fmt.Println("Hello Goroutine!")
}

func main() {
    hello()
    fmt.Println("main goroutine done!")
}

func main1() {
    go hello() // 启动一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

func main2() {
    go hello()
    fmt.Println("main goroutine done!")
    time.Sleep(time.Second) // 等待hello函数返回
}

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,所以在实际使用goroutine时需要特别注意其调度。

多个goroutine

func hello(i int) {
    fmt.Println("Hello Goroutine!", i)
}

func main() {
    for i := 0; i < 10; i++ {
        go hello(i)
    }
    time.Sleep(time.Second)
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

当goroutine遇上loop

func hello(i int) {
    fmt.Println("Hello Goroutine!", i)
}

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            hello(i)
        }()
    }
    time.Sleep(time.Second)
}

由于闭包只是绑定到i变量上,并没有保存到goroutine栈中,这样写会导致for循环结束后才执行goroutine多线程操作,这时候value值指向了最后一个元素,所以上面代码极大可能都是输出了最后一个元素。

  1. 通过参数传递数据到协程
func hello(i int) {
    fmt.Println("Hello Goroutine!", i)
}

func main() {
    for i := 0; i < 10; i++ {
        go func(idx int) {
            hello(idx)
        }(i)
    }
    time.Sleep(time.Second)
}

这里将idx作为一个参数传入goroutine中,每个idx都会被独立计算并保存到goroutine的栈中

  1. 定义临时变量
func hello(i int) {
    fmt.Println("Hello Goroutine!", i)
}

func main() {
    for i := 0; i < 10; i++ {
        val := i
        go func() {
            hello(val)
        }()
    }
    time.Sleep(time.Second)
}

另一种方法是在循环内定义新的变量,由于在循环内定义的变量在循环遍历的过程中是不共享的,因此也可以达到同样的效果

并发同步

在代码中生硬的使用time.Sleep肯定是不合适的,我们推荐使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

方法名功能
(wg * WaitGroup) Add(n int)计数器+n
(wg *WaitGroup) Done()计数器-1
(wg *WaitGroup) Wait()阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

我们利用sync.WaitGroup将上面的代码优化一下:

var wg sync.WaitGroup

func hello() {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!")
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    fmt.Println("main goroutine done!")
    wg.Wait() // 等待所有登记的goroutine都结束
}

并发安全

有时候会存在多个goroutine同时操作一个资源(临界区),这种情况就会发生数据竞态问题。类比卫生间被整层楼同性别同学竞争使用的场景。

var x int64
var wg sync.WaitGroup

func add() {
    defer wg.Done()
    for i := 0; i < 1000000; i++ {
        x = x + 1
    }
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync.Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    defer wg.Done()
    for i := 0; i < 1000000; i++ {
        lock.Lock() // 加锁
        x = x + 1
        lock.Unlock() // 解锁
    }
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

读写互斥锁

互斥锁是完全互斥的,实际上我们更多的场景是读多写少的,当我们并发的读取不涉及修改的资源是没必要加锁的,这时我们使用读写锁sync.RWMutex是一种更好的选择。

读写锁分为读锁和写锁。

当一个goroutine获取读锁之后,其他的goroutine如果获取读锁会继续获得锁,若果获取写锁就会等待;

当一个goroutine获取写锁之后,其他的goroutine无论获取读锁还是写锁均会等待。

互斥锁

var (
    x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex
)

func write() {
    lock.Lock()   // 加互斥锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    lock.Unlock()                     // 解互斥锁
    wg.Done()
}

func read() {
    lock.Lock()                  // 加互斥锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    lock.Unlock()                // 解互斥锁
    wg.Done()
}

func main() {
    start := time.Now()
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write()
    }

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go read()
    }

    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
}

读写锁

var (
    x      int64
    wg     sync.WaitGroup
    rwlock sync.RWMutex
)

func write() {
    rwlock.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    rwlock.Unlock()                   // 解写锁
    wg.Done()
}

func read() {
    rwlock.RLock()               // 加读锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    rwlock.RUnlock()             // 解读锁
    wg.Done()
}

func main() {
    start := time.Now()
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write()
    }

    // 读多写少场景
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go read()
    }

    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
}

我们可以看到读写锁非常适合读多写少的场景,如果读和写操作差别不大,读写锁的优势就发挥不出来。

并发通信

单纯的将函数并发执行是没有使用场景的,函数与函数之间需要交换数据才能真正体现并发执行的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发现竞态问题。为了保证数据交换的正确性,需要使用互斥量对内存进行加锁,但这种使用姿势不够灵活且容器造成性能问题。

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

channel是go并发的通信桥梁,可以让一个goroutine发送特定值到另一个goroutine进行通信。channel遵循先进先出(FIFO)机制,保证收发数据的顺序,每个channel都是一个具体类型的通道,声明时需要指定其元素类型。

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作

发送和接收都使用 <- 操作符,关闭使用内置的close函数。

// 初始化一个channel
ch := make(chan int)

// 发送操作
ch <- 10 // 把10发送到ch中

// 接收操作
x := <- ch // 从ch中接收值并赋值给变量x

// 关闭操作
close(ch)

对于channel关闭操作,需要注意的是,只有在通知接收方goroutine把所有的数据都发送完毕的时候才需要关闭通道。且channel是可以被GC机制回收掉的,所以关闭通道不是必须操作的。

channel常见操作整理

操作/状态nil非空未满已关闭
发送阻塞发送值发送值发送值阻塞panic
接收阻塞阻塞接收值接收值接收值读取完毕数据后返回零值
关闭panic关闭成功返回零值关闭成功,读取完毕数据后返回零值关闭成功,读取完毕数据后返回零值关闭成功,读取完毕数据后返回零值panic

无缓冲channel

无缓冲的通道又称为阻塞的通道。

func main() {
    ch := make(chan string)
    ch <- "bytedance"
    fmt.Println("发送成功")
}

上面这段代码能够编译通过,但是执行的时候会出现 deadlock 错误;为啥那么会出现该错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收值的时候才能发送值。简单来说就是无缓冲的通道必须有接收才能发送。

左边的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}

func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲channel

解决上面问题的方法还有一种就是使用有缓冲区的通道。

我们可以在使用make函数初始化通道的时候为其指定通道的容量

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。

标准库

Context

我们能发现调用大部分外部仓库方法时,第一个参数都是ctx context.Context,包括公共库的大部分接口设计也是遵循该规范。

标准要求:每个方法的第一个参数都将 context 作为第一个参数,并使用 ctx 变量名惯用语。

Context接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

context 接口包含四个方法:

  • Deadline(): 返回绑定当前 context 的任务被取消的截止时间,如果没有设定期限,将返回 ok == false

  • Done(): 当当前的 context 被取消时,将返回一个关闭的 channel,如果当前 context 不会被取消,将返回 nil

  • Err():

    • 如果 Done() 返回的 channel 没有关闭,将返回 nil
    • 如果 Done() 返回的 channel 已经关闭,将返回非空的值表示任务结束的原因;
    • 如果是 context 被取消,Err() 将返回 Canceled
    • 如果是 context 超时,Err() 将返回 DeadlineExceeded
  • Value(): 返回 context 存储的键值对中当前 key 对应的值,如果没有对应的 key,则返回 nil

可以看到 Done() 方法返回的 channel 正是用来传递结束信号以抢占并中断当前任务;Deadline()方法表示一段时间后当前 goroutine 是否会被取消;以及一个Err()方法,来解释 goroutine 被取消的原因;而 Value() 则用于获取特定于当前任务树的额外信息。

emptyCtx

emptyCtx 是一个 int 类型的变量,但实现了 context 的接口。emptyCtx 没有超时时间,不能取消,也不能存储任何额外信息,所以 emptyCtx 用来作为 context 树的根节点。

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
    return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
    return todo
}

BackgroundTODO 并没有什么不同,只不过用不同的名字来区分不同的场景罢了。

  • Background 通常被用于主函数、初始化以及测试中,作为一个顶层的 context
  • TODO 是在不确定使用什么 context 或者不知道有没有必要使用 context 的时候才会使用

context注意事项

  • 不要把 context 放在结构体中,要以参数的方式显示传递;
  • context 作为参数的函数方法,应该把 context 作为第一个参数;
  • 给一个函数方法传递 context 的时候,不要传递 nil,如果不知道传递什么,就使用 context.TODO()
  • context 是线程安全的,可以放心的在多个 Goroutine 中传递。

context使用指南

  • context.TODO: 不知道用什么 context 以及不知道需不需要用 context 的时候用
  • context.Background: 一般用于根 context
  • context.WithValue 传值
  • context.WithCancel 可取消
  • context.WithDeadline 到指定时间点自动取消(或在这之前手动取消)
  • context.WithTimeout 一段时间后自动取消(或在这之前手动取消)

Http

Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现。

HTTP客户端

基本的HTTP/HTTPS请求 Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。

resp, err := http.Get("https://www.volcengine.com")
...

resp, err := http.Post("https://www.volcengine.com/upload", "image/jpeg", &buf)
...

resp, err := http.PostForm("https://www.volcengine.com", url.Values{"key": {"Value"}, "id": {"123"}})
...

程序在使用完response后必须关闭回复的主体。

resp, err := http.Get("https://www.volcengine.com")
if err != nil {
    // handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...

GET请求示例

使用net/http包编写一个简单的发送HTTP请求的Client端,并用Go语言内置的net/url这个标准库来处理请求参数

func main() {
    apiUrl := "http://127.0.0.1:8000/get"
    // URL param
    data := url.Values{}
    data.Set("service", "vmp")
    data.Set("model", "vestack")
    u, err := url.ParseRequestURI(apiUrl)
    if err != nil {
        fmt.Printf("parse url requestUrl failed, err: %v\n", err)
    }
    
    u.RawQuery = data.Encode() // URL encode
    fmt.Println(u.String())
    resp, err := http.Get(u.String())
    if err != nil {
        fmt.Println("post failed, err: %v\n", err)
        return
    }
    defer resp.Body.Close()
    
    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("get resp failed, err: %v\n", err)
        return
    }
    fmt.Println(string(b))
}

对应的Server端HandlerFunc如下:

func getHandler(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
    data := r.URL.Query()
    fmt.Println(data.Get("service"))
    fmt.Println(data.Get("model"))
    answer := `{"status": "ok"}`
    w.Write([]byte(answer))
}

Post请求示例

上面演示了使用net/http包发送GET请求的示例,发送POST请求的示例代码如下:

func main() {
    url := "http://127.0.0.1:9000/post"
    contentType := "application/json"
    data := `{"service":"vmp", "model":"vestack"}`

    resp, err := http.Post(url, contentType, strings.NewReader(data))
    if err != nil {
        fmt.Println("post failed, err: %v\n", err)
        return
    }
    defer resp.Body.Close()

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("get resp failed, err: %v\n", err)
        return
    }
    fmt.Println(string(b))
}

对应的Server端HandlerFunc如下:

func postHandler(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
    b, err := ioutil.ReadAll(r.Body)
    if err != nil {
        fmt.Println("read request.Body failed, err: %v\n", err)
        return
    }
    fmt.Println(string(b))
    answer := `{"status": "ok"}`
    w.Write([]byte(answer))
}

自定义Client

要管理HTTP客户端的头域、重定向策略和其他设置,需要创建一个自定义Client:

client := &http.Client{
    CheckRedirect: redirectPolicyFunc,
}

resp, err := client.Get("https://www.volcengine.com")
// ...

req, err := http.NewRequest("GET", "https://www.volcengine.com", nil)
// ...

req.Header.Add("X-Forward-Env", `SIT-GL"`)
resp, err := client.Do(req)
// ...

自定义Transport

要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:

tr := &http.Transport{
    TLSClientConfig:    &tls.Config{RootCAs: pool},
    DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://www.volcengine.com")

Client和Transport类型都可以安全的被多个go程同时使用。出于效率考虑,应该一次建立、尽量重用。

自定义Server

使用Go语言中的net/http包来编写一个简单的接收HTTP请求的Server端示例,net/http包是对net包的进一步封装,专门用来处理HTTP协议的数据。

func sayHello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello Bytedance!")
}

func main() {
    http.HandleFunc("/", sayHello)
    err := http.ListenAndServe(":8000", nil)
    if err != nil {
        fmt.Printf("http server failed, err: %v\n", err)
        return
    }
}

要管理服务端的行为,可以创建一个自定义的Server:

s := &http.Server{
    Addr:           ":8080",
    Handler:        sayHello,
    ReadTimeout:    10 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())

依赖管理

为什么需要依赖管理?

  • 最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码。如果不同的项目依赖同一个第三方的库的不同版本,就会出现冲突。

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。

Go Module常用命令

go mod download    下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit        编辑go.mod文件
go mod graph       打印模块依赖图
go mod init        初始化当前文件夹, 创建go.mod文件
go mod tidy        增加缺少的module,删除无用的module
go mod vendor      将依赖复制到vendor下
go mod verify      校验依赖
go mod why         解释为什么需要依赖
module code.hello.org/hello/nubela

go 1.18

require (
    k8s.io/api v0.24.6
    k8s.io/apimachinery v0.24.6
    k8s.io/client-go v0.24.6
    github.com/avast/retry-go v3.0.0+incompatible
    k8s.io/klog/v2 v2.110.1
    k8s.io/kubernetes v1.20.4
)

require (
    k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
    sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
    sigs.k8s.io/yaml v1.2.0 // indirect
)

replace (
    k8s.io/apiserver => k8s.io/apiserver v0.24.6
    k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.24.6
    k8s.io/<span data-word-id="754" class="abbreviate-word">metrics</span> => k8s.io/metrics v0.24.6
    k8s.io/mount-utils => k8s.io/mount-utils v0.24.6
    k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.6
)

版本号要求

go mod 对版本号的定义是有一定要求的,它要求的格式为 v<major>.<minor>.<patch>,如果 major 版本号大于 1 时,其版本号还需要体现在 Module 名字中。go mod 会在你依赖的后面打一个 +incompatible 标志

伪版本

当依赖仓库没有及时更新tag或者tag规范不符合要求时,那么当我们用 go mod 去拉这个项目的时候,就会将 commitId 作为版本号,它的格式大概是 vx.y.z-yyyymmddhhmmss-abcdef格式

Indirect标志

我们用 go mod 的时候应该经常会看到 有的依赖后面会打了一个 // indirect 的标识位,这个标识位是表示 间接的依赖。

什么叫间接依赖呢?打个比方,项目 A 依赖了项目 B,项目 B 又依赖了项目 C,那么对项目 A 而言,项目 C 就是间接依赖;

这里要注意,并不是所有的间接依赖都会出现在 go.mod 文件中。间接依赖出现在 go.mod 文件的情况,可能符合下面的场景的一种或多种:

  • 直接依赖未启用 Go module
  • 直接依赖 go.mod 文件中缺失部分依赖

replace使用

replace 指替换,它指示编译工具替换 require 指定中出现的包

需要注意的是:replace 指定中需要替换的包及其版本号必须出现在 require 列表中才有效

replace使用场景

  • 替换无法下载的包
  • 替换不兼容的包
转载自:https://juejin.cn/post/7314159614064967714
评论
请登录