亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

從Context到go設(shè)計(jì)理念輕松上手教程

 更新時(shí)間:2022年09月27日 09:54:54   作者:陳雪鋒  
這篇文章主要為大家介紹了從Context到go設(shè)計(jì)理念輕松上手教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

context包比較小,是閱讀源碼比較理想的一個(gè)入手,并且里面也涵蓋了許多go設(shè)計(jì)理念可以學(xué)習(xí)。
go的Context作為go并發(fā)方式的一種,無(wú)論是在源碼net/http中,開(kāi)源框架例如gin中,還是內(nèi)部框架trpc-go中都是一個(gè)比較重要的存在,而整個(gè) context 的實(shí)現(xiàn)也就不到600行,所以也想借著這次機(jī)會(huì)來(lái)學(xué)習(xí)學(xué)習(xí),本文基于go 1.18.4。

話(huà)不多說(shuō),例:

為了使可能對(duì)context不太熟悉的同學(xué)有個(gè)熟悉,先來(lái)個(gè)example ,摘自源碼:

我們利用WithCancel創(chuàng)建一個(gè)可取消的Context,并且遍歷頻道輸出,當(dāng) n==5時(shí),主動(dòng)調(diào)用cancel來(lái)取消。
而在gen func中有個(gè)協(xié)程來(lái)監(jiān)聽(tīng)ctx當(dāng)監(jiān)聽(tīng)到ctx.Done()即被取消后就退出協(xié)程。

func main(){
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
                                        close(dst)
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
 
ctx, cancel := context.WithCancel(context.Background())
// defer cancel() // 實(shí)際使用中應(yīng)該在這里調(diào)用 cancel
 
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
                       cancel() // 這里為了使不熟悉 go 的更能明白在這里調(diào)用了 cancel()
break
}
}
// Output:
// 1
// 2
// 3
// 4
// 5
}

這是最基本的使用方法。

概覽

對(duì)于context包先上一張圖,便于大家有個(gè)初步了解(內(nèi)部函數(shù)并未全列舉,后續(xù)會(huì)逐一講解):

最重要的就是右邊的接口部分,可以看到有幾個(gè)比較重要的接口,下面逐一來(lái)說(shuō)下:

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

首先就是Context接口,這是整個(gè)context包的核心接口,就包含了四個(gè) method,分別是:

Deadline() (deadline time.Time, ok bool) // 獲取 deadline 時(shí)間,如果沒(méi)有的話(huà) ok 會(huì)返回 false
Done() <-chan struct{} // 返回的是一個(gè) channel ,用來(lái)應(yīng)用監(jiān)聽(tīng)任務(wù)是否已經(jīng)完成
Err() error // 返回取消原因 例如:Canceled\DeadlineExceeded
Value(key any) any // 根據(jù)指定的 key 獲取是否存在其 value 有則返回

可以看到這個(gè)接口非常清晰簡(jiǎn)單明了,并且沒(méi)有過(guò)多的Method,這也是go 設(shè)計(jì)理念,接口盡量簡(jiǎn)單、小巧,通過(guò)組合來(lái)實(shí)現(xiàn)豐富的功能,后面會(huì)看到如何組合的。

再來(lái)看另一個(gè)接口canceler,這是一個(gè)取消接口,其中一個(gè)非導(dǎo)出 method cancel,接收一個(gè)bool和一個(gè)error,bool用來(lái)決定是否將其從父Context中移除,err用來(lái)標(biāo)明被取消的原因。還有個(gè)Done()和Context接口一樣,這個(gè)接口為何這么設(shè)計(jì),后面再揭曉。

type canceler interface{  
cancel(removeFromParent bool, err error)  
Done() <-chan struct{}
}

接下來(lái)看這兩個(gè)接口的實(shí)現(xiàn)者都有誰(shuí),首先Context直接實(shí)現(xiàn)者有 valueCtx(比較簡(jiǎn)單放最后講)和emptyCtx

而canceler直接實(shí)現(xiàn)者有cancelCtx和timerCtx ,并且這兩個(gè)同時(shí)也實(shí)現(xiàn)了Context接口(記住我前面說(shuō)得另外兩個(gè)是直接實(shí)現(xiàn),這倆是嵌套接口實(shí)現(xiàn)松耦合,后面再說(shuō)具體好處),下面逐一講解每個(gè)實(shí)現(xiàn)。

空的

見(jiàn)名知義,這是一個(gè)空實(shí)現(xiàn),事實(shí)也的確如此,可以看到啥啥都沒(méi)有,就是個(gè)空實(shí)現(xiàn),為何要寫(xiě)呢?

type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {  return}
func (*emptyCtx) Done() <-chan struct{} {  return nil}
func (*emptyCtx) Err() error {  return nil}
func (*emptyCtx) Value(key any) any {  return nil}
func (e *emptyCtx) String() string {  switch e {  case background:    return "context.Background"  case todo:    
return "context.TODO"  }  return "unknown empty Context"}

再往下讀源碼會(huì)發(fā)現(xiàn)兩個(gè)有意思的變量,底層一模一樣,一個(gè)取名叫 background,一個(gè)取名叫todo,為何呢?耐心的可以看看解釋?zhuān)鋵?shí)是為了方便大家區(qū)分使用,背景 是在入口處來(lái)傳遞最初始的context,而todo 則是當(dāng)你不知道用啥,或者你的函數(shù)雖然接收ctontext參數(shù),但是并沒(méi)有做任何實(shí)現(xiàn)時(shí),那么就使用todo即可。后續(xù)如果有具體實(shí)現(xiàn)再傳入具體的上下文。所以上面才定義了一個(gè)空實(shí)現(xiàn),就為了給這倆使用呢,這倆也是我們最常在入口處使用的。

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
}

下面再看看具體的定義吧。

cancelCtx與timerCtx、valueCtx

type cancelCtx struct{
  Context
  mu       sync.Mutex  // 鎖住下面字段的操作
  // 存放的是 chan struct{}, 懶創(chuàng)建,
  //  只有第一次被 cancel 時(shí)才會(huì)關(guān)閉
  done     atomic.Value  
  // children 存放的是子 Context canceler ,并且當(dāng)?shù)谝淮伪?cancel 時(shí)會(huì)被
  // 設(shè)為 nil
  children map[canceler]struct{}
  //  第一次被調(diào)用 cancel 時(shí),會(huì)被設(shè)置
  err      error                 
}
 
type timerCtx struct{
 cancelCtx
 timer *time.Timer // 定時(shí)器,用來(lái)監(jiān)聽(tīng)是否超時(shí)該取消
 deadline time.Time // 終止時(shí)間
}
 
type valueCtx struct {
 Context
 key, val any
}

這里就看出來(lái)為何cancelCtx為非導(dǎo)出了,因?yàn)樗ㄟ^(guò)內(nèi)嵌Context接口也也是實(shí)現(xiàn)了Context的。并且通過(guò)這種方式實(shí)現(xiàn)了松耦合,可以通過(guò) WithCancel(父Context) (ctx Context,cancel CancelFunc) 來(lái)傳遞任何自定義的Context實(shí)現(xiàn)。

而timerCtx是嵌套的cancelCtx,同樣他也可以同時(shí)調(diào)用Context接口所有 method與cancelCtx所有method ,并且還可以重寫(xiě)部分方法。而 valueCtx和上面兩個(gè)比較獨(dú)立,所以直接嵌套的Context。

這里應(yīng)該也看明白了為何canceler為何一個(gè)可導(dǎo)出Done一個(gè)不可導(dǎo)出 cancel,Done是重寫(xiě)Context的method會(huì)由上層調(diào)用,所以要可導(dǎo)出, cancel則是由return func(){c.cancel(false,DeadlineExeceed) 類(lèi)似的封裝導(dǎo)出,所以不應(yīng)該導(dǎo)出。

這是go中推崇的通過(guò)組合而非繼承來(lái)編寫(xiě)代碼。其中字段解釋我已在后面注明,后面也會(huì)講到。看懂了大的一個(gè)設(shè)計(jì)理念,下面我們就逐一擊破,通過(guò)上面可以看到timerCtx其實(shí)是復(fù)用了cancelCtx能力,所以cancelCtx最為重要,下面我們就先將cancelCtx實(shí)現(xiàn)。

取消

它非導(dǎo)出,是通過(guò)一個(gè)方法來(lái)直接返回Context類(lèi)型的,這也是go理念之一,不暴露實(shí)現(xiàn)者,只暴露接口(前提是實(shí)現(xiàn)者中的可導(dǎo)出method不包含接口之外的method, 否則導(dǎo)出的method外面也無(wú)法調(diào)用)。

先看看外部構(gòu)造函數(shù)WithCancel,

  • 先判斷parent是否為nil,如果為nil就panic,這是為了避免到處判斷是否為nil。所以永遠(yuǎn)不要使用nil來(lái)作為一個(gè)Context傳遞。
  • 接著將父Context封裝到cancelCtx并返回,這沒(méi)啥說(shuō)得,雖然只有一行代碼,但是多處使用,所以做了封裝,并且后續(xù)如果要更改行為調(diào)用者也無(wú)需更改。很方便。
  • 調(diào)用propagateCancel,這個(gè)函數(shù)作用就是當(dāng)parent是可以被取消的時(shí)候就會(huì)對(duì)子Context也進(jìn)行取消的取消或者準(zhǔn)備取消動(dòng)作。
  • 返回Context與CancelFunc type >CancelFunc func()就是一個(gè) type func別名,底層封裝的是c.cancel方法,為何這么做呢?這是為了給上層應(yīng)用一個(gè)統(tǒng)一的調(diào)用,cancelCtx與timerCtx以及其他可以實(shí)現(xiàn)不同的cancel但是對(duì)上層是透明并且一致的行為就可。這個(gè)func應(yīng)該是協(xié)程安全并且多次調(diào)用只有第一次調(diào)用才有效果。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc){
     if parent == nil {
    panic("cannot create context from nil parent")
  }
  c := newCancelCtx(parent)
  propagateCancel(parent, &c)
  return&c, func() { c.cancel(true, Canceled) }
}
 
func newCancelCtx(parent Context) cancelCtx {
 return cancelCtx{Context: parent}
}

接下來(lái)就來(lái)到比較重要的func  propagateCancel,我們看看它做了啥,

首先是判斷父context的Done()方法返回的channel是否為nil,如果是則直接返回啥也不做了。這是因?yàn)楦窩ontext從來(lái)不會(huì)被取消的話(huà),那就沒(méi)必要進(jìn)行下面動(dòng)作。這也表名我們使用.與貓(上下文。Background()) 這個(gè)函數(shù)是不會(huì)做任何動(dòng)作的。

 done := parent.Done()
  if done == nil {
    return // parent is never canceled
  }

接下里就是一個(gè)select ,如果父Context已經(jīng)被取消了的話(huà),那就直接取消子Context就好了,這個(gè)也理所應(yīng)當(dāng),父親都被取消了,兒子當(dāng)然也應(yīng)該取消,沒(méi)有存在必要了。

select {
  case <-done:
    // parent is already canceled
    child.cancel(false, parent.Err())
    return
  default:
  }

如果父 Context 沒(méi)有被取消,這里就會(huì)做個(gè)判斷,

  • 看看parent是否是一個(gè)*cancelCtx,如果是的話(huà)就返回其p,再次檢查 p.err是否為nil,如果不為nil就說(shuō)明parent被取消,接著取消 子 Context,如果沒(méi)被取消的話(huà),就將其加入到p.children中,看到這里的 map是個(gè)canceler,可以接收任何實(shí)現(xiàn)取消器 的類(lèi)型。這里為何要加鎖呢?因?yàn)橐獙?duì)p.err以及p.children進(jìn)行讀取與寫(xiě)入操作,要確保協(xié)程安全所以才加的鎖。
  • 如果不是*cancelCtx類(lèi)型就說(shuō)明parent是個(gè)被封裝的其他實(shí)現(xiàn) Context 接口的類(lèi)型,則會(huì)將goroutines是個(gè)int加1這是為了測(cè)試使用的,可以不管它。并且會(huì)啟動(dòng)個(gè)協(xié)程,監(jiān)聽(tīng)父Context ,如果父Context被取消,則取消子Context,如果監(jiān)聽(tīng)到子Context已經(jīng)結(jié)束(可能是上層主動(dòng)調(diào)用CancelFunc)則就啥也不用做了。
if p, ok := parentCancelCtx(parent); ok {
    p.mu.Lock()
    if p.err != nil {
      // parent has already been canceled
      child.cancel(false, p.err)
    } else {
      if p.children == nil {
        p.children = make(map[canceler]struct{})
      }
      p.children[child] = struct{}{}
    }
    p.mu.Unlock()
  } else {
    atomic.AddInt32(&goroutines, +1)
    go func() {
      select {
      case <-parent.Done():
        child.cancel(false, parent.Err())
      case <-child.Done():
      }
    }()
  }

接下來(lái)看看parentCancelCtx的實(shí)現(xiàn):它是為了找尋parent底下的 *cancelCtx,

它首先檢查parent.Done()如果是一個(gè)closedchan這個(gè)頻道 在初始化時(shí)已經(jīng)是個(gè)一個(gè)被關(guān)閉的通道或者未nil的話(huà)(emptyCtx)那就直接返回 nil,false。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
  done := parent.Done()
  if done == closedchan || done == nil {
    return nil, false
}
var closedchan = make(chan struct{})
func init() {
  close(closedchan)
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
  return nil, false
}

接著判斷是否parent是*cancelCtx類(lèi)型,如果不是則返回nil,false,這里調(diào)用了parent.Value方法,并最終可能會(huì)落到value方法:

func value(c Context, key any) any {
  for {
    switch ctx := c.(type) {
    case *valueCtx:
      if key == ctx.key {
        return ctx.val
      }
      c = ctx.Context
    case *cancelCtx:
      if key == &cancelCtxKey {
        return c
      }
      c = ctx.Context
    case *timerCtx:
      if key == &cancelCtxKey {
        return &ctx.cancelCtx
      }
      c = ctx.Context
    case *emptyCtx:
      return nil
    default:
      return c.Value(key)
    }
  }
}
  • 如果是*valueCtx,并且key==ctx.key則返回,否則會(huì)將c賦值為 ctx.Context,繼續(xù)下一個(gè)循環(huán)
  • 如果是*cancelCtx并且key==&cancelCtxKey則說(shuō)明找到了,直接返回,否則c= ctx.上下文繼續(xù)
  • 如果是timerCtx,并且key== &cancelCtxKey則會(huì)返回內(nèi)部的cancelCtx
  • 如果是*emptyCtx 則直接返回nil,
  • 默認(rèn)即如果是用戶(hù)自定義實(shí)現(xiàn)則調(diào)用對(duì)應(yīng)的Value找尋

可以發(fā)現(xiàn)如果嵌套實(shí)現(xiàn)過(guò)多的話(huà)這個(gè)方法其實(shí)是一個(gè)遞歸調(diào)用。

如果是則要繼續(xù)判斷p.done與parent.Done()是否相等,如果沒(méi)有則說(shuō)明:*cancelCtx已經(jīng)被包裝在一個(gè)自定義實(shí)現(xiàn)中,提供了一個(gè)不同的包裝,在這種情況下就返回nil,false:

pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
  return nil, false
}
return p, true

構(gòu)造算是結(jié)束了,接下來(lái)看看如何取消的:

  • 檢查err是否為nil
 if err == nil {
    panic("context: internal error: missing cancel error")
  } 
  • 由于要對(duì)err、cancelCtx.done以及children進(jìn)行操作,所以要加鎖
  • 如果c.err不為nil則說(shuō)明已經(jīng)取消過(guò)了,直接返回。否則將c.err=err賦值,這里看到只有第一次調(diào)用才會(huì)賦值,多次調(diào)用由于已經(jīng)有 != nil+鎖的檢查,所以會(huì)直接返回,不會(huì)重復(fù)賦值
c.mu.Lock()
  if c.err != nil {
    c.mu.Unlock()
    return // already canceled
  }
       c.err = err
  • 會(huì)嘗試從c.done獲取,如果為nil,則保存一個(gè)closedchan,否則就關(guān)閉d,這樣當(dāng)你context.Done()方法返回的channel才會(huì)返回。
d, _ := c.done.Load().(chan struct{})
  if d == nil {
    c.done.Store(closedchan)
  } else {
    close(d)
  }
  • 循環(huán)遍歷c.children去關(guān)閉子Context,可以看到釋放子context時(shí)會(huì)獲取 子Context的鎖,同時(shí)也會(huì)獲取父Context的鎖。所以才是線(xiàn)程安全的。結(jié)束后釋放鎖
     d, _ := c.done.Load().(chan struct{})
  if d == nil {
    c.done.Store(closedchan)
  } else {
    close(d)
  }
  • 如果要將其從父Context刪除為true,則將其從父上下文刪除
if removeFromParent {
    removeChild(c.Context, c)
  }

removeChild也比較簡(jiǎn)單,當(dāng)為*cancelCtx就將其從Children內(nèi)刪除,為了保證線(xiàn)程安全也是加鎖的。

func removeChild(parent Context, child canceler) {
  p, ok := parentCancelCtx(parent)
  if !ok {
    return
  }
  p.mu.Lock()
  if p.children != nil {
    delete(p.children, child)
  }
  p.mu.Unlock()
}

Done就是返回一個(gè)channel用于告知應(yīng)用程序任務(wù)已經(jīng)終止:這一步是只讀沒(méi)有加鎖,如果沒(méi)有讀取到則嘗試加鎖,再讀一次,還沒(méi)讀到則創(chuàng)建一個(gè)chan,可以看到這是一個(gè)懶創(chuàng)建的過(guò)程。所以當(dāng)用戶(hù)主動(dòng)調(diào)用CancelFunc時(shí),其實(shí)根本就是將c.done內(nèi)存儲(chǔ)的chan close掉,這其中可能牽扯到父關(guān)閉,也要循環(huán)關(guān)閉子Context過(guò)程。

func (c *cancelCtx) Done() <-chan struct{} {
  d := c.done.Load()
  if d != nil {
    return d.(chan struct{})
  }
  c.mu.Lock()
  defer c.mu.Unlock()
  d = c.done.Load()
  if d == nil {
    d = make(chan struct{})
    c.done.Store(d)
  }
  return d.(chan struct{})
}

cancelCtx主要內(nèi)容就這么多,接下里就是timerCtx了

計(jì)時(shí)器

回顧下timerCtx定義,就是內(nèi)嵌了一個(gè)cancelCtx另外多了兩個(gè)字段timer和deadline,這也是組合的體現(xiàn)。

type timerCtx struct {
  cancelCtx
  timer *time.Timer // Under cancelCtx.mu.
 
  deadline time.Time
}

下面就看看兩個(gè)構(gòu)造函數(shù),WithDeadline與WithTimeout,WithTimeout就是對(duì)WithDealine的一層簡(jiǎn)單封裝。

檢查不多說(shuō)了, 第二個(gè)檢查如果父context的截止時(shí)間比傳遞進(jìn)來(lái)的早的話(huà),這個(gè)時(shí)間就無(wú)用了,那么就退化成cancelCtx了。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  if parent == nil {
    panic("cannot create context from nil parent")
  }
  if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    return WithCancel(parent)
  }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
  return WithDeadline(parent, time.Now().Add(timeout))
}

構(gòu)造timerCtx并調(diào)用propagateCancel,這個(gè)已經(jīng)在上面介紹過(guò)了。

c := &timerCtx{
    cancelCtx: newCancelCtx(parent),
    deadline:  d,
  }
  propagateCancel(parent, c)

接著會(huì)看,會(huì)先利用time.直到(d.分時(shí)。Now()) 來(lái)判斷傳入的 deadlineTime與當(dāng)前時(shí)間差值,如果在當(dāng)前時(shí)間之前的話(huà)說(shuō)明已經(jīng)該取消了,所以會(huì)直接調(diào)用cancel函數(shù)進(jìn)行取消,并且將其從父Context中刪除。否則就創(chuàng)建一個(gè)定時(shí)器,當(dāng)時(shí)間到達(dá)會(huì)調(diào)用取消函數(shù),這里是定時(shí)調(diào)用,也可能用戶(hù)主動(dòng)調(diào)用。

dur := time.Until(d)
  if dur <= 0 {
    c.cancel(true, DeadlineExceeded)
    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) }

下面看看cancel實(shí)現(xiàn)吧,相比較cancelCtx就比較簡(jiǎn)單了,先取消 cancelCtx,也要加鎖,將c.timer停止并賦值nil,這里也是第一次調(diào)用才會(huì)賦值nil,因?yàn)橥鈱舆€有個(gè)c.timer !=nil的判斷,所以多次調(diào)用只有一次賦值。

func (c *timerCtx) cancel(removeFromParent bool, err error) {
  c.cancelCtx.cancel(false, err)
  if removeFromParent {
    // Remove this timerCtx from its parent cancelCtx's children.
    removeChild(c.cancelCtx.Context, c)
  }
  c.mu.Lock()
  if c.timer != nil {
    c.timer.Stop()
    c.timer = nil
  }
  c.mu.Unlock()
}

相比較于cancelCtx還覆蓋實(shí)現(xiàn)了一個(gè)Deadline(),就是返回當(dāng)前 Context的終止時(shí)間。

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
  return c.deadline, true
}

下面就到了最后一個(gè)內(nèi)置的valueCtx了。

結(jié)構(gòu)器就更加加單,就多了key,val

type valueCtx struct {
 Context
 key, val any
}

也就有個(gè)Value method不同,可以看到底層使用的就是我們上面介紹的value函數(shù),重復(fù)復(fù)用

func (c *valueCtx) Value(key any) any {
  if c.key == key {
    return c.val
  }
  return value(c.Context, key)
}

幾個(gè)主要的講解完了,可以看到不到600行代碼,就實(shí)現(xiàn)了這么多功能,其中蘊(yùn)含了組合、封裝、結(jié)構(gòu)體嵌套接口等許多理念,值得好好琢磨。下面我們?cè)倏纯雌渲杏行┯幸馑嫉牡胤?。我們一般打印字符串都是使?fmt 包,那么不使用fmt包該如何打印呢?context包里就有相應(yīng)實(shí)現(xiàn),也很簡(jiǎn)單,就是 switch case來(lái)判斷v類(lèi)型并返回,它這么做的原因也有說(shuō):

“因?yàn)槲覀儾幌M舷挛囊蕾?lài)于unicode表”,這句話(huà)我還沒(méi)理解,有知道的小伙伴可以在底下評(píng)論,或者等我有時(shí)間看看fmt包實(shí)現(xiàn)。

func stringify(v any) string {
  switch s := v.(type) {
  case stringer:
    return s.String()
  case string:
    return s
  }
  return "<not Stringer>"
}
 
func (c *valueCtx) String() string {
  return contextName(c.Context) + ".WithValue(type " +
    reflectlite.TypeOf(c.key).String() +
    ", val " + stringify(c.val) + ")"
}

使用Context的幾個(gè)原則

直接在函數(shù)參數(shù)傳遞,不要在struct傳遞,要明確傳遞,并且作為第一個(gè)參數(shù),因?yàn)檫@樣可以由調(diào)用方來(lái)傳遞不同的上下文在不同的方法上,如果你在 struct內(nèi)使用context則一個(gè)實(shí)例是公用一個(gè)context也就導(dǎo)致了協(xié)程不安全,這也是為何net包Request要拷貝一個(gè)新的Request WithRequest(context go 1.7 才被引入),net包牽扯過(guò)多,要做到兼容才嵌入到 struct內(nèi)。

不要使用nil而當(dāng)你不知道使用什么時(shí)則使用TODO,如果你用了nil則會(huì) panic。避免到處判斷是否為nil。

WithValue不應(yīng)該傳遞業(yè)務(wù)信息,只應(yīng)該傳遞類(lèi)似request-id之類(lèi)的請(qǐng)求信息。

無(wú)論用哪個(gè)類(lèi)型的Context,在構(gòu)建后,一定要加上:defer cancel(),因?yàn)檫@個(gè)函數(shù)是可以多次調(diào)用的,但是如果沒(méi)有調(diào)用則可能導(dǎo)致Context沒(méi)有被取消繼而其關(guān)聯(lián)的上下文資源也得不到釋放。

在使用WithValue時(shí),包應(yīng)該將鍵定義為未導(dǎo)出的類(lèi)型以避免發(fā)生碰撞,這里貼個(gè)官網(wǎng)的例子:

// package user 這里為了演示直接在 main 包定義
// User 是存儲(chǔ)在 Context 值
type User struct {
  Name string
  Age  int
}
 
// key 是非導(dǎo)出的,可以防止碰撞
type key int
 
// userKey 是存儲(chǔ) User 類(lèi)型的鍵值,也是非導(dǎo)出的。
var userKey key
 
// NewContext 創(chuàng)建一個(gè)新的 Context,攜帶 *User
func NewContext(ctx context.Context, u *User) context.Context {
  return context.WithValue(ctx, userKey, u)
}
 
// FromContext 返回存儲(chǔ)在 ctx 中的 *User
func FromContext(ctx context.Context) (*User, bool) {
  u, ok := ctx.Value(userKey).(*User)
  return u, ok
}

那怎么能夠防止碰撞呢?可以做個(gè)示例:看最后輸出,我們?cè)诘谝恍芯陀?userKey的值0,存儲(chǔ)了一個(gè)值“a”。

然后再利用NewContext存儲(chǔ)了&User,底層實(shí)際用的是 context.WithValue(ctx,userKey,u)

讀取時(shí)用的是FromContext,兩次存儲(chǔ)即使底層的key值都為0, 但是互不影響,這是為什么呢?

還記得WithValue怎么實(shí)現(xiàn)的么?你每調(diào)用一次都會(huì)包一層,并且一層一層解析,而且它會(huì)比較c.key==key,這里記住go的==比較是比較值和類(lèi)型的,二者都相等才為true,而我們使用type key int所以u(píng)serKey與0底層值雖然一樣,但是類(lèi)型已經(jīng)不一樣了(這里就是main.userKey與0),所以外部無(wú)論定義何種類(lèi)型都無(wú)法影響包內(nèi)的類(lèi)型。這也是容易令人迷惑的地方

package main
import (
  "context"
  "fmt"
)
func main() {
  ctx := context.WithValue(context.Background(), , "a")
  ctx = NewContext(ctx, &User{})
  v, _ := FromContext(ctx)
  fmt.Println(ctx.Value(0), v) // Output: a, &{ 0}
}

以上就是從Context到go設(shè)計(jì)理念輕松上手教程的詳細(xì)內(nèi)容,更多關(guān)于Context到go設(shè)計(jì)理念的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go語(yǔ)言中init函數(shù)特點(diǎn)、用途和注意事項(xiàng)詳解

    Go語(yǔ)言中init函數(shù)特點(diǎn)、用途和注意事項(xiàng)詳解

    go語(yǔ)言中有一個(gè)非常神奇的函數(shù)init,它可以在所有程序執(zhí)行開(kāi)始前被執(zhí)行,并且每個(gè)package下面可以存在多個(gè)init函數(shù),這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言中init函數(shù)特點(diǎn)、用途和注意事項(xiàng)的相關(guān)資料,需要的朋友可以參考下
    2023-07-07
  • 基于Go語(yǔ)言構(gòu)建RESTful API服務(wù)

    基于Go語(yǔ)言構(gòu)建RESTful API服務(wù)

    在實(shí)際開(kāi)發(fā)項(xiàng)目中,你編寫(xiě)的服務(wù)可以被其他服務(wù)使用,這樣就組成了微服務(wù)的架構(gòu);也可以被前端調(diào)用,這樣就可以前后端分離。那么,本文主要介紹什么是 RESTful API,以及 Go 語(yǔ)言是如何玩轉(zhuǎn) RESTful API 的
    2021-07-07
  • 一文詳解Go中方法接收器的選擇

    一文詳解Go中方法接收器的選擇

    許多 Go 初學(xué)者在方法接收器的選擇上可能會(huì)感到困惑,不知道該選擇值接收器還是指針接收器。本文將會(huì)對(duì)方法接收器進(jìn)行介紹,并給出如何選擇正確方法接收器的指導(dǎo)建議,希望對(duì)大家有所幫助
    2023-04-04
  • golang編程入門(mén)之http請(qǐng)求天氣實(shí)例

    golang編程入門(mén)之http請(qǐng)求天氣實(shí)例

    這篇文章主要介紹了golang編程入門(mén)之http請(qǐng)求天氣實(shí)例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-08-08
  • 如何有效控制Go線(xiàn)程數(shù)實(shí)例探究

    如何有效控制Go線(xiàn)程數(shù)實(shí)例探究

    這篇文章主要為大家介紹了如何有效控制?Go?線(xiàn)程數(shù)的問(wèn)題探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • GoLang RabbitMQ實(shí)現(xiàn)六種工作模式示例

    GoLang RabbitMQ實(shí)現(xiàn)六種工作模式示例

    這篇文章主要介紹了GoLang RabbitMQ實(shí)現(xiàn)六種工作模式,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-12-12
  • goland 清除所有的默認(rèn)設(shè)置操作

    goland 清除所有的默認(rèn)設(shè)置操作

    這篇文章主要介紹了goland 清除所有的默認(rèn)設(shè)置操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-04-04
  • 使用go讀取gzip格式的壓縮包的操作

    使用go讀取gzip格式的壓縮包的操作

    這篇文章主要介紹了使用go讀取gzip格式的壓縮包的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-12-12
  • Golang中runtime的使用詳解

    Golang中runtime的使用詳解

    這篇文章主要介紹了Golang中runtime的使用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • Golang 如何限制木馬圖片上傳服務(wù)器的實(shí)例

    Golang 如何限制木馬圖片上傳服務(wù)器的實(shí)例

    本文主要介紹了Golang 如何限制木馬圖片上傳服務(wù)器的實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-02-02

最新評(píng)論