轻松上手!手把手带你掌握从context到go设计理念

腾讯云开发者 腾讯云开发者     2022-12-04     142

关键词:

导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯后端开发工程师陈雪锋。

context包比较小,是阅读源码比较理想的一个入手,并且里面也涵盖了许多go设计理念可以学习。

go的Context作为go并发方式的一种,无论是在源码net/http中,开源框架例如gin中,还是内部框架trpc-go中都是一个比较重要的存在,而整个 context 的实现也就不到600行,所以也想借着这次机会来学习学习,本文基于go 1.18.4。话不多说,例:

为了使可能对context不太熟悉的同学有个熟悉,先来个example ,摘自源码:

我们利用WithCancel创建一个可取消的Context,并且遍历频道输出,当 n==5时,主动调用cancel来取消。

而在gen func中有个协程来监听ctx当监听到ctx.Done()即被取消后就退出协程。

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() // 实际使用中应该在这里调用 cancel


  for n := range gen(ctx) 
    fmt.Println(n)
    if n == 5 
                       cancel() // 这里为了使不熟悉 go 的更能明白在这里调用了 cancel()
      break
    
  
  // Output:
  // 1
  // 2
  // 3
  // 4
  // 5

这是最基本的使用方法。

概览

对于context包先上一张图,便于大家有个初步了解(内部函数并未全列举,后续会逐一讲解):

最重要的就是右边的接口部分,可以看到有几个比较重要的接口,下面逐一来说下:

type Context interface


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


        Done() <-chan struct
        Err() error


        Value(key any) any


首先就是Context接口,这是整个context包的核心接口,就包含了四个 method,分别是:

Deadline() (deadline time.Time, ok bool) // 获取 deadline 时间,如果没有的话 ok 会返回 false
Done() <-chan struct // 返回的是一个 channel ,用来应用监听任务是否已经完成
Err() error // 返回取消原因 例如:Canceled\\DeadlineExceeded
Value(key any) any // 根据指定的 key 获取是否存在其 value 有则返回

可以看到这个接口非常清晰简单明了,并且没有过多的Method,这也是go 设计理念,接口尽量简单、小巧,通过组合来实现丰富的功能,后面会看到如何组合的。

再来看另一个接口canceler,这是一个取消接口,其中一个非导出 method cancel,接收一个bool和一个error,bool用来决定是否将其从父Context中移除,err用来标明被取消的原因。还有个Done()和Context接口一样,这个接口为何这么设计,后面再揭晓。

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

接下来看这两个接口的实现者都有谁,首先Context直接实现者有 *valueCtx(比较简单放最后讲)和*emptyCtx

而canceler直接实现者有*cancelCtx和*timerCtx ,并且这两个同时也实现了Context接口(记住我前面说得另外两个是直接实现,这俩是嵌套接口实现松耦合,后面再说具体好处),下面逐一讲解每个实现。

空的

见名知义,这是一个空实现,事实也的确如此,可以看到啥啥都没有,就是个空实现,为何要写呢?

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"

再往下读源码会发现两个有意思的变量,底层一模一样,一个取名叫 background,一个取名叫todo,为何呢?耐心的可以看看解释,其实是为了方便大家区分使用,背景 是在入口处来传递最初始的context,而todo 则是当你不知道用啥,或者你的函数虽然接收ctontext参数,但是并没有做任何实现时,那么就使用todo即可。后续如果有具体实现再传入具体的上下文。所以上面才定义了一个空实现,就为了给这俩使用呢,这俩也是我们最常在入口处使用的。

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, 懒创建,
  //  只有第一次被 cancel 时才会关闭
  done     atomic.Value  
  // children 存放的是子 Context canceler ,并且当第一次被 cancel 时会被
  // 设为 nil
  children map[canceler]struct 
  //  第一次被调用 cancel 时,会被设置
  err      error                 



type timerCtx struct
 cancelCtx
 timer *time.Timer // 定时器,用来监听是否超时该取消
 deadline time.Time // 终止时间



type valueCtx struct 
 Context
 key, val any

这里就看出来为何cancelCtx为非导出了,因为它通过内嵌Context接口也也是实现了Context的。并且通过这种方式实现了松耦合,可以通过 WithCancel(父Context) (ctx Context,cancel CancelFunc) 来传递任何自定义的Context实现。

而timerCtx是嵌套的cancelCtx,同样他也可以同时调用Context接口所有 method与cancelCtx所有method ,并且还可以重写部分方法。而 valueCtx和上面两个比较独立,所以直接嵌套的Context。

这里应该也看明白了为何canceler为何一个可导出Done一个不可导出 cancel,Done是重写Context的method会由上层调用,所以要可导出, cancel则是由return func()c.cancel(false,DeadlineExeceed) 类似的封装导出,所以不应该导出。

这是go中推崇的通过组合而非继承来编写代码。其中字段解释我已在后面注明,后面也会讲到。看懂了大的一个设计理念,下面我们就逐一击破,通过上面可以看到timerCtx其实是复用了cancelCtx能力,所以cancelCtx最为重要,下面我们就先将cancelCtx实现。

取消

它非导出,是通过一个方法来直接返回Context类型的,这也是go理念之一,不暴露实现者,只暴露接口(前提是实现者中的可导出method不包含接口之外的method, 否则导出的method外面也无法调用)。

先看看外部构造函数WithCancel,

  • 先判断parent是否为nil,如果为nil就panic,这是为了避免到处判断是否为nil。所以永远不要使用nil来作为一个Context传递。

  • 接着将父Context封装到cancelCtx并返回,这没啥说得,虽然只有一行代码,但是多处使用,所以做了封装,并且后续如果要更改行为调用者也无需更改。很方便。

  • 调用propagateCancel,这个函数作用就是当parent是可以被取消的时候就会对子Context也进行取消的取消或者准备取消动作。

  • 返回Context与CancelFunc type >CancelFunc func()就是一个 type func别名,底层封装的是c.cancel方法,为何这么做呢?这是为了给上层应用一个统一的调用,cancelCtx与timerCtx以及其他可以实现不同的cancel但是对上层是透明并且一致的行为就可。这个func应该是协程安全并且多次调用只有第一次调用才有效果。

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 cancelCtxContext: parent

接下来就来到比较重要的func  propagateCancel,我们看看它做了啥,

首先是判断父context的Done()方法返回的channel是否为nil,如果是则直接返回啥也不做了。这是因为父Context从来不会被取消的话,那就没必要进行下面动作。这也表名我们使用.与猫(上下文。Background()) 这个函数是不会做任何动作的。

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

接下里就是一个select ,如果父Context已经被取消了的话,那就直接取消子Context就好了,这个也理所应当,父亲都被取消了,儿子当然也应该取消,没有存在必要了。

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

如果父 Context 没有被取消,这里就会做个判断,

  • 看看parent是否是一个*cancelCtx,如果是的话就返回其p,再次检查 p.err是否为nil,如果不为nil就说明parent被取消,接着取消 子 Context,如果没被取消的话,就将其加入到p.children中,看到这里的 map是个canceler,可以接收任何实现取消器 的类型。这里为何要加锁呢?因为要对p.err以及p.children进行读取与写入操作,要确保协程安全所以才加的锁。

  • 如果不是*cancelCtx类型就说明parent是个被封装的其他实现 Context 接口的类型,则会将goroutines是个int加1这是为了测试使用的,可以不管它。并且会启动个协程,监听父Context ,如果父Context被取消,则取消子Context,如果监听到子Context已经结束(可能是上层主动调用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():
      
    ()
  

接下来看看parentCancelCtx的实现:它是为了找寻parent底下的 *cancelCtx,

它首先检查parent.Done()如果是一个closedchan这个频道 在初始化时已经是个一个被关闭的通道或者未nil的话(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类型,如果不是则返回nil,false,这里调用了parent.Value方法,并最终可能会落到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则返回,否则会将c赋值为 ctx.Context,继续下一个循环

  • 如果是*cancelCtx并且key==&cancelCtxKey则说明找到了,直接返回,否则c= ctx.上下文继续

  • 如果是*timerCtx,并且key== &cancelCtxKey则会返回内部的*cancelCtx

  • 如果是*emptyCtx 则直接返回nil,

  • 默认即如果是用户自定义实现则调用对应的Value找寻

可以发现如果嵌套实现过多的话这个方法其实是一个递归调用。

如果是则要继续判断p.done与parent.Done()是否相等,如果没有则说明:*cancelCtx已经被包装在一个自定义实现中,提供了一个不同的包装,在这种情况下就返回nil,false:

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

return p, true

构造算是结束了,接下来看看如何取消的:

  • 检查err是否为nil

if err == nil 
    panic("context: internal error: missing cancel error")
  
  • 由于要对err、cancelCtx.done以及children进行操作,所以要加锁

  • 如果c.err不为nil则说明已经取消过了,直接返回。否则将c.err=err赋值,这里看到只有第一次调用才会赋值,多次调用由于已经有 != nil+锁的检查,所以会直接返回,不会重复赋值

c.mu.Lock()
  if c.err != nil 
    c.mu.Unlock()
    return // already canceled
  
       c.err = err
  • 会尝试从c.done获取,如果为nil,则保存一个closedchan,否则就关闭d,这样当你context.Done()方法返回的channel才会返回。

d, _ := c.done.Load().(chan struct)
  if d == nil 
    c.done.Store(closedchan)
   else 
    close(d)
  
  • 循环遍历c.children去关闭子Context,可以看到释放子context时会获取 子Context的锁,同时也会获取父Context的锁。所以才是线程安全的。结束后释放锁

for child := range c.children 
    // NOTE: acquiring the child's lock while holding parent's lock.
    child.cancel(false, err)
  
  c.children = nil
  c.mu.Unlock()
  • 如果要将其从父Context删除为true,则将其从父上下文删除

if removeFromParent 
    removeChild(c.Context, c)
  

removeChild也比较简单,当为*cancelCtx就将其从Children内删除,为了保证线程安全也是加锁的。

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就是返回一个channel用于告知应用程序任务已经终止:这一步是只读没有加锁,如果没有读取到则尝试加锁,再读一次,还没读到则创建一个chan,可以看到这是一个懒创建的过程。所以当用户主动调用CancelFunc时,其实根本就是将c.done内存储的chan close掉,这其中可能牵扯到父关闭,也要循环关闭子Context过程。

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主要内容就这么多,接下里就是timerCtx了

计时器

回顾下timerCtx定义,就是内嵌了一个cancelCtx另外多了两个字段timer和deadline,这也是组合的体现。

type timerCtx struct 
  cancelCtx
  timer *time.Timer // Under cancelCtx.mu.


  deadline time.Time

下面就看看两个构造函数,WithDeadline与WithTimeout,WithTimeout就是对WithDealine的一层简单封装。

检查不多说了, 第二个检查如果父context的截止时间比传递进来的早的话,这个时间就无用了,那么就退化成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))

构造timerCtx并调用propagateCancel,这个已经在上面介绍过了。

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

接着会看,会先利用time.直到(d.分时。Now()) 来判断传入的 deadlineTime与当前时间差值,如果在当前时间之前的话说明已经该取消了,所以会直接调用cancel函数进行取消,并且将其从父Context中删除。否则就创建一个定时器,当时间到达会调用取消函数,这里是定时调用,也可能用户主动调用。

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实现吧,相比较cancelCtx就比较简单了,先取消 cancelCtx,也要加锁,将c.timer停止并赋值nil,这里也是第一次调用才会赋值nil,因为外层还有个c.timer !=nil的判断,所以多次调用只有一次赋值。

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还覆盖实现了一个Deadline(),就是返回当前 Context的终止时间。

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

下面就到了最后一个内置的valueCtx了。

结构器就更加加单,就多了key,val

type valueCtx struct 
 Context
 key, val any

也就有个Value method不同,可以看到底层使用的就是我们上面介绍的value函数,重复复用

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

几个主要的讲解完了,可以看到不到600行代码,就实现了这么多功能,其中蕴含了组合、封装、结构体嵌套接口等许多理念,值得好好琢磨。下面我们再看看其中有些有意思的地方。我们一般打印字符串都是使用 fmt 包,那么不使用fmt包该如何打印呢?context包里就有相应实现,也很简单,就是 switch case来判断v类型并返回,它这么做的原因也有说:

“因为我们不希望上下文依赖于unicode表”,这句话我还没理解,有知道的小伙伴可以在底下评论,或者等我有时间看看fmt包实现。

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的几个原则

直接在函数参数传递,不要在struct传递,要明确传递,并且作为第一个参数,因为这样可以由调用方来传递不同的上下文在不同的方法上,如果你在 struct内使用context则一个实例是公用一个context也就导致了协程不安全,这也是为何net包Request要拷贝一个新的Request WithRequest(context go 1.7 才被引入),net包牵扯过多,要做到兼容才嵌入到 struct内。

不要使用nil而当你不知道使用什么时则使用TODO,如果你用了nil则会 panic。避免到处判断是否为nil。

WithValue不应该传递业务信息,只应该传递类似request-id之类的请求信息。

无论用哪个类型的Context,在构建后,一定要加上:defer cancel(),因为这个函数是可以多次调用的,但是如果没有调用则可能导致Context没有被取消继而其关联的上下文资源也得不到释放。

在使用WithValue时,包应该将键定义为未导出的类型以避免发生碰撞,这里贴个官网的例子:

// package user 这里为了演示直接在 main 包定义
// User 是存储在 Context 值
type User struct 
  Name string
  Age  int



// key 是非导出的,可以防止碰撞
type key int


// userKey 是存储 User 类型的键值,也是非导出的。
var userKey key


// NewContext 创建一个新的 Context,携带 *User
func NewContext(ctx context.Context, u *User) context.Context 
  return context.WithValue(ctx, userKey, u)



// FromContext 返回存储在 ctx 中的 *User
func FromContext(ctx context.Context) (*User, bool) 
  u, ok := ctx.Value(userKey).(*User)
  return u, ok

那怎么能够防止碰撞呢?可以做个示例:看最后输出,我们在第一行就用 userKey的值0,存储了一个值“a”。

然后再利用NewContext存储了&User,底层实际用的是 context.WithValue(ctx,userKey,u)

读取时用的是FromContext,两次存储即使底层的key值都为0, 但是互不影响,这是为什么呢?

还记得WithValue怎么实现的么?你每调用一次都会包一层,并且一层一层解析,而且它会比较c.key==key,这里记住go的==比较是比较值和类型的,二者都相等才为true,而我们使用type key int所以userKey与0底层值虽然一样,但是类型已经不一样了(这里就是main.userKey与0),所以外部无论定义何种类型都无法影响包内的类型。这也是容易令人迷惑的地方

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

 作者简介

陈雪锋

腾讯后端开发工程师

腾讯后端开发工程师,有多年golang以及云原生开发经验,对云原生、容器调度、监控系统、API网关也多有涉猎。

 推荐阅读

深入浅出带你走进Redis!

揭秘KVM年度核心技术突破的背后原理!

避坑指南!如何在TKE上安装KubeSphere?

一站式DevOps真的能提速增效吗?TVP吐槽大会邀您来验证!

9 月 24 日,CODING DevOps 专题 TVP 吐槽大会火爆开启,一同见证领域大咖巅峰对决!

扫码立即参会赢好礼👇

👇点击「阅读原文」,注册成为社区创作者,认识大咖,打造你的技术影响力!

前端新手看过来,手把手带你轻松上手html的实操

超文本标记语言(英语:HypertextMarkupLanguage,简称:HTML )是一种用来结构化Web网页及其内容的标记语言。网页内容可以是:一组段落、一个重点信息列表、也可以含有图片和数据表。正如标题所示,本文将对HTML及其功能做一个... 查看详情

带你十天轻松搞定go微服务系列(代码片段)

本文开始,我们会出一个系列文章跟大家详细展示一个go-zero微服务示例,整个系列分十篇文章,目录结构如下:环境搭建(本文)服务拆分用户服务产品服务订单服务支付服务RPC服务Auth验证服务监控链路追踪分布式事务期望通... 查看详情

带你十天轻松搞定go微服务系列(代码片段)

本文开始,我们会出一个系列文章跟大家详细展示一个go-zero微服务示例,整个系列分十篇文章,目录结构如下:环境搭建(本文)服务拆分用户服务产品服务订单服务支付服务RPC服务Auth验证服务监控链路追踪分布式事务期望通... 查看详情

了解javaweb开发的技术栈从掌握tomcat的使用开始,手把手带你用maven创建web项目

...的各个对象作用,掌握Druid的使用🍬Maven从安装到手把手教学进行项目管理🍬MyBatis完成代理方式查询数据以及核心文件配置、ἶ 查看详情

带你十天轻松搞定go微服务系列(九链路追踪)(代码片段)

...个系列分十篇文章,目录结构如下:环境搭建:带你十天轻松搞定Go微服务系列(一)服务拆分:带你十天轻松搞定Go微服务系列(二)用户服务:带你十天轻松搞定Go微服务系列(三)产品服务:带你十天轻松搞定Go微服务系列... 查看详情

带你十天轻松搞定go微服务系列(代码片段)

...个系列分十篇文章,目录结构如下:环境搭建:带你十天轻松搞定Go微服务系列(一)服务拆分:带你十天轻松搞定Go微服务系列(二)用户服务:带你十天轻松搞定Go微服务系列(三)产品服务:带你十天轻松搞定Go微服务系列... 查看详情

ui动效设计从入门到项目实战高清无密百度云盘

...力!本课程采用基础理论与实战案例相结合的教学方式,手把手带你学习炫酷的动效设计! 第1章课程简介介绍该课程的学习内容,以及课程内的案例效果展示1-1课程介绍第2章动效基础知识学习动效基础操作知识点梳理,快... 查看详情

手把手带你使用go-kit(代码片段)

手把手带你使用go-kitgo-kit是什么Gokit是一个微服务工具包集合。利用它提供的额API和规范可以创建健壮、可维护性高的微服务体系Go-kit的三层架构1、Service这里就是我们的业务类、接口等相关信息存放2、EndPoint定义Request、Response... 查看详情

一文带你轻松掌握多种编程范式(代码片段)

一文带你轻松掌握多种编程范式前言结构化程序的设计基于对象的程序设计面向对象的程序设计基于接口的程序设计基于接口编程的模板实现前言 编程范式有多种,主要有结构化的程序设计思想、基于对象的程序设计思想... 查看详情

3万字长文记录docker最全学习笔记,手把手带你入个门

前言以下是我为大家准备的几个精品专栏,喜欢的小伙伴可自行订阅,你的支持就是我不断更新的动力哟!MATLAB-30天带你从入门到精通MATLAB深入理解高级教程(附源码)tableau可视化数据分析高级教程 docker不是一个值得投入... 查看详情

spring入门到精通,一文带你轻松搞定spring!

Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。本文章将深入浅出讲解Spring的核心技术IoC、AOP,剖析框架的源代码。让大家快速掌握框... 查看详情

字节内部超全kotlin学习笔记,快速上手kotlin开发,基础+实战+源码,手把手带你吃透kotlin语法与协程。(代码片段)

目前市面上主流的App和库,大都是使用Kotlin语言开发的,在PlayStore的前1000个应用程序中有80%以上使用Kotlin。这也意味着,对于Android应用开发者来说,仅仅掌握Java是不够的,你还应该学习Kotlin,才能追上技... 查看详情

手把手带你使用策略模式实现表单校验(代码片段)

策略模式文章目录策略模式前言一了解策略模式二实现表单校验前言你的项目有哪些亮点?你对设计模式是否了解?这些都是面试官常问的问题,这两个问题都可以通过本文解决。毋庸置疑,使用策略模式实现表... 查看详情

spring实战入门,带你轻松掌握spring框架

Spring框架是什么?Spring是于2003年兴起的一个轻量级的Java开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IoC)和面向切面编程(AOP)。Spring是可以在JavaSE/EE中使用的轻量级开源框架。Spring的主... 查看详情

servicemesh实战手把手带你学会istio

ServiceMesh技术历经三年多,无论是国内还是国外,这门新技术相关的资料和书籍都太少,网络上也鲜有成体系的课程,没有办法很好地理解ServiceMesh的核心功能。ServiceMesh作为下一代微服务技术的代名词,一鸣惊人,大有一统微服... 查看详情

利用go优越的性能设计与实现高性能企业级微服务网关

...协议与数据包、tcp粘包拆包及udp、tcp、http等代码编写。轻松领略到网络编程快感。第3章网络代理之HTTP代理*基础篇【nginx高大上功能,用go实现起来很e 查看详情

手把手带你搞定c语言实现三子棋游戏,让你的代码有趣起来(超详细教程,从思路到代码,快码起来!)(代码片段)

【手把手带你搞定】C语言实现三子棋游戏什么是三子棋游戏分析代码实现菜单打印井字格创建二维棋盘数组初始化棋盘数组打印棋盘下棋玩家下棋电脑下棋判断结果完整代码总结在学习完函数和数组的基本使用之后,我们应... 查看详情

手把手带你熟悉一个前端项目

带你快速上手项目,以https://github.com/duxianwei520/react为例项目文档自行阅读项目文档,了解项目的整体内容。package.json自行阅读代码,了解项目所用的技术栈。该项目主要技术栈是react+redux+antd+webpack+axios目... 查看详情