Go语言的context

news2025/7/23 6:11:23

Golang context 实现原理

本篇文章是基于小徐先生的文章的修改和个人注解,要查看原文可以点击上述的链接查看

目前我这篇文章的go语言版本是1.24.1

context上下文

context被当作第一个参数(官方建议),并且不断的传递下去,基本上一个项目代码到处都是context,但是你们真的知道他有什么作用吗?

接下来就来看看这个golang世界中的典型工具吧

func main()  {
    ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
    defer cancel()
    go Monitor(ctx)

    time.Sleep(20 * time.Second)
}

func Monitor(ctx context.Context)  {
    for {
        fmt.Print("monitor")
    }
}

但是他到底时如何处理并发控制和实现呢?

接下来就来深入看看他的原理和使用

一.context包介绍

context可以用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的,context包定义了上下文类型,可以使用backgroundTODO创建一个上下文,在函数调用链之间传播context,也可以使用WithDeadlineWithTimeoutWithCancelWithValue 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。

目前我们常用的一些库都是支持context的,例如gindatabase/sql等库都是支持context的,这样更方便我们做并发控制了,只要在服务器入口创建一个context上下文,不断透传下去即可。

二.context的使用

2.1 context.Context

他是核心数据结构,看一下它的样子吧:

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

Context 为 interface,定义了四个核心 api:

• Deadline:返回 context 的过期时间

• Done:返回 context 中的 channel

• Err:返回错误

• Value:返回 context 中的对应 key 的值

2.2 标准error

var Canceled = errors.New("context canceled")

var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

• Canceled:context 被 cancel 时会报此错误

• DeadlineExceeded:context 超时时会报此错误

三.类的实现

3.1 emptyCtx

通过看它的源码我们会发现,这个空的上下文其实就是实现了这个context这个接口,对于实现的4个方法都是返回的nil,这就是为什么说他是empty

在之前的版本中他可能是int类型,后来已经被修改了

type emptyCtx struct{}

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
}

• emptyCtx 是一个空的 context

• Deadline 方法会返回一个公元元年时间以及 false 的 flag,标识当前 context 不存在过期时间;

• Done 方法返回一个 nil 值,用户无论往 nil 中写入或者读取数据,均会陷入阻塞;

• Err 方法返回的错误永远为 nil;

• Value 方法返回的 value 同样永远为 nil.

3.1.1 context.Background() & context.TODO()

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

我们在看代码的时候,经常可以看到不是Background就是todo作为上下文的起始,那他们有什么区别呢?

两者的区别

这两个函数其实只是互为别名,没有差别,官方给的定义是:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不确定应该使用哪种上下文时使用;

所以在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。

看一下两者的底层是什么?

两者其实都是一个对context的一个继承

type backgroundCtx struct{ emptyCtx }
type todoCtx struct{ emptyCtx }

func Background() Context {
	return backgroundCtx{}
}

func TODO() Context {
	return todoCtx{}
}

看到这里,你会发现上面的两种方式是创建根context,不具备任何功能,具体实践其实还是要依靠context包提供的With系列函数来进行派生:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

这四个函数都要基于父Context衍生,通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,画个图表示一下:

基于一个父Context可以随意衍生,其实这就是一个Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,每个子节点都依赖于其父节点,例如上图,我们可以基于Context.Background衍生出四个子contextctx1.0-cancelctx2.0-deadlinectx3.0-timeoutctx4.0-withvalue,这四个子context还可以作为父context继续向下衍生,即使其中ctx1.0-cancel 节点取消了,也不影响其他三个父节点分支。

创建context方法和context的衍生方法就这些,关于这些with函数,会在后续的使用中看到

3.1.2 With类函数

  1. WithCancel(parent Context)

用途:创建一个新的上下文和取消函数。当调用取消函数时,所有派生自这个上下文的操作将被通知取消。

应用场景:当一个长时间运行的操作需要能够被取消时。例如,用户在网页中点击“取消”按钮时,相关的数据库或 HTTP 请求应立即停止。

2. WithDeadline(parent Context, d time.Time)

用途:创建一个新的上下文,该上下文在指定的时间点自动取消。

应用场景:在请求处理时设置最大执行时间。例如,调用外部 API 时,如果响应时间超过预期,将自动取消请求,以避免无效的等待。

3. WithTimeout(parent Context, timeout time.Duration)

用途:创建一个新的上下文,它会在指定的持续时间内自动取消。

应用场景:适用于设置操作的超时时间,确保系统不会在某个操作上无休止地等待。常用于网络请求或长时间运行的任务。

4. WithValue(parent Context, key, val interface{})

用途:创建一个新的上下文,并将键值对存储在该上下文中。

应用场景:在处理请求时,将特定的数据(如用户身份信息、RequestID)在处理链中传递,而不需要在每个函数参数中显式传递。

3.2 cancelCtx

接下来看一下第二个实现的类吧,看名字就能看出来他是一个带有取消功能的上下文。

type cancelCtx struct {
	Context

	mu       sync.Mutex            
	done     atomic.Value          
	children map[canceler]struct{} // 这里是一个set{}
	err      error                 
	cause    error                
}

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

// 这里体现了goland的一个编程哲学
// 作为一个父context,它只需要关注子类的这两个方法即可,
// 它的子类可能更有能力,但是与父亲无关,只需要知道他是否还存在即可
// 会通过就近生成interface的方式,把无关的信息都屏蔽掉。
// 也就是谁使用谁声明谁管理

• 继承了一个 context 作为其父 context. 可见,cancelCtx 必然为某个 context 的子 context

• 内置了一把锁,用以协调并发场景下的资源获取;

• done:实际类型为 chan struct{},即用以反映 cancelCtx 生命周期的通道;

• children:一个 set,指向 cancelCtx 的所有子 context;

• err:记录了当前 cancelCtx 的错误. 必然为某个 context 的子 context;

• cause:是在go1.20之后加入的字段,主要作用适用于记录导致context被取消的具体原因

在这里,要加入一些其他的内容----同步调用和异步调用

同步调用,其实就是一种串行的方式,也就是我们平时写的程序,他是一步一步进行下去,类似一条链的形式。

而异步调用则是开辟协程,并且不会阻塞主线程,并且主线程对子协程的感知能力很弱,开辟了多个子协程,就会形成类似树的形式

就会导致,主线程对子协程的管理能力下降,从而致使协程无法回收,最后导致协程泄露的一个问题。

在创建协程方面,我们要知道一点,如果不知道你创建的协程什么时候结束,你就不应该去创建,不应该滥用并发。

如何解决这个协程的控制呢?

那么今天的主角就是cancelCtx了

先说cancelCtx继承了Context,对其方法进行了一个重写,但是并没有对Deadline方法重写,而是直接继承的父类的。Deadline不进行重写是因为他没有过期取消的能力。

func (c *cancelCtx) Value(key any) any {
    // 这里为什么要加入这个判断,在后续会介绍,主要就是用于判断
    // 其自身是否是一个cancelCtx类型,这个cancelCtxKey是一个定值
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

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{})
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

3.2.1 WithCancel取消控制

既然想实现这种父子联动的行为,就轮到了With下的一个函数WithCancel函数,通过这个函数从而得到一个cancelCtx对象,从而实现一个对子协程的一个控制

func main()  {
    ctx,cancel := context.WithCancel(context.Background())
    // 以context.Background()为父,创建得到一个子context和cancel
    go Speak(ctx)
    time.Sleep(10*time.Second)
    cancel()
    time.Sleep(1*time.Second)
}

func Speak(ctx context.Context)  {
    for range time.Tick(time.Second){
        select {
        case <- ctx.Done():
            fmt.Println("我要闭嘴了")
            return
        default:
            fmt.Println("balabalabalabala")
        }
    }
}

来对这个例子做出一个解释:

select就相当于是一个多路复用,进行一个监听的操作,通过这个WithCancel获取上下文和取消函数

当调用这个cancel函数的时候,就会直接通过这个ctx.Done发送这个取消机制,从而实现一个控制的效果

这里的操作也就是我们常说的超时控制了,当然cancel并没有涉及到超时,他是通过调用cancel()才可以实现一个关闭。

这个结构体就是cancel取消函数的结构体,他返回的是一个函数类型,所以调用的时候需要加上()

来看下这个函数的具体流程这里先提前告知一点,如果停止一个cancelCtx,则这个它下面所有的子上下文都将被杀死,具体的操作看propagateCancel函数

type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := withCancel(parent)
    //Canceled是一个error,自定义的错误信息
	return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := &cancelCtx{}
	c.propagateCancel(parent, c)
	return c
}

func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	c.Context = parent
    // 这一步就是说父亲是emptyCtx,他就没有必要取消
    // 所以没有必要大费周折的取消它,直接就返回就行
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}
    
    // 如果父亲被取消了,那儿子也应该直接取消,记录一下取消的错误和原因
	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

    // 如果我的父也是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
	}

    //检查父Context是否支持AfterFunc,也就是一个回调机制
    // 
	if a, ok := parent.(afterFuncer); ok {
		// parent implements an AfterFunc method.
		c.mu.Lock()
		stop := a.AfterFunc(func() {
			child.cancel(false, parent.Err(), Cause(parent))
		})
		c.Context = stopCtx{
			Context: parent,
			stop:    stop,
		}
		c.mu.Unlock()
		return
	}

    // 开启一个守护协程,时刻监听,第一个判断父亲是不是被终止了
    // 如果父亲被终止了,就应该给孩子也砍一刀,让他们也都终止
    // 如果孩子被终止了,那就被终止了,什么也不需要处理,传播具有单向性
	goroutines.Add(1)
	go func() {
		select {
		case <-parent.Done():
			child.cancel(false, parent.Err(), Cause(parent))
		case <-child.Done():
		}
	}()
}

parentCancelCtx用于判断是不是cancelCtx类型

func (c *cancelCtx) Value(key any) any {
    // 这里为什么要加入这个判断,在后续会介绍,主要就是用于判断
    // 其自身是否是一个cancelCtx类型,这个cancelCtxKey是一个定值
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

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
	}
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	return p, true
}

再看一下返回的闭包函数吧

cancelCtx.cancel 方法有三个入参,第一个 removeFromParent 是一个 bool 值,表示当前 context 是否需要从父 context 的 children set 中删除;第二个 err 则是 cancel 后需要展示的错误,第三个则表示导致错误原因。

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	if cause == nil {
		cause = err
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	c.cause = cause
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
        // closedchan 这里是一个全局chan
        // 关闭,从而取消监听,Store就是为了确保原子性和可见性
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err, cause)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
	if s, ok := parent.(stopCtx); ok {
		s.stop()
		return
	}
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}

3.3 timerCtx

接下来看第三个实现的类

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

timerCtx 在 cancelCtx 基础上又做了一层封装,除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间.

这样就有了实现时停的操作,它对Dealine进行了一个重写,其他都是继承的cancelCtx的

Deadline返回的是 deadline time.Time

3.3.1WithTimeout和WithDeadline

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

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

这两个函数他们的参数就可以看出区别,context.WithTimeout是指经过的时间段,而WithDeadline则是指时间点。

这里属于是超时取消。

3.4 valueCtx

type valueCtx struct {
    Context
    key, val any
}

说一下这个valueCtx吧,在不同位置设置的value其实在查找方面是有问题的,比如你在最下面那一层去存放,也就是B下面的子节点存放value,只有A和B才可以访问这个value,C和D是无法访问到的。

3.4.1WithValue

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}
}

通过这个函数来设置value值。

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

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

相关文章

数据库原理 试卷

以下是某高校教学管理系统的毕业论文指导ER图&#xff0c;数据信息&#xff1a;一名教师指导多名学生&#xff0c;一名学生只能选择一名教师&#xff0c;试分析完成以下各题&#xff0c;如用SQL命令完成的&#xff0c;在SQL Server2008验证后把答案写在题目的下方。 图1 毕业论…

【Qt开发】对话框

目录 1&#xff0c;对话框的介绍 2&#xff0c;Qt内置对话框 2-1&#xff0c;消息对话框QMessageBox 2-2&#xff0c;颜色对话框QColorDialog 2-3&#xff0c;文件对话框QFileDialog 2-4&#xff0c;字体对话框QFontDialog 2-5&#xff0c;输入对话框QInputDialog 1&…

2025年渗透测试面试题总结-匿名[校招]攻防研究员(应用安全)(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 匿名[校招]攻防研究员(应用安全) 基础部分 1. HTTP状态码 2. HTTP请求方法及作用 3. 网络分层及协议 OW…

碰一碰发视频系统--基于H5场景开发

#碰一碰发视频# 旨在构建一个基于移动网页&#xff08;H5&#xff09;的视频“碰传”交互系统&#xff0c;提供类似华为/苹果设备 NFC 轻碰分享的便捷体验。其核心技术依赖于移动端可用的近场通信&#xff08;NFC 或 H5 相关 API&#xff09;和可靠的媒体数据传输方案。实现细节…

MagicAnimate 论文解读:引入时间一致性的视频人物动画生成方法

1. 前言/动机 问题&#xff1a;现有动画生成方法缺乏对时间信息的建模&#xff0c;常常出现时间一致性差的问题 描述&#xff1a; 现有的动画生成方法通常采用帧变形&#xff08;frame-warping&#xff09;技术&#xff0c;将参考图像变形以匹配目标动作。尽管这类方法能生成较…

数据结构:递归(Recursion)

目录 示例1&#xff1a;先打印&#xff0c;再递归 示例2&#xff1a;先递归&#xff0c;再打印 递归的两个阶段 递归是如何使用栈内存 复杂度分析 递归中的静态变量 内存结构图解 递归&#xff1a;函数调用自己 必须有判断条件来使递归继续或停止 我们现在通过这两个示…

Cesium快速入门到精通系列教程一:打造第一个Cesium应用

一、打造第一个Cesium应用 1、官方渠道下载Cesium&#xff08;可选择历史版本&#xff09; ​​GitHub Releases页面​​&#xff1a;https://github.com/CesiumGS/cesium/releases 访问 Cesium GitHub Releases&#xff0c;此处列出了所有正式发布的版本。 通过标签&#…

力扣题解106:从中序与后序遍历序列构造二叉树

一、题目内容 题目要求根据二叉树的中序遍历序列和后序遍历序列来重建二叉树。具体来说&#xff0c;我们需要利用中序遍历序列和后序遍历序列的特点&#xff0c;通过递归的方法逐步构建出完整的二叉树。 中序遍历序列的特点是&#xff1a;左子树 -> 根节点 -> 右子树。后…

学习STC51单片机25(芯片为STC89C52RCRC)

每日一言 生活就像弹簧&#xff0c;你弱它就强&#xff0c;你强它就弱&#xff0c;别轻易认输。 ESP8266作为路由器模式&#xff08;AP模式&#xff09;也就是在局域网内可以有服务器的作用 那么我们需要将pc作为设备进行连接ESP的发射出来的WIFE 叫做这个AI啥的 也有可能叫做…

宁夏农业科技:创新引领,赋能现代农业新篇章

在广袤的宁夏大地上&#xff0c;农业科技如同一股强劲的春风&#xff0c;吹拂着每一寸土地&#xff0c;为宁夏的农业发展注入了新的活力与希望。近年来&#xff0c;宁夏农业科技以其独特的创新力和实践力&#xff0c;不断推动着现代农业的转型升级&#xff0c;让这片古老的土地…

Accelerate 2025北亚巡展正式启航!AI智御全球·引领安全新时代

近日&#xff0c;网络安全行业年度盛会Accelerate 2025北亚巡展正式在深圳启航&#xff01;智库专家、产业领袖及Fortinet高管、产品技术团队和300余位行业客户齐聚一堂&#xff0c;围绕“AI智御全球引领安全新时代”主题&#xff0c;共同探讨AI时代网络安全新范式。大会聚焦三…

005学生心理咨询评估系统技术解析:搭建科学心理评估平台

学生心理咨询评估系统技术解析&#xff1a;搭建科学心理评估平台 在心理健康教育日益受重视的当下&#xff0c;学生心理咨询评估系统成为了解学生心理状态的重要工具。该系统涵盖试卷管理、试题管理等核心模块&#xff0c;面向管理员和用户两类角色&#xff0c;通过前台展示与…

贪心算法应用:多重背包启发式问题详解

贪心算法应用&#xff1a;多重背包启发式问题详解 多重背包问题是经典的组合优化问题&#xff0c;也是贪心算法的重要应用场景。本文将全面深入地探讨Java中如何利用贪心算法解决多重背包问题。 多重背包问题定义 **多重背包问题(Multiple Knapsack Problem)**是背包问题的变…

【保姆级教程】PDF批量转图文笔记

如果你有一个PDF文档&#xff0c;然后你想把它发成图文笔记emmm&#xff0c;最好再加个水印&#xff0c;你会怎么做&#xff1f; 其实也不麻烦&#xff0c;打开PDF文档&#xff0c;挨个截图&#xff0c;然后打开PS一张一张图片拖进去&#xff0c;再把水印图片拖进去&#xff0…

数据库系统概论(十一)SQL 集合查询 超详细讲解(附带例题表格对比带你一步步掌握)

数据库系统概论&#xff08;十一&#xff09;SQL 集合查询 超详细讲解&#xff08;附带例题表格对比带你一步步掌握&#xff09; 前言一、什么是集合查询&#xff1f;二、集合操作的三种类型1. 并操作2. 交操作3. 差操作 三、使用集合查询的前提条件四、常见问题与注意事项五、…

clickhouse如何查看操作记录,从日志来查看写入是否成功

背景 插入表数据后&#xff0c;因为原本表中就有数据&#xff0c;一时间没想到怎么查看插入是否成功&#xff0c;因为对数据源没有很多的了解&#xff0c;这时候就想怎么查看下插入是否成功呢&#xff0c;于是就有了以下方法 具体方法 根据操作类型查找&#xff0c;比如inse…

5G-A:开启通信与行业变革的新时代

最近&#xff0c;不少细心的用户发现手机信号标识悄然发生了变化&#xff0c;从熟悉的 “5G” 变成了 “5G-A”。这一小小的改变&#xff0c;却蕴含着通信技术领域的重大升级&#xff0c;预示着一个全新的通信时代正在向我们走来。今天&#xff0c;就让我们深入了解一下 5G-A&a…

TDengine 集群运行监控

简介 为了确保集群稳定运行&#xff0c;TDengine 集成了多种监控指标收集机制&#xff0c;并通过 taosKeeper 进行汇总。taosKeeper 负责接收这些数据&#xff0c;并将其写入一个独立的 TDengine 实例中&#xff0c;该实例可以与被监控的 TDengine 集群保持独立。TDengine 中的…

uniapp路由跳转toolbar页面

需要阅读uview-ui的API文档 注意需要使用type参数设置后才起作用 另外route跳转的页面会覆盖toolbar工具栏 toConternt(aid) {console.log(aid:, aid)this.$u.route({// url: "pages/yzpg/detail",url: "pages/yzappl/index",// url: "pages/ind…

【linux】知识梳理

操作系统的分类 1. 桌⾯操作系统: Windows/macOS/Linux 2. 移动端操作系统: Android(安卓)/iOS(苹果) 3. 服务器操作系统: Linux/Windows Server 4. 嵌⼊式操作系统: Android(底层是 Linux) Liunx介绍 liunx系统:服务器端最常见的操作系统类型 发行版:Centos和Ubuntu 远程连接操…