You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

439 lines
24 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-cn" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="闭包 # C语言中函数名称就是函数的首地址。Go语言中函数名称跟C语言一样函数名指向函数的首地址即函数的入口地址。从前面《 基础篇-函数-一等公民》那一章节我们知道Go 语言中函数是一等公民,它可以绑定变量,作函数参数,做函数返回值,那么它底层是怎么实现的呢?
我们先来了解下 Function Value 这个概念。
Function Value # Go 语言中函数是一等公民函数可以绑定到变量也可以做参数传递以及做函数返回值。Golang把这样的参数、返回值、变量称为Function value。
Go 语言中Function value本质上是一个指针但是其并不直接指向函数的入口地址而是指向的runtime.funcval( runtime/runtime2.go)这个结构体。该结构体中的fn字段存储的是函数的入口地址
type funcval struct { fn uintptr // variable-size, fn-specific data here } 我们以下面这段代码为例来看下Function value是如何使用的:
func A(i int) { i&#43;&#43; fmt.Println(i) } func B() { f1 := A f1(1) } func C() { f2 := A f2(2) } 上面代码中函数A被赋值给变量f1和f2这种情况下编译器会做出优化让f1和f2共用一个funcval结构体该结构体是在编译阶段分配到数据段的只读区域(.rodata)。如下图所示那样f1和f2都指向了该结构体的地址addr2该结构体的fn字段存储了函数A的入口地址addr1
为什么f1和f2需要通过了一个二级指针来获取到真正的函数入口地址而不是直接将f1f2指向函数入口地址addr1。关于这个原因就涉及到Golang中闭包设计与实现了。
闭包 # 闭包(Closure) 通俗点讲就是能够访问外部函数内部变量的函数。像这样能被访问的变量通常被称为捕获变量。
闭包函数指令在编译阶段生成,但因为每个闭包对象都要保存自己捕获的变量,所以要等到执行阶段才创建对应的闭包对象。我们来看下下面闭包的例子:
package main func A() func() int { i := 3 return func() int { return i } } func main() { f1 := A() f2 := A() print(f1()) pirnt(f2()) } 上面代码中当执行main函数时会在其栈帧区间内为局部变量f1和f2分配栈空间当执行第一个A函数时候会在其栈帧空间分配栈空间来存放局部变量i然后在堆上分配一个funcval结构体其地址假定addr2)该结构体的fn字段存储的是A函数内那个闭包函数的入口地址其地址假定为addr1。A函数除了分配一个funcval结构体外还会挨着该结构体分配闭包函数的变量捕获列表该捕获列表里面只有一个变量i。由于捕获列表的存在所以说闭包函数是一个有状态函数。">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#343a40">
<meta name="color-scheme" content="light dark"><meta property="og:title" content="闭包" />
<meta property="og:description" content="闭包 # C语言中函数名称就是函数的首地址。Go语言中函数名称跟C语言一样函数名指向函数的首地址即函数的入口地址。从前面《 基础篇-函数-一等公民》那一章节我们知道Go 语言中函数是一等公民,它可以绑定变量,作函数参数,做函数返回值,那么它底层是怎么实现的呢?
我们先来了解下 Function Value 这个概念。
Function Value # Go 语言中函数是一等公民函数可以绑定到变量也可以做参数传递以及做函数返回值。Golang把这样的参数、返回值、变量称为Function value。
Go 语言中Function value本质上是一个指针但是其并不直接指向函数的入口地址而是指向的runtime.funcval( runtime/runtime2.go)这个结构体。该结构体中的fn字段存储的是函数的入口地址
type funcval struct { fn uintptr // variable-size, fn-specific data here } 我们以下面这段代码为例来看下Function value是如何使用的:
func A(i int) { i&#43;&#43; fmt.Println(i) } func B() { f1 := A f1(1) } func C() { f2 := A f2(2) } 上面代码中函数A被赋值给变量f1和f2这种情况下编译器会做出优化让f1和f2共用一个funcval结构体该结构体是在编译阶段分配到数据段的只读区域(.rodata)。如下图所示那样f1和f2都指向了该结构体的地址addr2该结构体的fn字段存储了函数A的入口地址addr1
为什么f1和f2需要通过了一个二级指针来获取到真正的函数入口地址而不是直接将f1f2指向函数入口地址addr1。关于这个原因就涉及到Golang中闭包设计与实现了。
闭包 # 闭包(Closure) 通俗点讲就是能够访问外部函数内部变量的函数。像这样能被访问的变量通常被称为捕获变量。
闭包函数指令在编译阶段生成,但因为每个闭包对象都要保存自己捕获的变量,所以要等到执行阶段才创建对应的闭包对象。我们来看下下面闭包的例子:
package main func A() func() int { i := 3 return func() int { return i } } func main() { f1 := A() f2 := A() print(f1()) pirnt(f2()) } 上面代码中当执行main函数时会在其栈帧区间内为局部变量f1和f2分配栈空间当执行第一个A函数时候会在其栈帧空间分配栈空间来存放局部变量i然后在堆上分配一个funcval结构体其地址假定addr2)该结构体的fn字段存储的是A函数内那个闭包函数的入口地址其地址假定为addr1。A函数除了分配一个funcval结构体外还会挨着该结构体分配闭包函数的变量捕获列表该捕获列表里面只有一个变量i。由于捕获列表的存在所以说闭包函数是一个有状态函数。" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://go.cyub.vip/function/closure/" /><meta property="article:section" content="function" />
<title>闭包 | 深入Go语言之旅</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/favicon.png" >
<link rel="stylesheet" href="/book.min.f06572240ce28e67eb332ac5cf5d59a696c47ad4c6f700d5842c5ed93dd8ec77.css" integrity="sha256-8GVyJAzijmfrMyrFz11ZppbEetTG9wDVhCxe2T3Y7Hc=" crossorigin="anonymous">
<script defer src="/flexsearch.min.js"></script>
<script defer src="/en.search.min.9749981f5a5252740b10f807556d022e7ea806edbe559c9ba2c2a1a86afdbf64.js" integrity="sha256-l0mYH1pSUnQLEPgHVW0CLn6oBu2&#43;VZybosKhqGr9v2Q=" crossorigin="anonymous"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BQ229RRTTX"></script>
<script>
var doNotTrack = false;
if (!doNotTrack) {
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-BQ229RRTTX', { 'anonymize_ip': false });
}
</script>
<!--
Made with Book Theme
https://github.com/alex-shpak/hugo-book
-->
</head>
<body dir="ltr">
<input type="checkbox" class="hidden toggle" id="menu-control" />
<input type="checkbox" class="hidden toggle" id="toc-control" />
<main class="container flex">
<aside class="book-menu">
<div class="book-menu-content">
<nav>
<h2 class="book-brand">
<a class="flex align-center" href="/"><img src="https://static.cyub.vip/images/202310/golang-480.png" alt="Logo" /><span>深入Go语言之旅</span>
</a>
</h2>
<div class="book-search">
<input type="text" id="book-search-input" placeholder="Search" aria-label="Search" maxlength="64" data-hotkeys="s/" />
<div class="book-search-spinner hidden"></div>
<ul id="book-search-results"></ul>
</div>
<ul>
<li>
<a href="https://www.cyub.vip/" target="_blank" rel="noopener">
个人博客
</a>
</li>
<li>
<a href="https://github.com/cyub" target="_blank" rel="noopener">
Github主页
</a>
</li>
<li>
<a href="https://www.topgoer.cn/?ref=go.cyub.vip" target="_blank" rel="noopener">
地鼠文档
</a>
</li>
</ul>
<ul>
<li>
<p><strong>
<a href="/">深入Go语言之旅</a></strong></p>
</li>
<li>
<p><strong>准备篇</strong></p>
<ul>
<li>
<a href="/compiler/">编译流程</a></li>
<li>
<a href="/analysis-tools/">分析工具</a>
<ul>
<li>
<a href="/analysis-tools/gdb/">GDB</a></li>
<li>
<a href="/analysis-tools/dlv/">Delve</a></li>
<li>
<a href="/analysis-tools/go-buildin-tools/">Go 内置工具</a></li>
</ul>
</li>
<li>
<a href="/go-assembly/">Go汇编</a></li>
</ul>
</li>
<li>
<p><strong>基础篇</strong></p>
<ul>
<li>
<a href="/type/">数据类型与数据结构</a>
<ul>
<li>
<a href="/type/string/">字符串</a></li>
<li>
<a href="/type/array/">数组</a></li>
<li>
<a href="/type/slice/">切片</a></li>
<li>
<a href="/type/nil/">nil</a></li>
<li>
<a href="/type/empty_struct/">空结构体</a></li>
<li>
<a href="/type/pointer/">指针</a></li>
<li>
<a href="/type/map/">映射</a></li>
</ul>
</li>
<li>
<a href="/function/">函数</a>
<ul>
<li>
<a href="/function/first-class/">一等公民</a></li>
<li>
<a href="/function/call-stack/">函数调用栈</a></li>
<li>
<a href="/function/pass-by-value/">值传递</a></li>
<li>
<a href="/function/closure/"class=active>闭包</a></li>
<li>
<a href="/function/method/">方法</a></li>
</ul>
</li>
<li>
<a href="/feature/">语言特性</a>
<ul>
<li>
<a href="/feature/comma-ok/">逗号ok模式</a></li>
<li>
<a href="/feature/for-range/">遍历 - for-range语法</a></li>
<li>
<a href="/feature/defer/">延迟执行 - defer语法</a></li>
<li>
<a href="/feature/select/">通道选择器 - select语法</a></li>
<li>
<a href="/feature/panic-recover/">恐慌与恢复 - panic/recover</a></li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>运行时篇</strong></p>
<ul>
<li>
<a href="/concurrency/">并发编程</a>
<ul>
<li>
<a href="/concurrency/memory-model/">内存模型</a></li>
<li>
<a href="/concurrency/context/">上下文 - context</a></li>
<li>
<a href="/concurrency/channel/">通道 - channel</a></li>
<li>
<a href="/concurrency/atomic/">原子操作 - atomic</a></li>
<li>
<a href="/concurrency/sync-map/">并发Map - sync.Map</a></li>
<li>
<a href="/concurrency/sync-waitgroup/">等待组 - sync.WaitGroup</a></li>
<li>
<a href="/concurrency/sync-once/">一次性操作 - sync.Once</a></li>
<li>
<a href="/concurrency/sync-pool/">缓冲池 - sync.Pool</a></li>
<li>
<a href="/concurrency/sync-cond/">条件变量 - sync.Cond</a></li>
<li>
<a href="/concurrency/sync-mutex/">互斥锁 - sync.Mutex</a></li>
<li>
<a href="/concurrency/sync-rwmutex/">读写锁 - sync.RWMutex</a></li>
</ul>
</li>
<li>
<a href="/gmp/">G-M-P调度机制</a>
<ul>
<li>
<a href="/gmp/gmp-model/">调度机制概述</a></li>
<li>
<a href="/gmp/scheduler/">调度器</a></li>
</ul>
</li>
<li>
<a href="/memory/">内存管理</a>
<ul>
<li>
<a href="/memory/allocator/">内存分配器</a></li>
<li>
<a href="/memory/gc/">GC</a></li>
</ul>
</li>
<li>
<a href="/type-system/">类型系统</a>
<ul>
<li>
<a href="/type-system/type/">类型系统</a></li>
<li>
<a href="/type-system/interface/">接口</a></li>
<li>
<a href="/type-system/reflect/">反射</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
<script>(function(){var e=document.querySelector("aside .book-menu-content");addEventListener("beforeunload",function(){localStorage.setItem("menu.scrollTop",e.scrollTop)}),e.scrollTop=localStorage.getItem("menu.scrollTop")})()</script>
</div>
</aside>
<div class="book-page">
<header class="book-header">
<div class="flex align-center justify-between">
<label for="menu-control">
<img src="/svg/menu.svg" class="book-icon" alt="Menu" />
</label>
<strong>闭包</strong>
<label for="toc-control">
</label>
</div>
</header>
<article class="markdown"><h1 id="闭包">
闭包
<a class="anchor" href="#%e9%97%ad%e5%8c%85">#</a>
</h1>
<p>C语言中函数名称就是函数的首地址。Go语言中函数名称跟C语言一样函数名指向函数的首地址即函数的入口地址。从前面《<strong>
<a href="/function/first-class/">基础篇-函数-一等公民</a></strong>》那一章节我们知道Go 语言中函数是一等公民,它可以绑定变量,作函数参数,做函数返回值,那么它底层是怎么实现的呢?</p>
<p>我们先来了解下 <code>Function Value</code> 这个概念。</p>
<h2 id="function-value">
Function Value
<a class="anchor" href="#function-value">#</a>
</h2>
<p>Go 语言中函数是一等公民函数可以绑定到变量也可以做参数传递以及做函数返回值。Golang把这样的参数、返回值、变量称为<strong>Function value</strong></p>
<p>Go 语言中<strong>Function value</strong>本质上是一个指针,但是其并不直接指向函数的入口地址,而是指向的<code>runtime.funcval</code>(<strong>
<a href="https://github.com/cyub/go-1.14.13/blob/master/src/runtime/runtime2.go#L195-L198">runtime/runtime2.go</a></strong>)这个结构体。该结构体中的fn字段存储的是函数的入口地址</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">funcval</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">fn</span> <span style="color:#66d9ef">uintptr</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// variable-size, fn-specific data here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>我们以下面这段代码为例来看下<strong>Function value</strong>是如何使用的:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">A</span>(<span style="color:#a6e22e">i</span> <span style="color:#66d9ef">int</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">i</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">B</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">f1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">A</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">f1</span>(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">C</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">f2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">A</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">f2</span>(<span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>上面代码中函数A被赋值给变量f1和f2这种情况下编译器会做出优化让f1和f2共用一个funcval结构体该结构体是在编译阶段分配到数据段的只读区域(.rodata)。如下图所示那样f1和f2都指向了该结构体的地址addr2该结构体的fn字段存储了函数A的入口地址addr1</p>
<figure class="text-center"><img src="https://static.cyub.vip/images/202105/fuc_var.png" width="400px"/>
</figure>
<p>为什么f1和f2需要通过了一个二级指针来获取到真正的函数入口地址而不是直接将f1f2指向函数入口地址addr1。关于这个原因就涉及到Golang中闭包设计与实现了。</p>
<h2 id="闭包-1">
闭包
<a class="anchor" href="#%e9%97%ad%e5%8c%85-1">#</a>
</h2>
<p><strong>闭包(Closure)</strong> 通俗点讲就是能够访问外部函数内部变量的函数。像这样能被访问的变量通常被称为捕获变量。</p>
<p>闭包函数指令在编译阶段生成,但因为每个闭包对象都要保存自己捕获的变量,所以要等到执行阶段才创建对应的闭包对象。我们来看下下面闭包的例子:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">A</span>() <span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">i</span>
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">f1</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">A</span>()
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">f2</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">A</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> print(<span style="color:#a6e22e">f1</span>())
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pirnt</span>(<span style="color:#a6e22e">f2</span>())
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>上面代码中当执行main函数时会在其栈帧区间内为局部变量f1和f2分配栈空间当执行第一个A函数时候会在其栈帧空间分配栈空间来存放局部变量i然后在堆上分配一个funcval结构体其地址假定addr2)该结构体的fn字段存储的是A函数内那个闭包函数的入口地址其地址假定为addr1。A函数除了分配一个funcval结构体外还会挨着该结构体分配闭包函数的变量捕获列表该捕获列表里面只有一个变量i。由于捕获列表的存在所以说<strong>闭包函数是一个有状态函数</strong></p>
<p>当A函数执行完毕后其返回值赋值给f1此时f1指向的就是地址addr2。同理下来f2指向地址addr3。f1和f2都能通过funcval取到了闭包函数入口地址但拥有不同的捕获列表。</p>
<p>当执行f1()时候Go 语言会将其对应funcval地址存储到特定寄存器比如amd64平台中使用rax寄存器这样在闭包函数中就可以通过该寄存器取出funcval地址然后通过偏移找到每一个捕获的变量。由此可以看出来<strong>Golang中闭包就是有捕获列表的Function value</strong></p>
<p>根据上面描述,我们画出内存布局图:</p>
<figure class="text-center"><img src="https://static.cyub.vip/images/202105/func_clo.png" width="400px"/>
</figure>
<p>若闭包捕获的变量会发生改变,编译器会智能的将该变量逃逸到堆上,这样外部函数和闭包引用的是同一个变量,此时不再是变量值的拷贝。这也是为什么下面代码总是打印循环的最后面一个值。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">fns</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">func</span>(), <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#ae81ff">5</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">fns</span> = append(<span style="color:#a6e22e">fns</span>, <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span> println(<span style="color:#a6e22e">i</span>)
</span></span><span style="display:flex;"><span> })
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">fn</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">fns</span> { <span style="color:#75715e">// 最后输出5个5而不是01234
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">fn</span>()
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>感兴趣的可以仿造上图,画出上面代码的内存布局图。重点关注闭包函数捕获的不是值拷贝,而是引用一个堆变量。</p>
</article>
<footer class="book-footer">
<div class="flex flex-wrap justify-between">
</div>
<script>(function(){function e(e){const t=window.getSelection(),n=document.createRange();n.selectNodeContents(e),t.removeAllRanges(),t.addRange(n)}document.querySelectorAll("pre code").forEach(t=>{t.addEventListener("click",function(){if(window.getSelection().toString())return;e(t.parentElement),navigator.clipboard&&navigator.clipboard.writeText(t.parentElement.textContent)})})})()</script>
</footer>
<div class="book-comments">
<script src="https://giscus.app/client.js" data-repo="cyub/go-1.14.13"
data-repo-id="MDEwOlJlcG9zaXRvcnkzMzc2ODUyMzQ=" data-category="Announcements"
data-category-id="DIC_kwDOFCCq8s4CZ3BC"
data-mapping="pathname" data-strict="0" data-emit-metadata="0"
data-input-position="bottom" data-reactions-enabled="0"
data-lang="zh-CN" data-theme="preferred_color_scheme" crossorigin="anonymous" async>
</script>
<noscript>Please enable JavaScript to view the comments powered by giscus.</noscript>
</div>
<label for="menu-control" class="hidden book-menu-overlay"></label>
</div>
</main>
</body>
</html>