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 > 示例: 解码S表达式 - 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" >
< 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.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< / a > < / li > < li class = "chapter-item expanded " > < a href = "../ch4/ch4-06.html" > < strong aria-hidd
< 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/./ch12/ch12-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" >
2022-08-24 10:48:15 +00:00
< li > < em > KusonStack一站式可编程配置技术栈(Go): < a href = "https://github.com/KusionStack/kusion" > https://github.com/KusionStack/kusion< / a > < / em > < / li >
< li > < em > KCL 配置编程语言(Rust): < a href = "https://github.com/KusionStack/KCLVM" > https://github.com/KusionStack/KCLVM< / a > < / em > < / li >
< li > < em > 凹语言™: < a href = "https://github.com/wa-lang/wa" > https://github.com/wa-lang/wa< / a > < / em > < / li >
2022-08-04 06:59:33 +00:00
< / ul >
< hr >
< h2 id = "126-示例-解码s表达式" > < a class = "header" href = "#126-示例-解码s表达式" > 12.6. 示例: 解码S表达式< / a > < / h2 >
< p > 标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如, 我们在4.5节中看到的, 要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型( §12.3),我们可以这样做:< / p >
< pre > < code class = "language-Go" > data := []byte{/* ... */}
var movie Movie
err := json.Unmarshal(data, & movie)
< / code > < / pre >
< p > Unmarshal函数使用了反射机制类修改movie变量的每个成员, 根据输入的内容为Movie成员创建对应的map、结构体和slice。< / p >
< p > 现在让我们为S表达式编码实现一个简易的Unmarshal, 类似于前面的json.Unmarshal标准库函数, 对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须提醒一下, 一个健壮的和通用的实现通常需要比例子更多的代码, 为了便于演示我们采用了精简的实现。我们只支持S表达式有限的子集, 同时处理错误的方式也比较粗暴, 代码的目的是为了演示反射的用法, 而不是构造一个实用的S表达式的解码器。< / p >
< p > 词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器scanner的Scan方法将提前扫描和返回下一个记号, 对于rune类型。大多数记号, 比如“(”, 对应一个单一rune可表示的Unicode字符, 但是text/scanner也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用Scan方法将返回这些记号的类型, 接着调用TokenText方法将返回记号对应的文本内容。< / p >
< p > 因为每个解析器可能需要多次使用当前的记号, 但是Scan会一直向前扫描, 所以我们包装了一个lexer扫描器辅助类型, 用于跟踪最近由Scan方法返回的记号。< / p >
< p > < u > < i > gopl.io/ch12/sexpr< / i > < / u > < / p >
< pre > < code class = "language-Go" > type lexer struct {
scan scanner.Scanner
token rune // the current token
}
func (lex *lexer) next() { lex.token = lex.scan.Scan() }
func (lex *lexer) text() string { return lex.scan.TokenText() }
func (lex *lexer) consume(want rune) {
if lex.token != want { // NOTE: Not an example of good error handling.
panic(fmt.Sprintf(" got %q, want %q" , lex.text(), want))
}
lex.next()
}
< / code > < / pre >
< p > 现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数, 用于读取S表达式的当前标记, 然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。< / p >
< pre > < code class = "language-Go" > func read(lex *lexer, v reflect.Value) {
switch lex.token {
case scanner.Ident:
// The only valid identifiers are
// " nil" and struct field names.
if lex.text() == " nil" {
v.Set(reflect.Zero(v.Type()))
lex.next()
return
}
case scanner.String:
s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors
v.SetString(s)
lex.next()
return
case scanner.Int:
i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors
v.SetInt(int64(i))
lex.next()
return
case '(':
lex.next()
readList(lex, v)
lex.next() // consume ')'
return
}
panic(fmt.Sprintf(" unexpected token %q" , lex.text()))
}
< / code > < / pre >
< p > 我们的S表达式使用标识符区分两个不同类型, 结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”时, 使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符, 我们都作为错误处理。后面的readList函数将处理结构体的成员名。< / p >
< p > 一个“(”标记对应一个列表的开始。第二个函数readList, 将一个列表解码到一个聚合类型中( map、结构体、slice或数组) , 具体类型依赖于传入待填充变量的类型。每次遇到这种情况, 循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”, endList函数用于检测结束标记。< / p >
< p > 最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记, 我们使用Index函数来获取数组每个元素的地址, 然后递归调用read函数处理。和其它错误类似, 如果输入数据导致解码器的引用超出了数组的范围, 解码器将抛出panic异常。slice也采用类似方法解析, 不同的是我们将为每个元素创建新的变量, 然后将元素添加到slice的末尾。< / p >
< p > 在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体, key部分对于成员的名字。和数组类似, 我们使用FieldByName找到结构体对应成员的变量, 然后递归调用read函数处理。对于map, key可能是任意类型, 对元素的处理方式和slice类似, 我们创建一个新的变量, 然后递归填充它, 最后将新解析到的key/value对添加到map。< / p >
< pre > < code class = "language-Go" > func readList(lex *lexer, v reflect.Value) {
switch v.Kind() {
case reflect.Array: // (item ...)
for i := 0; !endList(lex); i++ {
read(lex, v.Index(i))
}
case reflect.Slice: // (item ...)
for !endList(lex) {
item := reflect.New(v.Type().Elem()).Elem()
read(lex, item)
v.Set(reflect.Append(v, item))
}
case reflect.Struct: // ((name value) ...)
for !endList(lex) {
lex.consume('(')
if lex.token != scanner.Ident {
panic(fmt.Sprintf(" got token %q, want field name" , lex.text()))
}
name := lex.text()
lex.next()
read(lex, v.FieldByName(name))
lex.consume(')')
}
case reflect.Map: // ((key value) ...)
v.Set(reflect.MakeMap(v.Type()))
for !endList(lex) {
lex.consume('(')
key := reflect.New(v.Type().Key()).Elem()
read(lex, key)
value := reflect.New(v.Type().Elem()).Elem()
read(lex, value)
v.SetMapIndex(key, value)
lex.consume(')')
}
default:
panic(fmt.Sprintf(" cannot decode list into %v" , v.Type()))
}
}
func endList(lex *lexer) bool {
switch lex.token {
case scanner.EOF:
panic(" end of file" )
case ')':
return true
}
return false
}
< / code > < / pre >
< p > 最后, 我们将解析器包装为导出的Unmarshal解码函数, 隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误, 但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic( §5.10) , 然后返回一个对panic对应的错误信息。< / p >
< pre > < code class = "language-Go" > // Unmarshal parses S-expression data and populates the variable
// whose address is in the non-nil pointer out.
func Unmarshal(data []byte, out interface{}) (err error) {
lex := & lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
lex.scan.Init(bytes.NewReader(data))
lex.next() // get the first token
defer func() {
// NOTE: this is not an example of ideal error handling.
if x := recover(); x != nil {
err = fmt.Errorf(" error at %s: %v" , lex.scan.Position, x)
}
}()
read(lex, reflect.ValueOf(out).Elem())
return nil
}
< / code > < / pre >
< p > 生产实现不应该对任何输入问题都用panic形式报告, 而且应该报告一些错误相关的信息, 例如出现错误输入的行号和位置等。尽管如此, 我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路, 以及如何使用反射机制来填充数据结构。< / p >
< p > < strong > 练习 12.8: < / strong > sexpr.Unmarshal函数和json.Unmarshal一样, 都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型, 支持从一个io.Reader流解码。修改sexpr.Unmarshal函数, 使用这个新的类型实现。< / p >
< p > < strong > 练习 12.9: < / strong > 编写一个基于标记的API用于解码S表达式, 参考xml.Decoder( 7.14) 的风格。你将需要五种类型的标记: Symbol、String、Int、StartList和EndList。< / p >
< p > < strong > 练习 12.10: < / strong > 扩展sexpr.Unmarshal函数, 支持布尔型、浮点数和interface类型的解码, 使用 < strong > 练习 12.3: < / strong > 的方案。( 提示: 要解码接口, 你需要将name映射到每个支持类型的reflect.Type。) < / 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 = "../ch12/ch12-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 = "../ch12/ch12-08.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 = "../ch12/ch12-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 = "../ch12/ch12-08.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 = "ch12/ch12-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 >