April 9, 2021
GC 对根对象扫描实现的源码分析
"\u003ch1 id=\"工作池gcwork\"\u003e工作池gcWork\u003c/h1\u003e\n\u003cp\u003e工作缓存池(\u003ccode\u003ework pool\u003c/code\u003e)实现了生产者和消费者模型,用于指向灰色对象。一个灰色对象在工作队列中被扫描标记,一个黑色对象表示已被标记不在队列中。\u003c/p\u003e\n\u003cp\u003e写屏障、根发现、栈扫描和对象扫描都会生成一个指向灰色对象的指针。扫描消费时会指向这个灰色对象,从而将先其变为黑色,再扫描它们,此时可能会产生一个新的指针指向灰色对象。这个就是三色标记法的基本知识点,应该很好理解。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003egcWork\u003c/code\u003e 是为垃圾回收器提供的一个生产和消费工作接口。\u003c/p\u003e\n\u003cp\u003e它可以用在stack上,如\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e(preemption must be disabled)\ngcw := \u0026amp;getg().m.p.ptr().gcw\n.. call gcw.put() to produce and gcw.tryGet() to consume ..\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e在标记阶段使用gcWork可以防止垃圾收集器转换到标记终止,这一点很重要,因为gcWork可能在本地持有GC工作缓冲区。可以通过禁用抢占(\u003ccode\u003esystemstack\u003c/code\u003e 或 \u003ccode\u003eacquirem\u003c/code\u003e)来实现。\u003c/p\u003e\n\u003cp\u003e数据结构\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003etype gcWork struct {\n\twbuf1, wbuf2 …\u003c/code\u003e\u003c/pre\u003e"
April 7, 2021
Runtime: Golang GC源码分析
"\u003cp\u003e在阅读此文前,需要先了解一下三色标记法以及混合写屏障这些概念。\u003c/p\u003e\n\u003cp\u003e源文件 \u003ccode\u003e[src/runtime/mgc.go](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go)\u003c/code\u003e 版本 1.16.2。\u003c/p\u003e\n\u003ch1 id=\"基本知识\"\u003e基本知识\u003c/h1\u003e\n\u003cp\u003e在介绍GC之前,我们需要认识有些与GC相关的基本信息,如GC的状态、模式、统计信息等。\u003c/p\u003e\n\u003ch2 id=\"三种状态\"\u003e三种状态\u003c/h2\u003e\n\u003cp\u003e共有三种状态\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003econst (\n\t_GCoff = iota // GC not running; sweeping in background, write barrier disabled\n\t_GCmark // GC marking roots and workbufs: allocate black, write barrier ENABLED\n\t_GCmarktermination // GC mark termination: allocate black, P\u0026#39;s help GC, write barrier ENABLED\n) …\u003c/code\u003e\u003c/pre\u003e"
April 6, 2021
Golang中的切片与GC
"\u003cp\u003e今天再看 \u003ccode\u003etimer\u003c/code\u003e 源码的时候,在函数 \u003ccode\u003e[clearDeletedTimers()](https://github.com/golang/go/blob/go1.16.2/src/runtime/time.go#L904-L992)\u003c/code\u003e 里看到一段对切片的处理代码,实现目的就是对一个切片内容进行缩容。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// src/runtime/time.go\n\n// The caller must have locked the timers for pp.\nfunc clearDeletedTimers(pp *p) {\n\ttimers := pp.timers\n\t......\n\t// 对无用的切片元素赋值 nil\n\tfor i := to; i \u0026lt; len(timers); i++ {\n\t\ttimers[i] = nil\n\t}\n\n\tatomic.Xadd(\u0026amp;pp.deletedTimers, -cdel)\n\tatomic.Xadd(\u0026amp;pp.numTimers, -cdel)\n\tatomic.Xadd(\u0026amp;pp.adjustTimers, -cearlier) …\u003c/code\u003e\u003c/pre\u003e"
March 29, 2021
Runtime: Golang 定时器实现原理及源码解析
"\u003cp\u003e定时器作为开发经常使用的一种数据类型,是每个开发者需要掌握的,对于一个高级开发很有必要了解它的实现原理,今天我们runtime源码来学习一下它的底层实现。\u003c/p\u003e\n\u003cp\u003e定时器分两种,分别为 Timer 和 Ticker,两者差不多,这里重点以Timer为例。\u003c/p\u003e\n\u003cp\u003e源文件位于 \u003ccode\u003e[src/time/sleep.go](https://github.com/golang/go/blob/go1.16.2/src/time/sleep.go)\u003c/code\u003e 和 \u003ccode\u003e[src/time/tick.go](https://github.com/golang/go/blob/go1.16.2/src/time/tick.go)\u003c/code\u003e 。 go version 1.16.2\u003c/p\u003e\n\u003ch1 id=\"数据结构\"\u003e数据结构\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003eTimer\u003c/code\u003e 数据结构\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// src/runtime/sleep.go\n\n// The Timer type represents a single event.\n// When the Timer expires, the current time will be sent on C,\n// unless the Timer was created by …\u003c/code\u003e\u003c/pre\u003e"
March 28, 2021
Golang中的CAS原子操作 和 锁
"\u003cp\u003e在高并发编程中,经常会出现对同一个资源并发访问修改的情况,为了保证最终结果的正确性,一般会使用 \u003ccode\u003e锁\u003c/code\u003e 和 \u003ccode\u003eCAS原子操作\u003c/code\u003e 来实现。\u003c/p\u003e\n\u003cp\u003e如要对一个变量进行计数统计,两种实现方式分别为\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epackage main\n\nimport (\n\t\u0026#34;fmt\u0026#34;\n\t\u0026#34;sync\u0026#34;\n)\n\n// 锁实现方式\nfunc main() {\n\tvar count int64\n\tvar wg sync.WaitGroup\n\tvar mu sync.Mutex\n\n\tfor i := 0; i \u0026lt; 10000; i++ {\n\t\twg.Add(1)\n\t\tgo func(wg *sync.WaitGroup) {\n\t\t\tdefer wg.Done()\n\t\t\tmu.Lock()\n\t\t\tcount = count + 1\n\t\t\tmu.Unlock()\n\t\t}(\u0026amp;wg)\n\t}\n\twg.Wait()\n\n\t// count = 10000\n\tfmt.Println(\u0026#34;count = \u0026#34;, count)\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e与\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epackage main\n\nimport ( …\u003c/code\u003e\u003c/pre\u003e"
March 23, 2021
Golang并发同步原语之-信号量Semaphore
"\u003cp\u003e信号量是并发编程中比较常见的一种同步机制,它会保持资源计数器一直在\u003ccode\u003e0-N\u003c/code\u003e(\u003ccode\u003eN\u003c/code\u003e表示权重值大小,在用户初始化时指定)之间。当用户获取的时候会减少一点,使用完毕后再恢复过来。当遇到请求时资源不够的情况下,将会进入休眠状态以等待其它进程释放资源。\u003c/p\u003e\n\u003cp\u003e在 Golang 官方扩展库中为我们提供了一个基于权重的信号量 \u003ccode\u003e[semaphore](https://github.com/golang/sync/blob/master/semaphore/semaphore.go)\u003c/code\u003e 并发原语。\u003c/p\u003e\n\u003cp\u003e你可以将下面的参数 \u003ccode\u003en\u003c/code\u003e 理解为资源权重总和,表示每次获取时的权重;也可以理解为资源数量,表示每次获取时必须一次性获取的资源数量。为了理解方便,这里直接将其理解为资源数量。\u003c/p\u003e\n\u003ch2 id=\"数据结构\"\u003e数据结构\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e[semaphoreWeighted](https://github.com/golang/sync/blob/master/semaphore/semaphore.go#L19-L33)\u003c/code\u003e 结构体\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003etype waiter struct {\n\tn int64\n\tready chan\u0026lt;- struct{} // Closed …\u003c/code\u003e\u003c/pre\u003e"
March 22, 2021
学习Golang GC 必知的几个知识点
"\u003cp\u003e对于gc的介绍主要位于 \u003ccode\u003e[src/runtime/mgc.go](https://github.com/golang/go/blob/go1.16.2/src/runtime/mgc.go)\u003c/code\u003e,以下内容是对注释的翻译。\u003c/p\u003e\n\u003ch2 id=\"gc-四个阶段\"\u003eGC 四个阶段\u003c/h2\u003e\n\u003cp\u003e通过源文件注释得知GC共分四个阶段:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eGC 清理终止 (\u003ccode\u003eGC performs sweep termination\u003c/code\u003e)\na. \u003ccode\u003eStop the world\u003c/code\u003e, 每个P 进入GC \u003ccode\u003esafepoint\u003c/code\u003e(安全点),从此刻开始,万物静止。\nb. 清理未被清理的span,如果GC被强制执行时才会出现这些未清理的span\u003c/li\u003e\n\u003cli\u003eGC 标记阶段(\u003ccode\u003eGC performs the mark phase\u003c/code\u003e)\na. 将gc标记从 \u003ccode\u003e_GCoff\u003c/code\u003e 修改为 \u003ccode\u003e_GCmark\u003c/code\u003e,开启写屏障(\u003ccode\u003ewrite barries\u003c/code\u003e)和 协助助手(\u003ccode\u003emutator assists\u003c/code\u003e),将根对象放入队列。 在STW期间,在所有P都启用写屏障之前不会有什么对象被扫描。\nb. \u003ccode\u003eStart the world\u003c/code\u003e(恢复STW)。标记工作线程和协助助手并发的执行。对于任何指针的写操作和指针值,都会被写屏障覆盖,使新分配的对象标记为黑 …\u003c/li\u003e\u003c/ol\u003e"
March 20, 2021
Runtime: Golang 之 sync.Pool 源码分析
"\u003cp\u003ePool 指一组可以单独保存和恢复的 \u003ccode\u003e临时对象\u003c/code\u003e。Pool 中的对象随时都有可能在没有收到任何通知的情况下被GC自动销毁移除。\u003c/p\u003e\n\u003cp\u003e多个goroutine同时操作Pool是\u003ccode\u003e并发安全\u003c/code\u003e的。\u003c/p\u003e\n\u003cp\u003e源文件为 \u003ccode\u003e[src/sync/pool.go](https://github.com/golang/go/blob/master/src/sync/pool.go)\u003c/code\u003e go version: 1.16.2\u003c/p\u003e\n\u003ch1 id=\"为什么使用pool\"\u003e为什么使用Pool\u003c/h1\u003e\n\u003cp\u003e在开发高性能应用时,经常会有一些完全相同的对象需要频繁的创建和销毁,每次创建都需要在堆中分配对象,等使用完毕后,这些对象需要等待GC回收。我们知道在Golang中使用三色标记法进行垃圾回收的,在回收期间会有一个短暂\u003ccode\u003eSTW\u003c/code\u003e(stop the world)的时间段,这样就会导致程序性能下降。\u003c/p\u003e\n\u003cp\u003e那么能否实现类似数据库连接池这种效果,用来避免对象的频繁创建和销毁,达到尽可能的资源复用呢?为了实现这种需求,标准库中有了\u003ccode\u003esync.Pool\u003c/code\u003e 这个数据结构。看名字很知道它是一个池。但是它和我们想象中的数据库连接池还是有些差别的。对于数据库连接池这种资源只要不手动释放就可以一直利用, …\u003c/p\u003e"
March 19, 2021
Runtime: Golang同步原语Mutex源码分析
"\u003cp\u003e在 \u003ccode\u003esync\u003c/code\u003e 包里提供了最基本的同步原语,如互斥锁 \u003ccode\u003eMutex\u003c/code\u003e。除 \u003ccode\u003eOnce\u003c/code\u003e 和 \u003ccode\u003eWaitGroup\u003c/code\u003e 类型外,大部分是由低级库提供的,更高级别的同步最好是通过 \u003ccode\u003echannel\u003c/code\u003e 通讯来实现。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eMutex\u003c/code\u003e 类型的变量默认值是未加锁状态,在第一次使用后,此值将\u003ccode\u003e不得\u003c/code\u003e复制,这点切记!!!\u003c/p\u003e\n\u003cp\u003e本文基于go version: 1.16.2\u003c/p\u003e\n\u003cp\u003eMutex 锁实现了 \u003ccode\u003eLocker\u003c/code\u003e 接口。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// A Locker represents an object that can be locked and unlocked.\ntype Locker interface {\n\tLock()\n\tUnlock()\n}\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"锁的模式\"\u003e锁的模式\u003c/h2\u003e\n\u003cp\u003e为了互斥公平性,Mutex 分为 \u003ccode\u003e正常模式\u003c/code\u003e 和 \u003ccode\u003e饥饿模式\u003c/code\u003e 两种。\u003c/p\u003e\n\u003ch3 id=\"正常模式\"\u003e正常模式\u003c/h3\u003e\n\u003cp\u003e在正常模式下,等待者 \u003ccode\u003ewaiter\u003c/code\u003e 会进入到一个\u003ccode\u003eFIFO\u003c/code\u003e队列,在获取锁时\u003ccode\u003ewaiter\u003c/code\u003e会按照先进先出的顺序获取。当唤醒一个\u003ccode\u003ewaiter\u003c/code\u003e 时它被并不会立即获取锁,而是要与\u003ccode\u003e新来的goroutine\u003c/code\u003e竞争,这种情况下新来的goroutine比较有优势,主要是因为它已经运行在CPU,可能它的数量还不少,所以\u003ccode\u003ewaiter\u003c/code\u003e大概率下获取不到 …\u003c/p\u003e"
March 5, 2021
Golang什么时候会触发GC
"\u003cp\u003eGolang采用了三色标记法来进行垃圾回收,那么在什么场景下会触发这个GC动作呢?\u003c/p\u003e\n\u003cp\u003e源码主要位于文件 \u003ccode\u003e[src/runtime/mgc.go](https://github.com/golang/go/blob/go1.16/src/runtime/mgc.go)\u003c/code\u003e go version 1.16\u003c/p\u003e\n\u003cp\u003e触发条件从大方面来说,分为 \u003ccode\u003e手动触发\u003c/code\u003e 和 \u003ccode\u003e系统触发\u003c/code\u003e 两种方式。手动触发一般很少用,主要通过开发者调用 \u003ccode\u003eruntime.GC()\u003c/code\u003e 函数来实现,而对于系统自动触发是 \u003ccode\u003e运行时\u003c/code\u003e 根据一些条件自行维护的,这也正是本文要介绍的内容。\u003c/p\u003e\n\u003cp\u003e不管哪种触发方式,底层回收机制是一样的,所以我们先看一下手动触发,看看能否根据它来找GC触发所需的条件。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// src/runtime/mgc.go\n\n// GC runs a garbage collection and blocks the caller until the\n// garbage collection is complete. It may also block the entire\n// program.\nfunc GC() {\n\tn := …\u003c/code\u003e\u003c/pre\u003e"
March 4, 2021
Golang 基于信号的异步抢占与处理
"\u003cp\u003e在Go1.14版本开始实现了 \u003ccode\u003e基于信号的协程抢占调度\u003c/code\u003e 模式,在此版本以前执行以下代码是永远也无法执行最后一条println语句。\u003c/p\u003e\n\u003cp\u003e本文基于go version 1.16\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epackage main\n\nimport (\n \u0026#34;runtime\u0026#34;\n \u0026#34;time\u0026#34;\n)\n\nfunc main() {\n runtime.GOMAXPROCS(1)\n go func() {\n for {\n }\n }()\n\n time.Sleep(time.Millisecond)\n println(\u0026#34;OK\u0026#34;)\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e原因很简单:在main函数里只有一个CPU,从上到下执行到 \u003ccode\u003etime.Sleep()\u003c/code\u003e 函数的时候,会将 \u003ccode\u003emain goroutine\u003c/code\u003e 放入运行队列,出让了P,开始执行匿名函数,但匿名函数是一个for循环,没有任何 \u003ccode\u003eIO\u003c/code\u003e 语句,也就无法引起对 \u003ccode\u003eG\u003c/code\u003e 的调度,所以当前仅有的一个 \u003ccode\u003eP\u003c/code\u003e 永远被其占用,导致无法打印OK。\u003c/p\u003e\n\u003cp\u003e这个问题在1.14版本开始有所改变,主要是因为引入了\u003ccode\u003e基于信号的抢占模式\u003c/code\u003e。在程序启动 …\u003c/p\u003e"
March 1, 2021
Golang 的调度策略之G的窃取
"\u003cp\u003e我们上篇文章( \u003ca href=\"https://blog.haohtml.com/archives/21411\"\u003eGolang 的底层引导流程/启动顺序\u003c/a\u003e)介绍了一个golang程序的启动流程,在文章的最后对于最重要的一点“\u003ccode\u003e调度\u003c/code\u003e“ (函数 \u003ccode\u003e[schedule()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L2607-L2723)\u003c/code\u003e) 并没有展开来讲,今天我们继续从源码来分析一下它的调度机制。\u003c/p\u003e\n\u003cp\u003e在此之前我们要明白golang中的调度主要指的是什么?在 \u003ccode\u003esrc/runtime/proc.go\u003c/code\u003e 文件里有一段注释这样写到\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e// Goroutine scheduler\u003c/p\u003e\n\u003cp\u003e// The scheduler’s job is to distribute ready-to-run goroutines over worker threads.\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e这里指如何找一个已准备好运行的 G 关联到PM 让其执行。对于G 的调度可以围绕三个方面来理解:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e时机:什么时候关联(调度)。对于调度时机一般是指有空闲P的时候都会去找G执行\u003c/li\u003e\n\u003cli\u003e对象:选择哪个G进行调度。这是我们本篇要讲的内容\u003c/li\u003e\n\u003cli\u003e机制:如何调度。\u003ccode\u003eexecute()\u003c/code\u003e 函数\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e理解 …\u003c/p\u003e"
February 27, 2021
Runtime: Golang是如何处理系统调用阻塞的?
"\u003cp\u003e我们知道在Golang中,当一个Goroutine由于执行 \u003ccode\u003e系统调用\u003c/code\u003e 而阻塞时,会将M从GPM中分离出去,然后P再找一个G和M重新执行,避免浪费CPU资源,那么在内部又是如何实现的呢?今天我们还是通过学习Runtime源码的形式来看下他的内部实现细节有哪些?\u003c/p\u003e\n\u003cp\u003ego version 1.15.6\u003c/p\u003e\n\u003cp\u003e我们知道一个P有四种运行状态,而当执行系统调用函数阻塞时,会从 \u003ccode\u003e_Prunning\u003c/code\u003e 状态切换到 \u003ccode\u003e_Psyscall\u003c/code\u003e,等系统调用函数执行完毕后再切换回来。\u003cimg src=\"https://blogstatic.haohtml.com/uploads/2021/01/0d20dfce0e3dd6968aebe84535b853c6.png\" alt=\"P的状态切换\"\u003eP的状态切换\u003c/p\u003e\n\u003cp\u003e从上图我们可以看出 \u003ccode\u003eP\u003c/code\u003e 执行系统调用时会执行 \u003ccode\u003e[entersyscall()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3134-L3142)\u003c/code\u003e 函数(另还有一个类似的阻塞函数 \u003ca href=\"https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3171-L3212\"\u003e\u003ccode\u003eentersyscallblock()\u003c/code\u003e\u003c/a\u003e ,注意两者的区别)。当系统调用执行完毕切换回去会执行 \u003ca href=\"https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3222-L3305\"\u003e\u003ccode\u003eexitsyscall()\u003c/code\u003e\u003c/a\u003e 函数,下面我们看一下这两个函数的实现。\u003c/p\u003e\n\u003ch1 id=\"进入系统调用\"\u003e进入系统调用\u003c/h1\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e// Standard syscall entry used by the go syscall …\u003c/code\u003e\u003c/pre\u003e"
February 26, 2021
Runtime: 当一个goroutine 运行结束后会发生什么
"\u003cp\u003e上一篇我们介绍了 \u003ca href=\"https://blog.haohtml.com/archives/23168\"\u003e创建一个goroutine 会经历些什么\u003c/a\u003e,今天我们再看下当一个\u003ccode\u003egoroutine\u003c/code\u003e 运行结束的时候,又会发生什么?\u003c/p\u003e\n\u003cp\u003ego version 1.15.6。\u003c/p\u003e\n\u003cp\u003e主要源文件为 \u003ccode\u003e[src/runtime/proc.go](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go)\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e当一个\u003ccode\u003egoroutine\u003c/code\u003e 运行结束的时候,默认会执行一个 \u003ccode\u003e[goexit1()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L2941-L2950)\u003c/code\u003e 的函数,这是一个只有八行代码的函数,其中最后以通过 \u003ccode\u003e[mcall()](https://github.com/golang/go/blob/go1.15.6/src/runtime/stubs.go#L34)\u003c/code\u003e 调用 \u003ccode\u003e[goexit0](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L2952-L3011)\u003c/code\u003e 函数结束。因此我们主 …\u003c/p\u003e"
February 17, 2021
Runtime: 创建一个goroutine都经历了什么?
"\u003cp\u003e我们都知道goroutine的在golang中发挥了很大的作用,那么当我们创建一个新的goroutine时,它是怎么一步一步创建的呢?都经历了哪些操作呢?今天我们通过源码来剖析一下创建goroutine都经历了些什么?go version 1.15.6\u003c/p\u003e\n\u003cp\u003e对goroutine最关键的两个函数是 \u003ccode\u003e[newproc()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3535-L3564)\u003c/code\u003e 和 \u003ccode\u003e[newproc1()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L3566-L3674)\u003c/code\u003e,而 \u003ccode\u003enewproc1()\u003c/code\u003e 函数是我们最需要关注的。\u003c/p\u003e\n\u003ch2 id=\"函数-newproc\"\u003e函数 newproc()\u003c/h2\u003e\n\u003cp\u003e我们先看一个简单的创建goroutine的例子,找出来创建它的函数。\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003epackage main\n\nfunc start(a, b, c int64) {\n\t_ = a + b + c\n}\n\nfunc main() {\n\tgo start(7, 2, 5)\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e输出结果: …\u003c/p\u003e"