赞
踩
注:学习《Go语言圣经》笔记,PDF点击下载,建议看书。
Go语言小白学习笔记,书上的内容照搬,大佬看了勿喷,以后熟悉了会总结成自己的读书笔记。
在本节中, 我们会构建一个简单算术表达式的求值器。 我们将使用一个接口Expr来表示Go语言中任意的表达式。 现在这个接口不需要有方法, 但是我们后面会为它增加一些。
我们的表达式语言由浮点数符号( 小数点) ; 二元操作符+, -, *, 和/; 一元操作符-x和+x;调用pow(x,y), sin(x), 和sqrt(x)的函数; 例如x和pi的变量; 当然也有括号和标准的优先级运算符。 所有的值都是float64类型。 这下面是一些表达式的例子:
下面的五个具体类型表示了具体的表达式类型。 Var类型表示对一个变量的引用。 ( 我们很快会知道为什么它可以被输出。 ) literal类型表示一个浮点型常量。 unary和binary类型表示有一到两个运算对象的运算符表达式, 这些操作数可以是任意的Expr类型。 call类型表示对一个函数的调用; 我们限制它的fn字段只能是pow, sin或者sqrt。
gopl.io/ch7/eval
为了计算一个包含变量的表达式, 我们需要一个environment变量将变量的名字映射成对应的值:
我们也需要每个表示式去定义一个Eval方法, 这个方法会根据给定的environment变量返回表达式的值。 因为每个表达式都必须提供这个方法, 我们将它加入到Expr接口中。 这个包只会对外公开Expr, Env, 和Var类型。 调用方不需要获取其它的表达式类型就可以使用这个求值器。
下面给大家展示一个具体的Eval方法。 Var类型的这个方法对一个environment变量进行查找,如果这个变量没有在environment中定义过这个方法会返回一个零值, literal类型的这个方法简单的返回它真实的值。
unary和binary的Eval方法会递归的计算它的运算对象, 然后将运算符op作用到它们上。 我们不将被零或无穷数除作为一个错误, 因为它们都会产生一个固定的结果无限。 最后, call的这个方法会计算对于pow, sin, 或者sqrt函数的参数值, 然后调用对应在math包中的函数。
func (u unary) Eval(env Env) float64 { switch u.op { case '+': return +u.x.Eval(env) case '-': return -u.x.Eval(env) } panic(fmt.Sprintf("unsupported unary operator: %q", u.op)) } func (b binary) Eval(env Env) float64 { switch b.op { case '+': return b.x.Eval(env) + b.y.Eval(env) case '-': return b.x.Eval(env) - b.y.Eval(env) case '*': return b.x.Eval(env) * b.y.Eval(env) case '/': return b.x.Eval(env) / b.y.Eval(env) } panic(fmt.Sprintf("unsupported binary operator: %q", b.op)) } func (c call) Eval(env Env) float64 { switch c.fn { case "pow": return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env)) case "sin": return math.Sin(c.args[0].Eval(env)) case "sqrt": return math.Sqrt(c.args[0].Eval(env)) } panic(fmt.Sprintf("unsupported function call:%s", c.fn)) }
一些方法会失败。 例如, 一个call表达式可能未知的函数或者错误的参数个数。 用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的( 尽管下面提到的Parse函数不会这样做) 。 这些错误会让Eval方法panic。 其它的错误, 像计算一个没有在environment变量中出现过的Var, 只会让Eval方法返回一个错误的结果。 所有的这些错误都可以通过在计算前检查Expr来发现。 这是我们接下来要讲的Check方法的工作, 但是让我们先测试Eval方法。
下面的TestEval函数是对evaluator的一个测试。 它使用了我们会在第11章讲解的testing包, 但是现在知道调用t.Errof会报告一个错误就足够了。 这个函数循环遍历一个表格中的输入, 这个表格中定义了三个表达式和针对每个表达式不同的环境变量。 第一个表达式根据给定圆的面积A计算它的半径, 第二个表达式通过两个变量x和y计算两个立方体的体积之和, 第三个表达式将华氏温度F转换成摄氏度。
对于表格中的每一条记录, 这个测试会解析它的表达式然后在环境变量中计算它, 输出结果。 这里我们没有空间来展示Parse函数, 但是如果你使用go get下载这个包你就可以看到这个函数。
go test(§11.1) 命令会运行一个包的测试用例:
这个-v标识可以让我们看到测试用例打印的输出; 正常情况下像这个一样成功的测试用例会阻止打印结果的输出。 这里是测试用例里fmt.Printf语句的输出:
幸运的是目前为止所有的输入都是适合的格式, 但是我们的运气不可能一直都有。 甚至在解释型语言中, 为了静态错误检查语法是非常常见的; 静态错误就是不用运行程序就可以检测出来的错误。 通过将静态检查和动态的部分分开, 我们可以快速的检查错误并且对于多次检查只执行一次而不是每次表达式计算的时候都进行检查。
让我们往Expr接口中增加另一个方法。 Check方法在一个表达式语义树检查出静态错误。 我们马上会说明它的vars参数。
具体的Check方法展示在下面。 literal和Var类型的计算不可能失败, 所以这些类型的Check方法会返回一个nil值。 对于unary和binary的Check方法会首先检查操作符是否有效, 然后递归的检查运算单元。 相似地对于call的这个方法首先检查调用的函数是否已知并且有没有正确个数的参数, 然后递归的检查每一个参数。
func (v Var) Check(vars map[Var]bool) error { vars[v] = true return nil } func (literal) Check(vars map[Var]bool) error { return nil } func (u unary) Check(vars map[Var]bool) error { if !strings.ContainsRune("+-", u.op) { return fmt.Errorf("unexpected unary op %q", u.op) } return u.x.Check(vars) } func (b binary) Check(vars map[Var]bool) error { if strings.ContainsRune("+-*/", b.op) { return fmt.Errorf("unexpected binary op %q", b.op) } if err := b.x.Check(vars); err != nil { return err } return b.y.Check(vars) } func (c call) Check(vars map[Var]bool) error { arity, ok := numParams[c.fn] if !ok { return fmt.Errorf("nuknow function %q", c.fn) } if len(c.args) != arity { return fmt.Errorf("call to %s has %d args, want %d", c.fn, len(c.args), arity) } for _, arg := range c.args { if err := arg.Check(vars); err != nil { return err } } return nil } var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}
我们在两个组中有选择地列出有问题的输入和它们得出的错误。 Parse函数( 这里没有出现)会报出一个语法错误和Check函数会报出语义错误。
在第3.2节中, 我们绘制了一个在编译器才确定的函数f(x,y)。 现在我们可以解析, 检查和计算在字符串中的表达式, 我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。 我们可以使用集合vars来检查表达式是否是一个只有两个变量,x和y的函数——实际上是3个, 因为我们为了方便会提供半径大小r。 并且我们会在计算前使用Check方法拒绝有格式问题的表达式, 这样我们就不会在下面函数的40000个计算过程( 100x100个栅格, 每一个有4个角) 重复这些检查。
这个ParseAndCheck函数混合了解析和检查步骤的过程:
为了编写这个web应用, 所有我们需要做的就是下面这个plot函数, 这个函数有和http.HandlerFunc相似的签名:
这个plot函数解析和检查在HTTP请求中指定的表达式并且用它来创建一个两个变量的匿名函数。 这个匿名函数和来自原来surface-plotting程序中的固定函数f有相同的签名, 但是它计算一个用户提供的表达式。 环境变量中定义了x, y和半径r。 最后plot调用surface函数, 它就是gopl.io/ch3/surface中的主要函数, 修改后它可以接受plot中的函数和输出io.Writer作为参数,而不是使用固定的函数f和os.Stdout。 图7.7中显示了通过程序产生的3个曲面。
第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。 encoding/xml包提供了一个相似的API。 当我们想构造一个文档树的表示时使用encoding/xml包会很方便, 但是对于很多程序并不是必须的。 encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。 在基于标记的样式中, 解析器消费输入和产生一个标记流; 四个主要的标记类型-StartElement, EndElement, CharData, 和Comment-每一个都是encoding/xml包中的具体类型。 每一个对(*xml.Decoder).Token的调用都返回一个标记。
这里显示的是和这个API相关的部分:
encoding/xml
这个没有方法的Token接口也是一个可识别联合的例子。 传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节, 这样就可以创造出新的实现; 在这个实现中每个具体类型都被统一地对待。 相反, 满足可识别联合的具体类型的集合被设计确定和暴露, 而不是隐藏。 可识别的联合类型几乎没有方法; 操作它们的函数使用一个类型开关的case集合来进行表述;这个case集合中每一个case中有不同的逻辑。
下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。 使用上面的API, 它可以在输入上一次完成它的工作而从来不要具体化这个文档树。
gopl.io/ch7/xmlselect
package main import ( "encoding/xml" "fmt" "io" "os" "strings" ) func main() { dec := xml.NewDecoder(os.Stdin) var stack []string for { tok, err := dec.Token() if err == io.EOF { break } else if err != nil { fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err) os.Exit(1) } switch tok := tok.(type) { case xml.StartElement: stack = append(stack, tok.Name.Local) //入栈 case xml.EndElement: stack = stack[:len(stack)-1] //出栈 case xml.CharData: if containsall(stack, os.Args[1:]) { fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok) } } } } // containsAll 判断 x 是否包含 y 中的所有元素,且顺序一直 func containsall(x, y []string) bool { for len(y) <= len(x) { if len(y) == 0 { return true } if x[0] == y[0] { y = y[1:] } x = x[1:] } return false }
每次main函数中的循环遇到一个StartElement时, 它把这个元素的名称压到一个栈里; 并且每次遇到EndElement时, 它将名称从这个栈中推出。 这个API保证了StartElement和EndElement的序列可以被完全的匹配, 甚至在一个糟糕的文档格式中。 注释会被忽略。 当xmlselect遇到一个CharData时, 只有当栈中有序地包含所有通过命令行参数传入的元素名称时它才会输出相应的文本。
下面的命令打印出任意出现在两层div元素下的h2元素的文本。 它的输入是XML的说明文档,并且它自己就是XML文档格式的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。