Go源码--context包

news2025/1/19 3:07:44

简介

Context 是go语言比较重要的且也是比较复杂的一个结构体,Context主要有两种功能:

  • 取消信号:包括直接取消(涉及的结构体:cancelCtx ; 涉及函数:WithCancel)和携带截止日期的取消(涉及结构体:timeCtx,cancelCtx;涉及函数:WithTimeout/WithDeadline/WithDeadlineCause);一般用来控制主从协程,当主协程执行取消操作时,当然希望从携程也执行取消操作(或者其他设定的操作),这样主协程可以对从协程的生命周期有控制权。
  • 传递消息:包括跨API边界和进程间传递(涉及结构体:valueCtx;涉及函数:WithValue);主从协程可以共享数据,这样在Context形成的带回溯的树状结构中,通过回溯任何协程可以根据key获取其他协程注册在valueCtx(继承了context)结构体中的value,从而做到在整个主从协程中共享数据。

由于上述涉及的结构体入参 都包含Context 返回值也包含Context,所以继承自Context的结构体 都可以是入参,这样上述函数就可以组合使用。WithValue一般和/WithCancel/WithTimeout/WithDeadline/WithDeadlineCause搭配使用,这样既可以控制从协程生命周期,又可以传递数据。这几个结构体的组合可以产生各种应用场景。我后续会介绍这种组合。下面我们先梳理下几种重要的接口和结构体吧。

重要结构体

Context接口

Context接口是梦开始的地方,拥有最崇高的地位。我们来看下其结构体

type Context interface {
// Deadline 返回代表此上下文完成工作时应取消的时间。
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}  // 返回 chan类型 如果返回的不是关闭的 则阻塞 否则 返回关闭的chan 不阻塞
	Err() error  // 返回Done不阻塞的原因 如果是 Canceled的则 返回cancel的原因 所以 这个参数不为空 不意味着是运行出错了
	Value(key any) any   // 返回 key对应的value,可以递归调用。用于查找整个context树上跟key匹配的value
}
emptyCtx 结构体

emptyCtx 采用空值实现了 Context的四个函数

type emptyCtx struct{}
backgroundCtx 结构体
type backgroundCtx struct{ emptyCtx }

其采用组合的方式来包含emptyCtx,从而可以调用其函数值,也就是说实现了Context这个结构体,这就是结构体嵌入的优势。todoCtx结构体 跟backgroundCtx 结构一样。其主要用来作为树形结构的根节点,所以其实现的context函数都是空实现。

cancelCtx 结构体

介绍 这个结构体前我们先来看下 canceler 接口

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

canceler 的实现是 *cancelCtx和 *timerCtx,主要为context类型结构体进行功能补充,可以进行取消操作。

cancelCtx 结构体如下:

// 可以被取消,当被取消,也会取消任何实现了canceler结构体的孩子Context
type cancelCtx struct {
	Context

	mu       sync.Mutex            // 保护下面的属性
	done     atomic.Value          // done存了chan struct{},被懒惰创建(使用时才创建)
	children map[canceler]struct{} // 实现了 canceler接口的孩子map,key有用,value没用。主要用来组成树形结构,因为key是cancler类型的
								    // 所以能作为key(除了根节点的树节点)的只有cancelCtx,timeCtx和嵌入这两个任何一个的结构体;
	err      error                 // 
	cause    error                 // 
}

cancelCtx主要用来主协程控制从协程的生命周期,主协程调用cance()函数后其chan关闭,其子协程也会执行 Done()(关闭的chan不再阻塞)函数取消阻塞来执行后续逻辑。这里分两种情况,一种是只有一个With函数,子协程不产生新的context,也就是大家经常用的,第二种情况是在子协程使用With函数来产生孩子节点,这时children就会发挥作用,会形成带回溯的树形结构。下文会有详细讲解。
跟cancelCtx直接相关的函数主要是 WithCancel。

timerCtx 结构体

timerCtx结构体有定时器和截止时间两个参数。它嵌入了一个cancelCtx结构体来实现Done和Err方法。通过停止计时器(到时间了)然后委托给cancelCtx.cancel来实现cancel,也就是说timerCtx的cancel是通过调用cancelCtx的cancel方法来实现的。我们来看下其结构体:

type timerCtx struct {
	cancelCtx // 内嵌取消context
	timer *time.Timer  // 定时器 用来定时取消操作

	deadline time.Time // 返回取消的时间戳
}

可以看到 由于内嵌 取消context其本身可以代表cancelCtx类型,可以断言成功。(这个特性可以将两者组合使用)
跟timerCtx直接相关的函数有 WithTimeout/WithDeadline/WithDeadlineCause三个。要再次强调下 只要是context的实现结构体都可以作为With系列函数的入参,所以不同context变体的组合可以实现各种复杂的主从控制和信息传递操作。以上两个结构体介绍的都是主从控制方面的,现在我们来看下传递参数相关的结构体。

valueCtx 结构体

其结构体如下:

type valueCtx struct {
	Context
	key, val any
}

这个结构体带来一个key-value对,其为key实现了Vaule函数,主要用来在树形结构中查找key对应的value。其他的方法调用都委托给了嵌入的Context。这样 通过 valueCtx和timerCtx或者cancelCtx的组合就能即传递参数又控制子协程。当然vauleCtx也可以嵌入其他Context来实现参数的传递,例如我们常用的 go http包。
我们来简要绘制一个图来看下如上结构体之间的关系。
在这里插入图片描述
简要介绍了结构体 接下来我们来介绍几种重要的函数,函数都是名如其人。

WithCancel

其英文解释如下:
// WithCancel returns a copy of parent with a new Done channel. The returned context’s Done channel is closed when the returned cancel function is called or when the parent context’s Done channel is closed, whichever happens first.Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.
翻译成中文是:
WithCancel返回带有Done 通道的上下文的副本,其实就是新建一个cancelCtx将其放入children key中。当返回的取消函数调用或者当父上下文通道已经关闭时,返回的上下文通道也会关闭。看哪个先发生吧。
其代码如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := withCancel(parent)  // 创造新cancelCtx 如果入参 ctx是cancelCtx类型 就将 新的cancelCtx放入其children key中
	return c, func() { c.cancel(true, Canceled, nil) } // 返回新 cancelCtx和 取消函数 
}

其使用例子如下:


func TestWithCancel(t *testing.T) {
	var wg sync.WaitGroup
	ctx, concel := context.WithCancel(context.Background())
	wg.Add(1)
	go doSomthing(ctx, &wg)
	time.Sleep(3 * time.Second)
	concel()
	wg.Wait()
}

func doSomthing(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
 	for {
		select {
		case <-ctx.Done():
			time.Sleep(2 * time.Second)

			fmt.Println("playing")
			return
		default:
			time.Sleep(1 * time.Second)
			fmt.Println("I am working")

		}

	}

}

其输出如下:

I am working
I am working
playing

我们可以看到 当还没有执行cancel时 doSoomthing 中 ctx.Done() 返回的chan是阻塞未关闭状态,当调用cancel()函数时,chan关闭,因为doSomthing()中入参是 context是一个接口,接口是入参,就会携带本身类型和其实现的结构体的指针。所以 ctx中的 chan就是唯一的,当主协程调用cancel关闭chan时,子协程ctx.Done函数 返回关闭的结构体也就不再阻塞。还记得我在介绍cancelCtx结构体时,最后一段的描述吗,这时候因为就一个With函数不会产生新的 cancelCtx。
接下来我们来深入剖析下WithCancel源码。
WithCancel 的withCancel函数源码如下:

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	// 创建一个新的 结构体 作为 符合条件的父节点 child; 树结构
	c := &cancelCtx{}

	// 如果符合某条件 将 子节点插入树中
	c.propagateCancel(parent, c)
	return c
}

其中 propagateCancel函数源码如下:

// propagateCancel 负责在父上下文被取消时,使子上下文也被取消。它设置了 cancelCtx 的父上下文(可以理解成树上的父子节点,制定一个节点的父节点和子节点)。
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	c.Context = parent // 回溯 指向父节点 删除本子节点用 方便GC 和 通过Value方法回溯

	// 父节点 主要是context.Background() 节点 没有必要取消 它是根节点(设想下 链表的根节点是不是都是空的,上文中TestWithCancel函数 执行到 
	// 这边就返回了 因为parent 是 context.Background() 是根节点 且 其Done是空实现)
	done := parent.Done()  
	if done == nil {
		return // parent is never canceled
	}
    // 
	// 父节点已经取消 后续就不用再执行了 因为 按照 context 的设计理念 父节点取消 肯定是想子节点也取消
	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

	// 将 parent Context 类型的 转换成 cancelCtx类型 然后构建 树形传递链(一夫多子 一子一父) map可能共享 所以需要枷锁
	if p, ok := parentCancelCtx(parent); ok {
		// parent is a *cancelCtx, or derives from one.
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err, p.cause)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
		return
	}

	// 略过
}

可以看到 上面有对孩子节点赋值的操作这是什么情况下发生的呢,接下来我们来看一个例子,是cancelCtx的另一个有传播树形链的使用场景。代码如下:



func doSomthing2(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println("playing2")
			return
		default:
			time.Sleep(1 * time.Second)
			fmt.Println("I am working2")

		}

	}

}

func doSomthing(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	ctx1, cancel1:= context.WithCancel(ctx)  // ctx 当入参 产生新的 ctx1 这时 ctx下文是 ctx1 上文是 Backgroud产生的空context
	defer cancel1() 
	go doSomthing2(ctx1, wg)

	for {
		select {
		case <-ctx.Done():
 			fmt.Println("playing")
			return
		default:
			time.Sleep(1 * time.Second)
			fmt.Println("I am working")

		}

	}

}

func TestWithCancel(t *testing.T) {
	var wg sync.WaitGroup
	ctx, concel := context.WithCancel(context.Background())
	wg.Add(2)
	go doSomthing(ctx, &wg)
	time.Sleep(3 * time.Second)
	concel()
	wg.Wait()
}

运行结果是:

I am working2
I am working
I am working
I am working2
I am working
playing
I am working2
playing2

可以看到 这时候 就形成了树形结构 我们来看下 形成的树:

在这里插入图片描述
如果 在子协程中 和子子协程中 运用With函数 进行嵌套最终可形成一颗很大的树,如图:
在这里插入图片描述
这时候 就形成比较复杂的主从控制方案,如果valueCtx再嵌套上cancelCtx,就可以根据key在树上寻找value(因为指针是双向的,可以根据value函数递归查找,只能往上找不能往下),这样可以做到参数的向下传递。但我们一般常用的是主协程一个With函数,从协程就引用主协程的ctx 也不嵌套,就全局唯一一个ctx,如同第一个例子。
其结构图如下:
在这里插入图片描述

WithValue

WithValue 基于 valueCtx结构体,主要用来父context给子context传递数据,也就是说子context可以获得其父context的内容 反之则不行。
一个例子:

func doSomthing(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println("playing")
			return
		default:
			time.Sleep(1 * time.Second)
			fmt.Println("I am working")
			fmt.Println(ctx.Value("key"))
		}

	}

}

func TestWithCancel(t *testing.T) {
	var wg sync.WaitGroup
	ctx:=context.WithValue(context.Background(),"key","value")
	ctx, concel := context.WithCancel(ctx)
	wg.Add(1)
	go doSomthing(ctx, &wg)
	time.Sleep(3 * time.Second)
	concel()
	wg.Wait()
}

运行结果如下:

I am working
value
I am working
value
I am working
value
playing

可以看到 主协程传递的是 key value 可以在子协程通过 Value(key)函数来获取value。通过valuetx嵌入cancelCtx可以得到一颗树形结构,如上图cancelCtx形成的多节点树形结构,由于valueCtx 可以嵌入 cancelCtx所以 可以加入其树形链条中。

其源码如下:

func WithValue(parent Context, key, val any) 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")
	}
	return &valueCtx{parent, key, val}
}

源码比较简单,valueCtx本身一般不会用来传播,一般都是嵌入其他的结构体 像cancelCtx,timerCtx 和其他的context,主要用来传递参数。子可以看所有的父参数,父不能看子,因为查找是递归查找。
valueCtx的主要功能是传递参数和查找参数,传递用的是 key,value。查找用的是Value函数 我们来看下Value函数。


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

// 用于在整个context链条(带回溯的树结构)上 寻找 匹配的值 是一个公共方法
func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val // 匹配上key 返回
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key) // 回溯 递归查找
		}
	}
}

通过 上述代码 我们可以看到 其之所以可以共享参数 主要是由于有递归查找的原因 所以 子可以查找父传递的参数 但是反之不行。
另一个用处是在http包内 传递参数,感兴趣的可以查阅相关资料。
人为取消是不是有时候不爽啊,那就加入定时功能吧,所以 下面的几个函数本质上就是在cancelCtx功能的扩展,我们看其结构体timerCtx就包含了cancelCtx,只不过在其基础上加了时间维度。下面来看看涉及的几个结构体。

WithDeadlineCause

WithDeadlineCause 是time相关功能的基础函数,WithDeadline和 WithTimeout都是基于这个函数。我们来看下其源码:


// 定时取消操作
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	
	// 获取父context的取消时间,如果比传入时间早 就执行WithCancel取消逻辑
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	
	// 否则 就新建一个新timerCtx 并把取消时间赋给deadline
	c := &timerCtx{
		deadline: d,
	}
	// 构建 context树形结构 将c, parent父子化
	c.cancelCtx.propagateCancel(parent, c)
	// 开始定时 直到达到取消时间 执行取消操作
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
		return c, func() { c.cancel(false, Canceled, nil) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()

	// dur时间过后 开始执行取消
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded, cause)
		})
	}
	// todo
	return c, func() { c.cancel(true, Canceled, nil) }
}

这个结构体不怎么使用 我们一般使用它两个变种。如下

WithDeadline

变种自WithDeadlineCause
源码如下:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	return WithDeadlineCause(parent, d, nil)
}

只不过去掉了取消原因 因为是定时取消 原因不用说了
我们看一个例子

func doWork(ctx context.Context,wg * sync.WaitGroup) {
	defer wg.Done()
	select {
	case <-time.After(3 * time.Second):
		// 模拟操作需要3秒完成
		fmt.Println("操作完成")
	case <-ctx.Done():
		// 当上下文取消时会进入这个分支
		fmt.Println("操作被取消:", ctx.Err())
	}
}
func TestWithDeadline(t *testing.T) {
	// 设置截止时间为当前时间后2秒
	deadline := time.Now().Add(2 * time.Second)
	ctx, cancel := context.WithDeadline(context.Background(), deadline)
	defer cancel() // 确保在主函数返回前取消上下文
	// 模拟操作

	var wg sync.WaitGroup
	wg.Add(1)
	go doWork(ctx,&wg)
	wg.Wait()
}

运行结果如下:

操作被取消: context deadline exceeded

比较简单 我们来看最后一个

WithTimeout

其源码如下:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout)) 
}

我们看到这个函数 只是 TestWithDeadline一个特例,入参是一个时间戳,只不过后续需要转换成当前时间往后时间戳刻度作为定时时间。
所以上面的例子稍微修改下就可以使用

func doWork(ctx context.Context,wg * sync.WaitGroup) {
	defer wg.Done()
	select {
	case <-time.After(3 * time.Second):
		// 模拟操作需要3秒完成
		fmt.Println("操作完成")
	case <-ctx.Done():
		// 当上下文取消时会进入这个分支
		fmt.Println("操作被取消:", ctx.Err())
	}
}
func TestWithTimeout(t *testing.T) {
	// 设置截止时间为当前时间后2秒
 	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel() // 确保在主函数返回前取消上下文
	// 模拟操作

	var wg sync.WaitGroup
	wg.Add(1)
	go doWork(ctx,&wg)
	wg.Wait()
}

总结

如此 Context的所有变体和相关函数都介绍完毕了。我们来总结下。

  1. Context接口是内核,cancelCtx内嵌了Context并实现了其部分函数,来执行一些取消操作,而timerCtx内嵌了 cancelCtx从而可以定时取消。
  2. valueCtx内嵌了Context,其本身还有 键值对参数,从而可以用来传递参数。其之所以不内嵌cancelCtx主要是为了提供一个基础组合结构体,用户就可以内嵌各种context变体来传递参数,使用范围更广,而不只是限制在cancel变体。
  3. With函数入参和返回值都有Context所以各种变体可以进行组合。例如cancelCtx和valueCtx组合既可以控制从协程又可以向从协程传递参数。
  4. With函数只有在子协程中也执行,才能构建Context树,否则With参数传递Contest就是全局唯一一个ctx,因为With函数Context入参是接口,所以实际上传递的是Context类型和其实现结构体的指针,所以单纯传递Context不会产生值复制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1881133.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

功能强大的声音模拟合成软件Togu Audio Line TAL-Mod 1.9.7

Togu Audio Line TAL一个虚拟模拟合成器,具有卓越的声音和几乎无限的调制能力。其特殊的振荡器模型能够创建广泛的声音,从经典的单声道到丰富的立体声引线、效果器和焊盘。路由可以使用虚拟跳线电缆来完成。只需连接调制输出以达到调制的目的。之后,您可以调整调制强度。您不…

dB分贝入门

主要参考资料&#xff1a; dB&#xff08;分贝&#xff09;定义及其应用: https://blog.csdn.net/u014162133/article/details/110388145 目录 dB的应用一、声音的大小二、信号强度三、增益 dB的应用 一、声音的大小 在日常生活中&#xff0c;住宅小区告知牌上面标示噪音要低…

Excel表格转Tex工具推荐

为了制作符合 SCI 论文要求的表格&#xff0c;直接用 LaTeX 编写通常比较复杂。我们可以先在 Excel 中绘制好所需的表格&#xff08;最好加上边框&#xff09;。最近我发现了一个非常好用的 Excel 转 LaTeX 工具&#xff0c;能够让 LaTeX 表格的编写变得非常方便。 工具&#…

数据资产治理的智能化探索:结合云计算、大数据、人工智能等先进技术,探讨数据资产治理的智能化方法,为企业提供可靠、高效的数据资产解决方案,助力企业提升竞争力

一、引言 在信息化时代&#xff0c;数据已成为企业最重要的资产之一。随着云计算、大数据、人工智能等先进技术的飞速发展&#xff0c;数据资产治理面临着前所未有的机遇与挑战。本文旨在探讨如何结合这些先进技术&#xff0c;实现数据资产治理的智能化&#xff0c;为企业提供…

X科网js逆向分析

登录抓包之后发现pwd字眼&#xff0c;直接搜索即可 通过$.md5(pwd)之后得到的加密结果就是我们的pwd参数 他说是md5我们不妨测试一下&#xff1a; 1&#xff09;测试使用$.md5(1)加密数字1 得到c4ca4&#xff0c;说明就是$.md5()&#xff0c;md5加密 2&#xff09;测试$.md5…

神经网络在机器学习中的应用:手写数字识别

机器学习是人工智能的一个分支&#xff0c;它使计算机能够从数据中学习并做出决策或预测。神经网络作为机器学习的核心算法之一&#xff0c;因其强大的非线性拟合能力而广泛应用于各种领域&#xff0c;包括图像识别、自然语言处理和游戏等。本文将介绍如何使用神经网络对MNIST数…

独一无二的设计模式——单例模式(Java实现)

1. 引言 亲爱的读者们&#xff0c;欢迎来到我们的设计模式专题&#xff0c;今天的讲解的设计模式&#xff0c;还是单例模式哦&#xff01;上次讲解的单例模式是基于Python实现&#xff08;独一无二的设计模式——单例模式&#xff08;python实现&#xff09;&#xff09;的&am…

【数据结构】C语言实现二叉树的基本操作——二叉树的层次遍历、求深度、求结点数……

C语言实现二叉树的基本操作 导读一、层次遍历1.1 算法思路1.2 算法实现1.2.1 存储结构的选择1.2.2 函数的三要素1.2.3 函数的实现 1.3 小结 二、求二叉树的深度2.1 层序遍历2.2 分治思想——递归 三、 求二叉树的结点数3.1 求二叉树的结点总数3.1.1 层序遍历3.1.2 分治思想——…

SpringBoot | 使用jwt令牌实现登录认证,使用Md5加密实现注册

对于登录认证中的令牌&#xff0c;其实就是一段字符串&#xff0c;那为什么要那么麻烦去用jwt令牌&#xff1f;其实对于登录这个业务&#xff0c;在平常我们实现这个功能时&#xff0c;可能大部分都是通过比对用户名和密码&#xff0c;只要正确&#xff0c;就登录成功&#xff…

【Python实战因果推断】9_元学习器4

目录 Double/Debiased Machine Learning Double/Debiased Machine Learning Double/Debiased ML 或 R-learner 可以看作是 FrischWaugh-Lovell 定理的改进版。其思路非常简单--在构建结果和治疗残差时使用 ML 模型 结果和干预残差&#xff1a; , 预估&#xff0c;预估 由于 …

Python pdfkit wkhtmltopdf html转换pdf 黑体字体乱码

wkhtmltopdf 黑体在html转换pdf时&#xff0c;黑体乱码&#xff0c;分析可能wkhtmltopdf对黑体字体不太兼容&#xff1b; 1.html内容如下 <html> <head> <meta http-equiv"content-type" content"text/html;charsetutf-8"> </head&…

springboot使用测试类报空指针异常

检查了Service注解&#xff0c;还有Autowired注解&#xff0c;还有其他注解&#xff0c;后面放心没能解决问题&#xff0c;最后使用RunWith(SpringRunner.class)解决了问题&#xff01;&#xff01; 真的是✓8了&#xff0c;烦死了这个✓8报错&#xff01;

Android Focused Window的更新

启动App时更新inputInfo/请求焦点窗口流程&#xff1a; App主线程调ViewRootImpl.java的relayoutWindow()&#xff1b;然后调用到Wms的relayoutWindow()&#xff0c;窗口布局流程。焦点窗口的更新&#xff0c;通过WMS#updateFocusedWindowLocked()方法开始&#xff0c;下面从这…

【Spring】DAO 和 Repository 的区别

DAO 和 Repository 的区别 1.概述2.DAO 模式2.1 User2.2 UserDao2.3 UserDaoImpl 3.Repository 模式3.1 UserRepository3.2 UserRepositoryImpl 4.具有多个 DAO 的 Repository 模式4.1 Tweet4.2 TweetDao 和 TweetDaoImpl4.3 增强 User 域4.4 UserRepositoryImpl 5.比较两种模式…

以太网交换机原理

没有配置&#xff0c;比较枯燥&#xff0c;二可以认识线缆&#xff0c; 三比较重要&#xff0c;慢慢理解&#xff0c;事半功倍。 各位老少爷们&#xff0c;在下给大家说段以太网交换机原理&#xff0c;说得不好大家多多包涵&#xff0c;说得好呢&#xff0c;大家叫个好&#x…

【每日一练】python运算符

1. 算术运算符 编写一个Python程序&#xff0c;要求用户输入两个数&#xff0c;并执行以下运算&#xff1a;加法、减法、乘法、求余、除法、以及第一个数的第二个数次方。将结果打印出来。 a input("请输入第一个数&#xff1a;") b input("请输入第二个数&…

诊断知识:UnconfirmedDTCLimit的使用

文章目录 前言UnconfirmedDTCLimit的含义UnconfirmedDTCLimit的使用UnconfirmedDTCLimit和Failed limit相等UnconfirmedDTCLimit小于Failed limit 总结 前言 在某OEM基础技术规范中&#xff0c;诊断需求经常会出现UnconfirmedDTCLimit这个词汇&#xff0c;但基础技术规范中并没…

菲尔兹奖得主测试GPT-4o,经典过河难题未能破解!最强Claude 3.5回答离谱!

目录 01 大言模型能否解决「狼-山羊-卷心菜」经典过河难题&#xff1f; 02 加大难度&#xff1a;100只鸡、1000只鸡如何&#xff1f; 01 大言模型能否解决「狼-山羊-卷心菜」经典过河难题&#xff1f; 最近&#xff0c;菲尔兹奖得主Timothy Gowers分享了他测试GPT-4o的经历&a…

入门JavaWeb之 JDBC 连接数据库

JDBC&#xff1a;Java Database Connectivity&#xff0c;Java 数据库连接 需要 jar 包支持&#xff1a; java.sql javax.sql mysql-connector-java&#xff08;连接驱动&#xff0c;必须导入&#xff09; 在 MySQL 先建个 jdbc 数据库后 USE jdbc; 执行后再 CREATE TABLE…

DIY智能小车:基于STM32的蓝牙遥控实战教程(附代码)

摘要: 本文将带你从零开始&#xff0c;使用STM32单片机打造一辆炫酷的蓝牙遥控小车。文章将详细讲解硬件选型、电路连接、软件设计以及代码实现&#xff0c;并提供丰富的Mermaid图表和代码示例&#xff0c;助你轻松完成自己的遥控小车项目。 关键词: STM32, 蓝牙遥控, 遥控小车…