Go context的使用和源码分析

什么是context

context是一个可以携带超时,取消信号,或者其他和当前请求相关的数据,用于在api,方法,或goroutine之前进行传递

为什么有context

用go来写后台服务中,通常每个请求都会启动一个goroutine,该goroutine可能会启动多个goroutine相互配合完成本次请求工作,例如一次请求多个模块的数据,可以起多个goroutine同时发起db,rpc请求。

和本次请求有关的基本数据就需要在多个goroutine,不同方法之间共享,例如userId,logId

除此之外,一般服务器会对请求设置超时,避免长时间占用goroutine导致资源耗尽,服务雪崩。当超时时间到了,或被手动取消时,和该次请求相关的goroutine都需要快速结束退出,因为他们的工作成果不再被需要了,同时也能节省资源开销

总结来说,context 是用来解决 goroutine 之间退出通知元数据传递的功能

基本使用

保存,传递kv

// key使用自定义类型

type UserInfo string

const UserId UserInfo = "userId"



func main() {

    ctx := context.Background()

    ctx = context.WithValue(ctx, UserId, "123")

    

    val := ctx.Value(UserId)

    userId, ok := val.(string)

    if ok {

       fmt.Println(userId)

    }

}



// 输出结果

123

设置超时时间:

func main() {

   ctx := context.Background()

   // 创建一个4s后超时的context

   cancelCtx, cancel := context.WithTimeout(ctx, time.Second*4)

   defer cancel()



   go func() {

      for {

         // 模拟耗时业务

 time.Sleep(2 * time.Second)



         select {

         // 接收超时信号,并退出

         case <-cancelCtx.Done():

            fmt.Printf("business %v", cancelCtx.Err())

            return

         case <-time.After(1 * time.Second):

         }

      }

   }()



   // 接收超时信号

   select {

   case <-cancelCtx.Done():

      fmt.Printf("main %v", cancelCtx.Err())

   }

   // 避免go进程退出

   time.Sleep(10 * time.Second)

}



// 结果:

// 4秒后打印

main context deadline exceeded

// 5秒后打印

business context deadline exceeded
  1. 创建一个4s后超时的context

  2. 主goroutine和业务goroutine都监听该cancelCtx的Done信号

  3. 4秒后主goroutine收到超时信号

  4. 5秒后业务goroutine收到超时信号

    1. 经过业务2s + timer 1s + 业务2s后,再次检查Done信号发现已经关闭
    2. 如果这里业务goroutine不监听退出信号,而只是主goroutine超时退出,则可能造成业务goroutine泄露

源码分析

以下源码使用版本:1.16.10

Context

context包中的所有接口及结构体关系如下:

image.png
Context接口定义的方法如下:

type Context interface {

 Deadline() (deadline time.Time, ok bool)

 Done() <-chan struct{}

 Err() error

 Value(key interface{}) interface{}

}

该方法主要给用户使用:

  • Deadline () :获取给该context设置的超时时间

  • Done() :返回一个只读channel,表示该context的是否被取消

    • 若Done() == nil,说明其不可被取消
    • 若Done() 不为 nil,则需要监听该channel,一旦有返回,就表示被取消
  • Err() :当ctx被取消时,该方法返回被取消原因,超时或手动取消

  • Value() :主要用于valueCtx获取设置的kv

canceler接口定义如下:

type canceler interface {

   cancel(removeFromParent bool, err error)

   Done() <-chan struct{}

}

该接口表示一个context是可取消的,主要context包内部使用,cancelCtx和timerCtx实现了 canceler 接口

  • cancel():调用cancel时会发送取消信号,以及将自己从父节点移除
  • Done():和Context接口中的Done一致

cancelCtx

创建可取消ctx:

ctx := context.Background()

cancelCtx, cancel := context.WithCancel(ctx)

context.WithCancel返回了可取消的ctx,cancelCtx和一个取消方法cancel

调用cancel方法,会使得所有其他goroutine中监听cancelCtx.Done()的地方收到退出信号,执行退出操作

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {

   if parent == nil {

      panic("cannot create context from nil parent")

   }

   // 创建一个新的ctx

   c := newCancelCtx(parent)

   // 检查父ctx是否可被取消,若是则将自己挂载到父ctx

   propagateCancel(parent, &c)

   // 返回新ctx,及取消方法

   return &c, func() { c.cancel(true, Canceled) }

}





func newCancelCtx(parent Context) cancelCtx {

   // 将父ctx放到Context

   return cancelCtx{Context: parent}

}

cancelCtx结构如下:

type cancelCtx struct { 

   Context 

   // 保护下以下字段

   mu       sync.Mutex

   // 懒加载 , 用于保存关闭信号           

 done     chan struct{}      

   // 挂载的子Context,  

 children map[canceler]struct{} 

   // 表示被取消的原因:手动取消或超时取消

 err      error                 // set to non-nil by the first cancel call

}

这是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样它就可以被看成一个 Context,同时该字段也保存其父Context

propagateCancel主要作用是向上追溯可取消的Context,若有就将自己注册进Context的children,这样一来当上层调用cancel方法时,就可以往下传递,把挂载的子Content也取消

func propagateCancel(parent Context, child canceler) {

   done := parent.Done()

   // 父节点不可取消

   if done == nil {

      return 

 }



   select {

   //  父节点已经被取消

   case <-done:

 child.cancel(false, parent.Err())

      return

   default:

   }



    // 向上找到最近一个可取消的Context

   if p, ok := parentCancelCtx(parent); ok {

      p.mu.Lock()

       // 若已经被取消

      if p.err != nil {

 child.cancel(false, p.err)

      } else {

         if p.children == nil {

            p.children = make(map[canceler]struct{})

         }

         // 将自己挂载到最近一个可取消的父Context

         p.children[child] = struct{}{}

      }

      p.mu.Unlock()

   // 若找不到可取消的context,但parent实现了done方法,也进行监听   

   } else {

      atomic.AddInt32(&goroutines, +1)

      go func() {

         select {

         case <-parent.Done():

            child.cancel(false, parent.Err())

         case <-child.Done():

         }

      }()

   }

}

propagateCancel具体做了啥?

  1. 若父节点p``arent.Done()为空,直接返回

    1. 只有cancelCtx或自定义的Context才会返回不为空的Done,若parent.Done == nil,说明父节点不可被取消,例如emptyCtx,valueCtx
  2. 如果父节点可以被取消,且已经被取消,则取消当前节点,并返回

  3. 向上找到最近一个可取消的Context,例如以下这种情况,就会找到parent.parent对应的Context

    1. 若已经取消,则取消当前节点
    2. 否则将自己挂在到该节点的children

image.png

  1. 若找不到可取消的context,但parent实现了done方法,也进行监听

    1. 因为找不到可取消的Context,则无法将自己挂在上面,就只能自己另起一个goroutine监听父节点的Done,来完成取消操作
    2. 但该goroutine的退出条件是两个,还有一个是case <-child.Done(),如果子节点自己取消了,就退出,不再管父节点的退出信号。因为如果父节点迟迟不退出,这个goroutine就泄露了,这里保证在子节点退出时,就能终止该监听goroutine

再来看看向上找到最近一个可取消Context的方法:parentCancelCtx

func parentCancelCtx(parent Context) (*cancelCtx, bool) {

   done := parent.Done()

   if done == closedchan || done == nil {

      return nil, false

   }

   p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)

   if !ok {

      return nil, false

   }

   p.mu.Lock()

   ok = p.done == done

   p.mu.Unlock()

   if !ok {

      return nil, false

   }

   return p, true

}
  1. 首先第一个判断

    1. 如果done == closedchan直接返回,后续子节点监听到该done如果已经被关闭,就执行cancel
    2. Done == nil在这个场景不可能成立,因为在上一步parent.Done()中会懒加载done
func (c *cancelCtx) Done() <-chan struct{} {

   c.mu.Lock()

   // 懒加载

   if c.done == nil {

      c.done = make(chan struct{})

   }

   d := c.done

   c.mu.Unlock()

   return d

}
  1. 下一步会根据&cancelCtxKey在parent中找最近的一个cancelCtx

    1. &cancelCtxKey是context包中定义的私有变量,如果cancelCtx遇到该key就会返回自己,否则会往上找,看看有没有parent是cancelCtx,若果是就返回该cancelCtx
// context包中的私有变量

var cancelCtxKey int



func (c *cancelCtx) Value(key interface{}) interface{} {

   // cancelCtx遇到cancelCtxKey就返回自己

   if key == &cancelCtxKey {

      return c

   }

   // 否则不断往parent中找

   return c.Context.Value(key)

}
  1. 如果没找到可取消的Context,则返回空,外部就监听parent.Done,而不是挂载到parent上

  2. 如果找到了,判断该可取消的Context的done,是否和parent.Done相等,如果不等还是返回空,如果相等就返回该可取消的Context,这一步该怎么理解?

    1. 如果父节点是调用WithCancel,WithTimeout,WithDeadline生成的context,则parent.Done,一定等于该可取消的Context.Done
    2. 如果父节点是自定义的Context,自己实现了Done方法,并且包装了context包里的cancelCtx,这里就不相等

举个例子,假设自定义了MyContext:

type MyContext struct {

   context.Context

}



// 自定义done方法

func (myc *MyContext) Done() <-chan struct{} {

   return make(chan struct{})

}

用MyContext包装cancelCtx:

ctx := context.Background()



cancelCtx, _ := context.WithCancel(ctx)

// 包装context包的cancelCtx

myContext := &MyContext{

   Context: cancelCtx,

}

此时调context.WithCancel(myContext)时,就会出现两个done不一样的情况

也就是说,parent自己实现了Done接口,其返回的done,和根据parent往上寻找的第一个cancelCtx的done不一样。此时有两个done,当前context监听哪一个呢?这里选择监听自己实现的done,因为这种情况下不应该绕过用户自定义的Done。且按照层级关系来说,也应该监听最解决自已的关闭信号

这样一来,当前Context就和父或祖宗Context产生关联,要么将自己挂到最接近的一个父cancelCtx上,如果没有,且父节点自定义实现了一套产生Done信号的方法,就需要新开goroutine监听该信号

再看看返回的CancelFunc具体执行的操作

func (c *cancelCtx) cancel(removeFromParent bool, err error) {

   if err == nil {

      panic("context: internal error: missing cancel error")

   }

   c.mu.Lock()

   if c.err != nil {

      // 已经被关闭

      c.mu.Unlock()

      return 

 }

   c.err = err

   if c.done == nil {

      c.done = closedchan

   } else {

       // 发出关闭信号

      close(c.done)

   }

   

   // 关闭子Context

   for child := range c.children {

 child.cancel(false, err)

   }

   c.children = nil

   c.mu.Unlock()

    // 若需要,从父节点删除自己

   if removeFromParent {

      removeChild(c.Context, c)

   }

}

总体来看执行了以下操作

  1. 如果该Context已经被关闭,即err != nil,则直接退出。这也保证了cancel方法的幂等性
  2. 记录err,该值为errors.New("context canceled"),用于其他地方监听该ctx.Done返回时知道关闭原因
  3. 关闭chan,让其他监听了该chan的context知道该 context 已经被取消了
  4. 取消由该Content生成的可取消的子Contet
  5. 若参数中removeFromParent 为true,将自己从父Context中取消挂载

现在问题来了:

  1. 什么时候传true?
  2. 为什么有时传 true,有时传 false?

什么时候会传true?答案是调用WithCancel() 时,其返回的cancelFunc中对cancel的调用会传true

return &c, func() { c.cancel(true, Canceled) }

当调用该cancelFunc,会将自己从父Context中删除,这是因为自己已经被取消了,就没有必要再在父Context的关系里面接收父或祖先节点的取消通知

在cancel方法内部取消子Content时,该参数为false,也就是不需要从父Context中删除。这是因为在取消完子Context后,会执行c.children = nil,将所有子Context和自己断绝关系。这样子Content就不需要把自己从父中移除

cancelCtx的这套设计,让我们可以选择取消一颗子树上的context:

image.png

假设我取消红色的cancelCtx,只会取消这棵子树上的context,对整棵树上的其他context没有影响

timerCtx

timerCtx基于cancelCtx,只是多了个timer,deadline。当deadline到期时,timer会执行cancel方法

type timerCtx struct {

   cancelCtx

   timer *time.Timer .

 deadline time.Time

}

如何创建一个可超时自动取消的context?

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {

   return WithDeadline(parent, time.Now().Add(timeout))

}

使用WithTimeout函数底层调用了WithDeadline,将一个timeout相对实现都转化为基于当前时间的绝对时间统一处理

WithDeadline方法:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {

   if parent == nil {

      panic("cannot create context from nil parent")

   }

   // 如果父也是timerCtx,且父的过期时间比当前时间早,就不用新起timer监听

   if cur, ok := parent.Deadline(); ok && cur.Before(d) {

 return WithCancel(parent)

   }

   c := &timerCtx{

      cancelCtx: newCancelCtx(parent),

      deadline:  d,

   }

   // 找到父或祖先的可取消Context进行挂载

   propagateCancel(parent, c)

   dur := time.Until(d)

   // 已经过期

   if dur <= 0 {

      c.cancel(true, DeadlineExceeded) // deadline has already passed

 return c, func() { c.cancel(false, Canceled) }

   }

   c.mu.Lock()

   defer c.mu.Unlock()

   if c.err == nil {

       // 到期后执行取消操作

      c.timer = time.AfterFunc(dur, func() {

         c.cancel(true, DeadlineExceeded)

      })

   }

   return c, func() { c.cancel(true, Canceled) }

}
  1. 如果父节点也是timerCtx,且父节点比当前节点早过期,就没必要新起一个timer,因为父节点过期时会通知当前节点一起取消
  2. 创建timerCtx,设置父和到期时间
  3. 和新建cancelCtx一样,找到父或祖先的可取消Context进行挂载,如果找不到,且父节点自定义实现了Done方法,就监听该Done
  4. 如果传进来的deadline已经到期了,或者执行1,2,,3步骤时到期了,就取消当前节点
  5. 新起timer进行定时取消操作,此时设置的错误原因就是超时,而不是被取消
var DeadlineExceeded error = deadlineExceededError{}



type deadlineExceededError struct{}



func (deadlineExceededError) Error() string   { return "context deadline exceeded" }

再看看其返回的cancel方法,除了timerCtx到期取消外,也可通过该方法手动取消:

func (c *timerCtx) cancel(removeFromParent bool, err error) {

    // 调用cancel方法

   c.cancelCtx.cancel(false, err)

   // 若需要,从父节点移除自己

   if removeFromParent {

 removeChild(c.cancelCtx.Context, c)

   }

   c.mu.Lock()

   if c.timer != nil {

       // 取消timer

      c.timer.Stop()

      c.timer = nil

   }

   c.mu.Unlock()

}

除了本身cancelCtx的cancel方法外,还将timer停止并清空

  • 自己被提前手动取消,就没有必要继续用timer到期取消了
  • 这里在停止timer后,还将c.time = nil,保证多次调用cancel的幂等性

valueCtx

type valueCtx struct {

   Context

   key, val interface{}

}

valueCtx有一个k,v对

往valueCtx里塞kv对,以及根据key找value的方法如下:



func WithValue(parent Context, key, val interface{}) Context {

   if parent == nil {

      panic("cannot create context from nil parent")

   }

   if key == nil {

      panic("nil key")

   }

   if !reflectlite.TypeOf(key).Comparable() {

      panic("key is not comparable")

   }

   // 创建一个valueCtx,保存key,val,将父节点作为Context

   return &valueCtx{parent, key, val}

}





func (c *valueCtx) Value(key interface{}) interface{} {

   // 如果当前节点的key就是key,返回当前节点的value

   if c.key == key {

      return c.val

   }

   // 否则向上递归找

   return c.Context.Value(key)

}

这里要求key是可比较的,也就是可以和另一个key比较是否相当,不然没法判断当前节点的key是否是参数中的key

最终会形成一棵树

image.png

可以看到,如果要从C4找key1,需要一直遍历到解决根节点,相比与用map保存kv的方式,时间复杂度较高,那为啥valueCtx这么设计呢?

解决并发修改问题:对于任何拿到valueCtx的人相当于都是只读的,你可以修改,只会往后追加,得到一个更长的链表的指针,而不可能去修改别人已经拿到的context,这显然更安全

但这样也会有一些问题,例如若果将C4.key改为key1,则对于C4来说,C1就没用了,但还占着空间

一般来说WithValue存放的信息为和请求想干的信息,例如userId,logId。key建议用自定义类型,这样即时两个key value一样,但类型不一样,也不会产生冲突

总结

到这里Context中的源码就讲解完了,总的来说其设计比较优雅,解决了goroutine,方法直接传递元数据,及超时控制需求

参考文档

https://qcrao.com/2019/06/12/dive-into-go-context/

亚洲第一中锋_哈达迪
关注 关注
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang context包的源码分析
飞越蓝天的专栏
09-15 197
context.go文件里面,主要有4个struct体,都继承Context接口;四个struct体 分别是emptyCtx、cancelCtx、timerCtx、valueCtx。提供了获取对应结构体的方法:WithCancel,WithDeadline,WithTimeout,WithValue;结构如图:我们就按照这个顺序,分别介绍对应的Context
Golang context源码分析
skh2015java的博客
08-08 412
context源码分析 Golang JDK 1.10.3 Context介绍 假如有多个goroutine来处理一个操作,这个操作有超时时间,当超时时间到了之后,如何通知这个操作的所有goroutine都退出。或者在操作的过程中有一个goroutine遇到异常需要退出,如何通知其他的goroutine也退出该操作? context就可以很好的解决以上问题。 Go服务器的每个请求都有自己的goroutine,而有的请求为了提高性能,会经常启动额外的goroutine处理请...
老卫带你学---go语言中context库里propagateCancel函数
老卫带你学
11-14 147
上面判断parentCancelCtx这个,因为并非所有人实现的context都有children ,当是golang内部实现的cancelCtx时候,可以添加child来让parent取消。而如果是自己实现的parent-context,则一定是让child监听parent-done来观察parent是否结束。如何判断parent是否是cancelCtx
go interface类型转换_Go:从 context 源码领悟接口的设计
weixin_39807067的博客
11-30 117
本文基于 Go1.12.7go语言中实现一个interface不用像其他语言一样需要显示的声明实现接口。go语言只要实现了某interface的方法就可以做类型转换。go语言没有继承的概念,只有Embedding的概念。想深入学习这些用法,阅读源码是最好的方式.Context源码非常推荐阅读,从中可以领悟出go语言接口设计的精髓。对外暴露Context接口Context源码中只对外显露出一个Co...
context canceled 到底谁在作祟?
最新发布
发光如星
09-08 1529
报警治理中context cancel报警凸显,通过探究go中context原理,结合业务实际使用场景,总结诱发场景和治理建议...
Golang context包入门
yueguanyun的专栏
03-22 9226
Golang context包入门 转自:http://studygolang.com/articles/9624 概述 Golang 的 context Package 提供了一种简洁又强大方式来管理 goroutine 的生命周期,同时提供了一种 Requst-Scope K-V Store。但是对于新手来说,Context 的概念不算非常的直观,这篇文章来带领大家了解一下
Golang--详解Context
JaweG
04-27 774
1-Context 应用场景 ①上层任务取消后,所有的下层任务都会被取消;②中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。 业务需要对访问的数据库,RPC ,或API接口,为了防止这些依赖导致我们的服务超时,需要针对性的做超时控制 为了详细了解服务性能,记录详细的调用链Log 2-Context 原理 Context 的调用应该是链式的,从Context 派生出新的子类:WithCancel、WithDeadline/WithTimeout、WithValu
深度解密Go语言之context
weixin_44014995的博客
10-24 918
什么是context context中文译作“上下文”,准确的说它是goroutine的上下文,包含goroutine的运行状态,环境,现场等信息等 context主要用来在goroutine之间传递上下文信息,包括:取消信号,超时时间,截止时间,K-V等 随着context包的引入,标准库中很多接口因此都加上了context参数,列如database/sql包。context几乎成为了并发控制和超时控制的标准做法。 context.Context 类型的值可以协调多个goroutine中的代码执行“
golang 服务中 context 超时处理的思考
泡泡茶壶的博客
05-21 4194
公司运行的服务代码中,随处可见各种各样的日志信息,其中大多数是用来记录各种异常的日志,一方面,当出现问题时,通过日志我们可以快速的定位引发问题的原因;另外我们可以通过日志平台,对一些错误级别比较高的日志进行监控,从而能够快速响应系统可能会出现的问题。在Go语言中,Context是一个非常重要的概念,它存在于一个完整的业务生命周期内,ContextDeadline()Done()Err()和Value()。其中,Deadline()方法返回context的截止日期,Done()方法返回一个只读的。
go context源码分析
05-18
Context包是Go语言内置的一个标准库,主要用于多个Goroutine之间的上下文传递和控制。它提供了一种机制来传递取消信号、截止时间和一些其他的请求/值,这些请求/值可以跨越多个API边界和Goroutine传递,而不需要显式...
深入解析Golang之Context
数据小冰的博客
05-22 4706
context是什么 context翻译成中文就是上下文,在软件开发环境中,是指接口之间或函数调用之间,除了传递业务参数之外的额外信息,像在微服务环境中,传递追踪信息traceID, 请求接收和返回时间,以及登录操作用户的身份等等。本文说的context是指golang标准库中的context包。Go标准库中的context包,提供了goroutine之间的传递信息的机制,信号同步,除此之外还有超时(timeout)和取消(cancel)机制。概括起来,Context可以控制子goroutine的运行,
Golang Gin 实战(十二)| ProtoBuf 使用源码分析原理实现
flysnow_org的博客
06-20 1671
Golang Gin 实战(十二)| ProtoBufProtoBuf最近几年也算比较流行,它是一种语言无关,平台无关,并且可以扩展,并结构数据序列化的方法。相比JSON/XML这类文本...
golang-context详解
m0_57960197的博客
08-27 901
context是golang中的经典工具,主要在异步场景中用于实现并发协调以及对goroutine的生命周期控制,除此之外,context还兼具一定的数据存储能力。
golang之context实用记录
数据库技术
09-07 675
一段时间后,调用父Context的cancel函数,会发现父Context的协程和子Context的协程都收到了信号,被结束了。新创建协程中传入子Context做参数,且需监控子Context的Done通道,若收到消息,则退出。注意:当 父Context的 Done() 关闭的时候,子 ctx 的 Done() 也会被关闭。利用上面的父Context再创建一个子Context使用该子Context创建一个协程。利用根Context创建一个父Context使用Context创建一个协程,
context canceled
Ftworld21的专栏
08-07 266
go func(ctx),传入ctx,会导致func函数里面调用出现context canceled。
Go:Context控制子协程之手动cancel和自动timeout
小楼一夜听春雨
11-02 1082
context 一般来说,goroutine是平级关系,但是通过引进context可以让其有逻辑上的父子关系 也就是,父要子停,子不得不停的意思 揣摩一下哈 手动cancel package main import ( "context" "fmt" "time" ) func HandelRequest(ctx context.Context) { go WriteLog(ctx) go WriteDB(ctx) for { select { case <-ctx.Done(
go context详解
牛奔的博客
08-11 5319
前言 平时在 Go 工程的开发中,几乎所有服务端的默认实现(例如:HTTP Server),都在处理请求时开启了新的 goroutine 进行处理。 但从一开始就存在一个问题,那就是当一个请求被取消或超时时,所有在该请求上工作的 goroutine 应该迅速退出,以便系统可以回收他们正在使用的资源。 因此 Go 官方在2014年,Go 1.7 版本中正式引入了 context 标准库。其主要作用...
Golang中context实现原理剖析
香烟,瓜子,矿泉水
09-18 2321
转载: Go 并发控制context实现原理剖析 1. 前言 Golang context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。 context翻译成中文是"上下文",即它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。 典型的使用场景如下图所示: 上图中由于goroutine派生出子goroutine,而子goroutine又继续派生新的
Go context.WithCancel(ctx) 创建带有取消能力的上下文
wgchen
06-02 285
Go context.WithCancel(ctx) 创建带有取消能力的上下文
写文章

热门文章

  • 如何优雅实现不存在插入否则更新,和mongodb upsert 2671
  • 详解go语言中for range的坑 1744
  • golang实现四舍五入 1660
  • 详解Go slice底层原理 1627
  • Golang 如何下载ftp文件 1279

分类专栏

  • 算法与数据结构 9篇
  • 源码分析 4篇
  • kratos 1篇
  • go 12篇
  • go-zero源码系列 2篇
  • 动态规划 2篇
  • 算法刷题笔记 34篇

最新评论

  • 深入理解 Go sync.Map

    CSDN-Ada助手: 恭喜你这篇博客进入【CSDN每天值得看】榜单,全部的排名请看 https://bbs.csdn.net/topics/615418965。

  • 详解go语言中for range的坑

    阿白,: 循环体内拿个变量值传递拷贝即可

最新文章

  • Golang 怎么高效处理ACM模式输入输出
  • 开源限流组件分析(三):golang-time/rate
  • 开源限流组件分析(二):uber-go/ratelimit
2024年7篇
2023年23篇
2022年44篇
2021年1篇

目录

目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

玻璃钢生产厂家玻璃钢巨型动物雕塑海南玻璃钢雕塑厂家直销吉安个性化玻璃钢雕塑供应商五华区玻璃钢广场雕塑设计浙江玻璃钢花盆市场报价淮滨玻璃钢雕塑厂家韶关雕塑玻璃钢玻璃钢动物牛雕塑公司地址商场美陈装饰墙面方型玻璃钢花盆河北通道商场美陈销售厂家景德仿铜西式玻璃钢雕塑商场环境美陈鲤城商场美陈制作梅州玻璃钢公园雕塑郑州价值观玻璃钢彩绘雕塑定制玻璃钢雕塑和紫铜雕塑的差别江苏中庭商场美陈价钱海淀玻璃钢新娘雕塑招做玻璃钢雕塑信息陕西仿古玻璃钢仿铜雕塑福建学校玻璃钢雕塑厂家大庆小品系列玻璃钢雕塑安装玻璃钢胸像雕塑绍兴玻璃钢花盆哪里有青海仿真人物玻璃钢雕塑订做河北特色商场美陈费用郑州玻璃钢雕塑258商场踏青美陈敦化玻璃钢雕塑厂香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化