2022-08-04 06:59:33 +00:00
<!DOCTYPE HTML>
< html lang = "zh" class = "sidebar-visible no-js light" >
< head >
<!-- Book generated using mdBook -->
< meta charset = "UTF-8" >
< title > 匿名函数 - Go语言圣经< / title >
<!-- Custom HTML head -->
< meta content = "text/html; charset=utf-8" http-equiv = "Content-Type" >
< meta name = "description" content = "<The Go Programming Language>中文版" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< meta name = "theme-color" content = "#ffffff" / >
< link rel = "icon" href = "../favicon.svg" >
< link rel = "shortcut icon" href = "../favicon.png" >
< link rel = "stylesheet" href = "../css/variables.css" >
< link rel = "stylesheet" href = "../css/general.css" >
< link rel = "stylesheet" href = "../css/chrome.css" >
< link rel = "stylesheet" href = "../css/print.css" media = "print" >
<!-- Fonts -->
< link rel = "stylesheet" href = "../FontAwesome/css/font-awesome.css" >
< link rel = "stylesheet" href = "../fonts/fonts.css" >
<!-- Highlight.js Stylesheets -->
< link rel = "stylesheet" href = "../highlight.css" >
< link rel = "stylesheet" href = "../tomorrow-night.css" >
< link rel = "stylesheet" href = "../ayu-highlight.css" >
<!-- Custom theme stylesheets -->
< link rel = "stylesheet" href = "../style.css" >
< / head >
< body >
<!-- Provide site root to javascript -->
< script type = "text/javascript" >
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
< / script >
<!-- Work around some values being stored in localStorage wrapped in quotes -->
< script type = "text/javascript" >
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') & & theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') & & sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
< / script >
<!-- Set the theme before any content is loaded, prevents flash -->
< script type = "text/javascript" >
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
< / script >
<!-- Hide / unhide sidebar before it is displayed -->
< script type = "text/javascript" >
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
< / script >
< nav id = "sidebar" class = "sidebar" aria-label = "Table of contents" >
< div class = "sidebar-scrollbox" >
2023-06-26 17:30:52 +00:00
< ol class = "chapter" > < li class = "chapter-item expanded affix " > < a href = "../index.html" > Go语言圣经< / a > < / li > < li class = "chapter-item expanded affix " > < a href = "../preface-zh.html" > 译者序< / a > < / li > < li class = "chapter-item expanded affix " > < a href = "../preface.html" > 前言< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1.html" > < strong aria-hidden = "true" > 1.< / strong > 入门< / a > < / li > < li > < ol class = "section" > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-01.html" > < strong aria-hidden = "true" > 1.1.< / strong > Hello, World< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-02.html" > < strong aria-hidden = "true" > 1.2.< / strong > 命令行参数< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-03.html" > < strong aria-hidden = "true" > 1.3.< / strong > 查找重复的行< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-04.html" > < strong aria-hidden = "true" > 1.4.< / strong > GIF动画< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-05.html" > < strong aria-hidden = "true" > 1.5.< / strong > 获取URL< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-06.html" > < strong aria-hidden = "true" > 1.6.< / strong > 并发获取多个URL< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-07.html" > < strong aria-hidden = "true" > 1.7.< / strong > Web服务< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch1/ch1-08.html" > < strong aria-hidden = "true" > 1.8.< / strong > 本章要点< / a > < / li > < / ol > < / li > < li class = "chapter-item expanded " > < a href = "../ch2/ch2.html" > < strong aria-hidden = "true" > 2.< / strong > 程序结构< / a > < / li > < li > < ol class = "section" > < li class = "chapter-item expanded " > < a href = "../ch2/ch2-01.html" > < strong aria-hidden = "true" > 2.1.< / strong > 命名< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch2/ch2-02.html" > < strong aria-hidden = "true" > 2.2.< / strong > 声明< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch2/ch2-03.html" > < strong aria-hidden = "true" > 2.3.< / strong > 变量< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch2/ch2-04.html" > < strong aria-hidden = "true" > 2.4.< / strong > 赋值< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch2/ch2-05.html" > < strong aria-hidden = "true" > 2.5.< / strong > 类型< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch2/ch2-06.html" > < strong aria-hidden = "true" > 2.6.< / strong > 包和文件< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch2/ch2-07.html" > < strong aria-hidden = "true" > 2.7.< / strong > 作用域< / a > < / li > < / ol > < / li > < li class = "chapter-item expanded " > < a href = "../ch3/ch3.html" > < strong aria-hidden = "true" > 3.< / strong > 基础数据类型< / a > < / li > < li > < ol class = "section" > < li class = "chapter-item expanded " > < a href = "../ch3/ch3-01.html" > < strong aria-hidden = "true" > 3.1.< / strong > 整型< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch3/ch3-02.html" > < strong aria-hidden = "true" > 3.2.< / strong > 浮点数< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch3/ch3-03.html" > < strong aria-hidden = "true" > 3.3.< / strong > 复数< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch3/ch3-04.html" > < strong aria-hidden = "true" > 3.4.< / strong > 布尔型< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch3/ch3-05.html" > < strong aria-hidden = "true" > 3.5.< / strong > 字符串< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch3/ch3-06.html" > < strong aria-hidden = "true" > 3.6.< / strong > 常量< / a > < / li > < / ol > < / li > < li class = "chapter-item expanded " > < a href = "../ch4/ch4.html" > < strong aria-hidden = "true" > 4.< / strong > 复合数据类型< / a > < / li > < li > < ol class = "section" > < li class = "chapter-item expanded " > < a href = "../ch4/ch4-01.html" > < strong aria-hidden = "true" > 4.1.< / strong > 数组< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch4/ch4-02.html" > < strong aria-hidden = "true" > 4.2.< / strong > Slice< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch4/ch4-03.html" > < strong aria-hidden = "true" > 4.3.< / strong > Map< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch4/ch4-04.html" > < strong aria-hidden = "true" > 4.4.< / strong > 结构体< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch4/ch4-05.html" > < strong aria-hidden = "true" > 4.5.< / strong > JSON< /
2022-08-04 06:59:33 +00:00
< div id = "sidebar-resize-handle" class = "sidebar-resize-handle" > < / div >
< / nav >
< div id = "page-wrapper" class = "page-wrapper" >
< div class = "page" >
< div id = "menu-bar-hover-placeholder" > < / div >
< div id = "menu-bar" class = "menu-bar sticky bordered" >
< div class = "left-buttons" >
< button id = "sidebar-toggle" class = "icon-button" type = "button" title = "Toggle Table of Contents" aria-label = "Toggle Table of Contents" aria-controls = "sidebar" >
< i class = "fa fa-bars" > < / i >
< / button >
< button id = "theme-toggle" class = "icon-button" type = "button" title = "Change theme" aria-label = "Change theme" aria-haspopup = "true" aria-expanded = "false" aria-controls = "theme-list" >
< i class = "fa fa-paint-brush" > < / i >
< / button >
< ul id = "theme-list" class = "theme-popup" aria-label = "Themes" role = "menu" >
< li role = "none" > < button role = "menuitem" class = "theme" id = "light" > Light (default)< / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "rust" > Rust< / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "coal" > Coal< / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "navy" > Navy< / button > < / li >
< li role = "none" > < button role = "menuitem" class = "theme" id = "ayu" > Ayu< / button > < / li >
< / ul >
< button id = "search-toggle" class = "icon-button" type = "button" title = "Search. (Shortkey: s)" aria-label = "Toggle Searchbar" aria-expanded = "false" aria-keyshortcuts = "S" aria-controls = "searchbar" >
< i class = "fa fa-search" > < / i >
< / button >
< / div >
< h1 class = "menu-title" > Go语言圣经< / h1 >
< div class = "right-buttons" >
< a href = "../print.html" title = "Print this book" aria-label = "Print this book" >
< i id = "print-button" class = "fa fa-print" > < / i >
< / a >
< a href = "https://github.com/gopl-zh/gopl-zh.github.com" title = "Git repository" aria-label = "Git repository" >
< i id = "git-repository-button" class = "fa fa-github" > < / i >
< / a >
< a href = "https://github.com/gopl-zh/gopl-zh.github.com/edit/master/./ch5/ch5-06.md" title = "Suggest an edit" aria-label = "Suggest an edit" >
< i id = "git-edit-button" class = "fa fa-edit" > < / i >
< / a >
< / div >
< / div >
< div id = "search-wrapper" class = "hidden" >
< form id = "searchbar-outer" class = "searchbar-outer" >
< input type = "search" id = "searchbar" name = "searchbar" placeholder = "Search this book ..." aria-controls = "searchresults-outer" aria-describedby = "searchresults-header" >
< / form >
< div id = "searchresults-outer" class = "searchresults-outer hidden" >
< div id = "searchresults-header" class = "searchresults-header" > < / div >
< ul id = "searchresults" >
< / ul >
< / div >
< / div >
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
< script type = "text/javascript" >
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
< / script >
< div id = "content" class = "content" >
<!-- Page table of contents -->
< div class = "sidetoc" > < nav class = "pagetoc" > < / nav > < / div >
< main >
<!-- 头部 -->
< ul dir = "auto" >
2024-06-12 07:20:07 +00:00
< li > < em > Go语言圣经读书笔记(挖坑中...): < a href = "https://github.com/chai2010/gopl-notes-zh" > https://github.com/chai2010/gopl-notes-zh< / a > < / em > < / li >
< li > < em > 凹语言(Go实现, 面向WASM设计): < a href = "https://github.com/wa-lang/wa" > https://github.com/wa-lang/wa< / a > < / em > < / li >
2023-06-26 17:46:27 +00:00
< / ul >
2022-08-04 06:59:33 +00:00
< hr >
< h2 id = "56-匿名函数" > < a class = "header" href = "#56-匿名函数" > 5.6. 匿名函数< / a > < / h2 >
< p > 拥有函数名的函数只能在包级语法块中被声明, 通过函数字面量( function literal) , 我们可绕过这一限制, 在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似, 区别在于func关键字后没有函数名。函数值字面量是一种表达式, 它的值被称为匿名函数( anonymous function) 。< / p >
< p > 函数字面量允许我们在使用函数时, 再定义它。通过这种技巧, 我们可以改写之前对strings.Map的调用: < / p >
< pre > < code class = "language-Go" > strings.Map(func(r rune) rune { return r + 1 }, " HAL-9000" )
< / code > < / pre >
< p > 更为重要的是, 通过这种方式定义的函数可以访问完整的词法环境( lexical environment) , 这意味着在函数中定义的内部函数可以引用该函数的变量, 如下例所示: < / p >
< p > < u > < i > gopl.io/ch5/squares< / i > < / u > < / p >
< pre > < code class = "language-Go" > // squares返回一个匿名函数。
2024-04-03 08:10:38 +00:00
// 该匿名函数每次被调用时都会返回下一个数的平方。
2022-08-04 06:59:33 +00:00
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
2024-04-03 08:10:38 +00:00
fmt.Println(f()) // " 1"
fmt.Println(f()) // " 4"
fmt.Println(f()) // " 9"
fmt.Println(f()) // " 16"
2022-08-04 06:59:33 +00:00
}
< / code > < / pre >
< p > 函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用匿名函数时, 该函数都会先使x的值加1, 再返回x的平方。第二次调用squares时, 会生成第二个x变量, 并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。< / p >
< p > squares的例子证明, 函数值不仅仅是一串代码, 还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量, 这意味着匿名函数和squares中, 存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包( closures) 技术实现函数值, Go程序员也把函数值叫做闭包。< / p >
< p > 通过这个例子, 我们看到变量的生命周期不由它的作用域决定: squares返回后, 变量x仍然隐式的存在于f中。< / p >
< p > 接下来,我们讨论一个有点学术性的例子,考虑这样一个问题:给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成。每个课程的前置课程如下:< / p >
< p > < u > < i > gopl.io/ch5/toposort< / i > < / u > < / p >
< pre > < code class = "language-Go" > // prereqs记录了每个课程的前置课程
var prereqs = map[string][]string{
" algorithms" : {" data structures" },
" calculus" : {" linear algebra" },
" compilers" : {
" data structures" ,
" formal languages" ,
" computer organization" ,
},
" data structures" : {" discrete math" },
" databases" : {" data structures" },
" discrete math" : {" intro to programming" },
" formal languages" : {" discrete math" },
" networks" : {" operating systems" },
" operating systems" : {" data structures" , " computer organization" },
" programming languages" : {" data structures" , " computer organization" },
}
< / code > < / pre >
< p > 这类问题被称作拓扑排序。从概念上说,前置条件可以构成有向图。图中的顶点表示课程,边表示课程间的依赖关系。显然,图中应该无环,这也就是说从某点出发的边,最终不会回到该点。下面的代码用深度优先搜索了整张图,获得了符合要求的课程序列。< / p >
< pre > < code class = "language-Go" > func main() {
for i, course := range topoSort(prereqs) {
fmt.Printf(" %d:\t%s\n" , i+1, course)
}
}
func topoSort(m map[string][]string) []string {
var order []string
seen := make(map[string]bool)
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
visitAll(keys)
return order
}
< / code > < / pre >
< p > 当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll) , 再将匿名函数赋值给这个变量。如果不分成两步, 函数字面量无法与visitAll绑定, 我们也无法递归调用该匿名函数。< / p >
< pre > < code class = "language-Go" > visitAll := func(items []string) {
// ...
visitAll(m[item]) // compile error: undefined: visitAll
// ...
}
< / code > < / pre >
< p > 在toposort程序的输出如下所示, 它的输出顺序是大多人想看到的固定顺序输出, 但是这需要我们多花点心思才能做到。哈希表prepreqs的value是遍历顺序固定的切片, 而不再试遍历顺序随机的map, 所以我们对prereqs的key值进行排序, 保证每次运行toposort程序, 都以相同的遍历顺序遍历prereqs。< / p >
< pre > < code > 1: intro to programming
2: discrete math
3: data structures
4: algorithms
5: linear algebra
6: calculus
7: formal languages
8: computer organization
9: compilers
10: databases
11: operating systems
12: networks
13: programming languages
< / code > < / pre >
< p > 让我们回到findLinks这个例子。我们将代码移动到了links包下, 将函数重命名为Extract, 在第八章我们会再次用到这个函数。新的匿名函数被引入, 用于替换原来的visit函数。该匿名函数负责将新连接添加到切片中。在Extract中, 使用forEachNode遍历HTML页面, 由于Extract只需要在遍历结点前操作结点, 所以forEachNode的post参数被传入nil。< / p >
< p > < u > < i > gopl.io/ch5/links< / i > < / u > < / p >
< pre > < code class = "language-Go" > // Package links provides a link-extraction function.
package links
import (
" fmt"
" net/http"
" golang.org/x/net/html"
)
// Extract makes an HTTP GET request to the specified URL, parses
// the response as HTML, and returns the links in the HTML document.
func Extract(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf(" getting %s: %s" , url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf(" parsing %s as HTML: %v" , url, err)
}
var links []string
visitNode := func(n *html.Node) {
if n.Type == html.ElementNode & & n.Data == " a" {
for _, a := range n.Attr {
if a.Key != " href" {
continue
}
link, err := resp.Request.URL.Parse(a.Val)
if err != nil {
continue // ignore bad URLs
}
links = append(links, link.String())
}
}
}
forEachNode(doc, visitNode, nil)
return links, nil
}
< / code > < / pre >
< p > 上面的代码对之前的版本做了改进, 现在links中存储的不是href属性的原始值, 而是通过resp.Request.URL解析后的值。解析后, 这些连接以绝对路径的形式存在, 可以直接被http.Get访问。< / p >
< p > 网页抓取的核心问题就是如何遍历图。在topoSort的例子中, 已经展示了深度优先遍历, 在网页抓取中, 我们会展示如何用广度优先遍历图。在第8章, 我们会介绍如何将深度优先和广度优先结合使用。< / p >
< p > 下面的函数实现了广度优先算法。调用者需要输入一个初始的待访问列表和一个函数f。待访问列表中的每个元素被定义为string类型。广度优先算法会为每个元素调用一次f。每次f执行完毕后, 会返回一组待访问元素。这些元素会被加入到待访问列表中。当待访问列表中的所有元素都被访问后, breadthFirst函数运行结束。为了避免同一个元素被访问两次, 代码中维护了一个map。< / p >
< p > < u > < i > gopl.io/ch5/findlinks3< / i > < / u > < / p >
< pre > < code class = "language-Go" > // breadthFirst calls f for each item in the worklist.
// Any items returned by f are added to the worklist.
// f is called at most once for each item.
func breadthFirst(f func(item string) []string, worklist []string) {
seen := make(map[string]bool)
for len(worklist) > 0 {
items := worklist
worklist = nil
for _, item := range items {
if !seen[item] {
seen[item] = true
worklist = append(worklist, f(item)...)
}
}
}
}
< / code > < / pre >
< p > 就像我们在章节3解释的那样, append的参数“f(item)...”, 会将f返回的一组元素一个个添加到worklist中。< / p >
< p > 在我们网页抓取器中, 元素的类型是url。crawl函数会将URL输出, 提取其中的新链接, 并将这些新链接返回。我们会将crawl作为参数传递给breadthFirst。< / p >
< pre > < code class = "language-go" > func crawl(url string) []string {
fmt.Println(url)
list, err := links.Extract(url)
if err != nil {
log.Print(err)
}
return list
}
< / code > < / pre >
< p > 为了使抓取器开始运行, 我们用命令行输入的参数作为初始的待访问url。< / p >
< pre > < code class = "language-Go" > func main() {
// Crawl the web breadth-first,
// starting from the command-line arguments.
breadthFirst(crawl, os.Args[1:])
}
< / code > < / pre >
< p > 让我们从 https://golang.org 开始,下面是程序的输出结果:< / p >
< pre > < code > $ go build gopl.io/ch5/findlinks3
$ ./findlinks3 https://golang.org
https://golang.org/
https://golang.org/doc/
https://golang.org/pkg/
https://golang.org/project/
https://code.google.com/p/go-tour/
https://golang.org/doc/code.html
https://www.youtube.com/watch?v=XCsL89YtqCs
http://research.swtch.com/gotour
< / code > < / pre >
< p > 当所有发现的链接都已经被访问或电脑的内存耗尽时,程序运行结束。< / p >
< p > < strong > 练习5.10: < / strong > 重写topoSort函数, 用map代替切片并移除对key的排序代码。验证结果的正确性( 结果不唯一) 。< / p >
< p > < strong > 练习5.11: < / strong > 现在线性代数的老师把微积分设为了前置课程。完善topSort, 使其能检测有向图中的环。< / p >
< p > < strong > 练习5.12: < / strong > gopl.io/ch5/outline2( 5.5节) 的startElement和endElement共用了全局变量depth, 将它们修改为匿名函数, 使其共享outline中的局部变量。< / p >
< p > < strong > 练习5.13: < / strong > 修改crawl, 使其能保存发现的页面, 必要时, 可以创建目录来保存这些页面。只保存来自原始域名下的页面。假设初始页面在golang.org下, 就不要保存vimeo.com下的页面。< / p >
< p > < strong > 练习5.14: < / strong > 使用breadthFirst遍历其他数据结构。比如, topoSort例子中的课程依赖关系( 有向图) 、个人计算机的文件层次结构( 树) ; 你所在城市的公交或地铁线路( 无向图) 。< / p >
< h3 id = "561-警告捕获迭代变量" > < a class = "header" href = "#561-警告捕获迭代变量" > 5.6.1. 警告:捕获迭代变量< / a > < / h3 >
< p > 本节, 将介绍Go词法作用域的一个陷阱。请务必仔细的阅读, 弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。< / p >
< p > 考虑这样一个问题: 你被要求首先创建一些目录, 再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单, 我们忽略了所有的异常处理。< / p >
< pre > < code class = "language-Go" > var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work…
for _, rmdir := range rmdirs {
rmdir() // clean up
}
< / code > < / pre >
< p > 你可能会感到困惑, 为什么要在循环体中用循环变量d赋值一个新的局部变量, 而不是像下面的代码一样直接使用循环变量dir。需要注意, 下面的代码是错误的。< / p >
< pre > < code class = "language-go" > var rmdirs []func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) // NOTE: incorrect!
})
}
< / code > < / pre >
< p > 问题的原因在于循环变量的作用域。在上面的程序中, for循环语句引入了新的词法块, 循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意, 函数值中记录的是循环变量的内存地址, 而不是循环变量某一时刻的值。以dir为例, 后续的迭代会不断更新dir的值, 当删除操作执行时, for循环已完成, dir中存储的值等于最后一次迭代的值。这意味着, 每次对os.RemoveAll的调用删除的都是相同的目录。< / p >
< p > 通常, 为了解决这个问题, 我们会引入一个与循环变量同名的局部变量, 作为循环变量的副本。比如下面的变量dir, 虽然这看起来很奇怪, 但却很有用。< / p >
< pre > < code class = "language-Go" > for _, dir := range tempDirs() {
dir := dir // declares inner dir, initialized to outer dir
// ...
}
< / code > < / pre >
< p > 这个问题不仅存在基于range的循环, 在下面的例子中, 对循环变量i的使用也存在同样的问题: < / p >
< pre > < code class = "language-Go" > var rmdirs []func()
dirs := tempDirs()
for i := 0; i < len(dirs); i++ {
os.MkdirAll(dirs[i], 0755) // OK
rmdirs = append(rmdirs, func() {
os.RemoveAll(dirs[i]) // NOTE: incorrect!
})
}
< / code > < / pre >
< p > 如果你使用go语句( 第八章) 或者defer语句( 5.8节) 会经常遇到此类问题。这不是go或defer本身导致的, 而是因为它们都会等待循环结束后, 再执行函数值。< / p >
<!-- 公众号 -->
< hr >
< table >
< tr >
< td >
< img width = "222px" src = "https://chai2010.cn/advanced-go-programming-book/css.png" >
< / td >
< td >
< img width = "222px" src = "https://chai2010.cn/advanced-go-programming-book/cch.png" >
< / td >
< / tr >
< / table >
< div id = "giscus-container" > < / div >
< footer class = "page-footer" >
< span > © 2015-2016 | < a href = "https://github.com/gopl-zh" > Go语言圣经中文版< / a > , 仅学习交流使用< / span >
< / footer >
< / main >
< nav class = "nav-wrapper" aria-label = "Page navigation" >
<!-- Mobile navigation buttons -->
< a rel = "prev" href = "../ch5/ch5-05.html" class = "mobile-nav-chapters previous" title = "Previous chapter" aria-label = "Previous chapter" aria-keyshortcuts = "Left" >
< i class = "fa fa-angle-left" > < / i >
< / a >
< a rel = "next" href = "../ch5/ch5-07.html" class = "mobile-nav-chapters next" title = "Next chapter" aria-label = "Next chapter" aria-keyshortcuts = "Right" >
< i class = "fa fa-angle-right" > < / i >
< / a >
< div style = "clear: both" > < / div >
< / nav >
< / div >
< / div >
< nav class = "nav-wide-wrapper" aria-label = "Page navigation" >
< a rel = "prev" href = "../ch5/ch5-05.html" class = "nav-chapters previous" title = "Previous chapter" aria-label = "Previous chapter" aria-keyshortcuts = "Left" >
< i class = "fa fa-angle-left" > < / i >
< / a >
< a rel = "next" href = "../ch5/ch5-07.html" class = "nav-chapters next" title = "Next chapter" aria-label = "Next chapter" aria-keyshortcuts = "Right" >
< i class = "fa fa-angle-right" > < / i >
< / a >
< / nav >
< / div >
< script type = "text/javascript" >
window.playground_copyable = true;
< / script >
< script src = "../elasticlunr.min.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../mark.min.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../searcher.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../clipboard.min.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../highlight.js" type = "text/javascript" charset = "utf-8" > < / script >
< script src = "../book.js" type = "text/javascript" charset = "utf-8" > < / script >
< script type = "text/javascript" charset = "utf-8" >
var pagePath = "ch5/ch5-06.md"
< / script >
<!-- Custom JS scripts -->
< script type = "text/javascript" src = "../js/custom.js" > < / script >
< script type = "text/javascript" src = "../js/bigPicture.js" > < / script >
< / body >
< / html >