Golang 语言进阶指南

背景

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

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

函数&方法

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

arduino 复制代码
// 函数
func hello(c *Client) {

}
scss 复制代码
// 方法
func (c *Client) hello() {

}

可变参数

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

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

}

参数传递

值传递:

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

go 复制代码
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
}

引用传递:

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

go 复制代码
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 这三种类型的实现机制类似指针, 所以可以直接传递,而不用取地址后传递指针。

返回值

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

go 复制代码
func hello(x, y string)(res string, err error){
    return
}

延迟函数

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

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

go 复制代码
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 语音支持函数式编程:

  • 将匿名函数作为另一个函数的参数:回调函数
  • 将匿名函数作为另一个函数的返回值:闭包
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)
}
go 复制代码
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 叫做高阶函数。

go 复制代码
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)
}
go 复制代码
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]
}

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

闭包

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

闭包 = 函数 + 引用环境

go 复制代码
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
}

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

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

go 复制代码
// 函数计数器
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())
}

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

go 复制代码
// 定义一个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。

scss 复制代码
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

css 复制代码
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

css 复制代码
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. 通过参数传递数据到协程
go 复制代码
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. 定义临时变量
css 复制代码
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将上面的代码优化一下:

go 复制代码
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同时操作一个资源(临界区),这种情况就会发生数据竞态问题。类比卫生间被整层楼同性别同学竞争使用的场景。

go 复制代码
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类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

csharp 复制代码
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无论获取读锁还是写锁均会等待。

互斥锁

scss 复制代码
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))
}

读写锁

scss 复制代码
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函数。

go 复制代码
// 初始化一个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

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

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

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

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

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

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

go 复制代码
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函数初始化通道的时候为其指定通道的容量

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

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

标准库

Context

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

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

Context接口

scss 复制代码
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 树的根节点。

scss 复制代码
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请求。

go 复制代码
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后必须关闭回复的主体。

go 复制代码
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这个标准库来处理请求参数

go 复制代码
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如下:

scss 复制代码
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请求的示例代码如下:

go 复制代码
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如下:

go 复制代码
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:

go 复制代码
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:

css 复制代码
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协议的数据。

go 复制代码
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:

yaml 复制代码
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常用命令

lua 复制代码
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         解释为什么需要依赖
bash 复制代码
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使用场景

  • 替换无法下载的包
  • 替换不兼容的包
相关推荐
蒙娜丽宁2 天前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
qq_172805592 天前
GO Govaluate
开发语言·后端·golang·go
littleschemer3 天前
Go缓存系统
缓存·go·cache·bigcache
程序者王大川4 天前
【GO开发】MacOS上搭建GO的基础环境-Hello World
开发语言·后端·macos·golang·go
Grassto4 天前
Gitlab 中几种不同的认证机制(Access Tokens,SSH Keys,Deploy Tokens,Deploy Keys)
go·ssh·gitlab·ci
高兴的才哥5 天前
kubevpn 教程
kubernetes·go·开发工具·telepresence·bridge to k8s
少林码僧5 天前
sqlx1.3.4版本的问题
go
蒙娜丽宁5 天前
Go语言结构体和元组全面解析
开发语言·后端·golang·go
蒙娜丽宁6 天前
深入解析Go语言的类型方法、接口与反射
java·开发语言·golang·go
三里清风_6 天前
Docker概述
运维·docker·容器·go