Golang 语言进阶指南
背景
当前领域各成员的开发水平参差不齐,大部分同学没有深度使用过 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值指向了最后一个元素,所以上面代码极大可能都是输出了最后一个元素。
- 通过参数传递数据到协程
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的栈中
- 定义临时变量
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
}
Background
和 TODO
并没有什么不同,只不过用不同的名字来区分不同的场景罢了。
Background
通常被用于主函数、初始化以及测试中,作为一个顶层的context
TODO
是在不确定使用什么context
或者不知道有没有必要使用context
的时候才会使用
context注意事项
- 不要把
context
放在结构体中,要以参数的方式显示传递; - 以
context
作为参数的函数方法,应该把context
作为第一个参数; - 给一个函数方法传递
context
的时候,不要传递nil
,如果不知道传递什么,就使用context.TODO()
; context
是线程安全的,可以放心的在多个 Goroutine 中传递。
context使用指南
context.TODO
: 不知道用什么 context 以及不知道需不需要用 context 的时候用context.Background
: 一般用于根 contextcontext.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