首页 > 其他分享 >将 Go 类型打印为 S-Expressions

将 Go 类型打印为 S-Expressions

时间:2022-09-04 12:44:06浏览次数:75  
标签:返回 val res fmt 打印 字符串 func Go Expressions

将 Go 类型打印为 S-Expressions

Photo by 西格蒙德 on 不飞溅

如果您已经使用 Go 一段时间,您可能知道当您使用“fmt”包渲染到控制台时,您可以从 go 结构中获得合理的输出。假设我定义了以下类型。

 类型内部结构{  
 InnerString 字符串  
 }  
 类型结构结构{  
 StringField 字符串  
 数字字段 float64  
 内场内场  
 }

如果我使用 fmt 打印这些实例,我会得到以下信息,

 fmt.Println(i)  
 {你好 1.23 {世界}}

不错,但如果我用 fmt.Printf 和 %s 格式字符串打印这些。

 fmt.Printf("%s\n", i)  
 {你好 %!s(float64=1.23) {世界}}

好的,这不是我所期望的,但如果我们使用 %v ,我们就会回到原来的状态。

 fmt.Printf("%v\n", i)  
 {你好 1.23 {世界}}

这种行为都记录在手册中,另外表明我可以使用 %+v,这将添加我的字段名称。

 fmt.Printf("%+v\n", i)  
 {StringField:Hello NumberField:1.23 InnerField:{InnerString:World}}

默认方法中缺少一项,它们不会告诉我正在打印的类型。我可以通过在 print 语句中嵌入类型来解决这个问题。

 fmt.Println("结构 = ", i)

我现在可以找出在我尝试调试问题或尝试找出程序流程时可以提供帮助的类型。

我不了解你,但我发现这些默认值没有帮助,我最终为我的许多类型定义了自定义 String 方法。

有一天,我有点沮丧并想,如果我能从我的 Go 结构中生成一个“lisp”风格的 S-Expression 会怎样?

[

S 表达式 - 维基百科,免费的百科全书

在计算机编程中,S 表达式(或符号表达式,缩写为 sexpr 或 sexp)是...

en.wikipedia.org

](https://en.wikipedia.org/wiki/S-expression)

那不是更容易阅读吗?想象一下,如果我的日志跟踪看起来更像以下内容,

 (结构“你好”1.23(内部“世界”))

如果字段名称与结构中声明的顺序相同,我不需要字段名称,但我确实知道正在打印哪些类型。在我看来,这些更容易阅读,特别是如果这是来自文件或数据库查询的长列表。

所以问题是,我们如何做到这一点?好吧,我可以为我的类型创建一个函数来执行此操作。我可以实现“纵梁”方法并使这项工作“自动神奇”。

[

围棋之旅

Stringer 接口 — go.dev

](https://go.dev/tour/methods/17)

 func (i Inner) String() 字符串 {  
 return fmt.Sprintf("( 内 \"%s\" )", i.InnerString)  
 }  
  
 func (s 结构) String() 字符串 {  
 return fmt.Sprintf("( 结构 \"%s\" %f %s )", s.StringField, s.NumberField, s.InnerField)  
 }

这一点都不难,当我用 fmt.Println 打印结果时,我得到了我的期望。

 (结构“你好”1.230000(内部“世界”))

很好,但是这里有一个奇怪的副作用;如果我定义纵梁接口并使用 %+v 打印,您希望得到什么?不是这个,

 (结构“你好”1.230000(内部“世界”))

这是 fmt 代码中的一个功能/错误,我不会修复,但值得了解。

我可以浏览我所有的代码并实现我的自定义打印例程,这个世界很棒。嗯,是吗?如果我使用 stringer 接口为另一个用例格式化数据会发生什么?也许传递给需要特定格式的记录器?好的,所以我可以在我的类型上定义一个新函数,让我们定义 MyString。

 func (i Inner) MyString() 字符串 {  
 return fmt.Sprintf("( 内 \"%s\" )", i.InnerString)  
 }  
  
 func (s 结构) MyString() 字符串 {  
 return fmt.Sprintf("( 结构 \"%s\" %f %s )", s.StringField, s.NumberField, s.InnerField)  
 }

好的,应该这样做,

 fmt.Println(i.MyString())  
 (结构“你好”1.230000 {World})

啊,没那么快;我的内部结构正在调用默认的 String 方法,并且不再正确转换。为了解决这个问题,我必须修改我的函数来调用特殊的 MyString 函数。

 func (s 结构) MyString() 字符串 {  
 return fmt.Sprintf("( 结构 \"%s\" %f %s )", s.StringField, s.NumberField, s.InnerField.MyString())  
 }

现在我们回到了我想要的结果。

 (结构“你好”1.230000(内部“世界”))

这不仅乏味,而且还容易出错。我会错过其中一个电话,也不会得到我期望的结果。那么为什么不按照标准库打印对象的方式来实现呢?这就是我要采取的方法。

首先,我为表达式和接口的各个部分定义了一个类型别名。我还为 NodeList 定义了一个纵梁实现。

我定义了“Lisper”接口来覆盖系统中看起来不太正确的任何类型的行为,或者如果我想避免将一些敏感数据打印到日志文件中。有时您不想将所有内容都打印出来……

 // NodeList 一个节点列表转换为字符串  
 键入节点列表 [] 字符串  
  
 func (nl NodeList) String() 字符串 {  
 返回字符串。加入(nl,“”)  
 }  
  
 // Lisper 在实现时返回一个节点切片并覆盖  
 // 默认转换为 lisp 节点,这类似于  
 // 纵梁接口的行为  
 类型 Lisper 接口 {  
 LispString() []字符串  
 }

您会注意到我在输出中引用了我的字符串以区分类型和字符串。我可以通过简单的方式达到预期的效果

 fmt.Sprint( “\””, 值, “\”” )

这里有一个问题,当我的字符串包含引号字符时会发生什么?理想情况下,它会被转义,不匹配的、未转义的引号看起来很奇怪,祝你好运构建一个解析器来读取这些表达式。 RegEx 和用转义引号替换任何找到的引号或特殊字符的例程适用于这些简单的情况。

 var quote = regexp.MustCompile("[,\"]")  
  
 //quoted 用转义引号替换所有引号和  
 // 将字符串用引号括起来  
 func 引用(n 字符串,总是布尔值)字符串 {  
 如果报价。匹配([]字节(n)){  
 返回 "\"" + 字符串。ReplaceAll(n, "\"", "\\\"") + "\""  
 } 否则如果总是 {  
 返回 "\"" + n + "\""  
 } 别的 {  
 返回 n  
 }  
 }

下一步是让这个更通用,要实现这一点,我们需要使用反射。 Go 中的反射很有吸引力,但经常被过度使用。对于这个用例,它完全符合要求。

[

反射定律 - Go 编程语言

Rob Pike 2011 年 9 月 6 日 计算中的反思是程序检查自身结构的能力……

去开发

](https://go.dev/blog/laws-of-reflection)

该方法的核心是一个函数的递归应用;因为我们希望将来能够修改这个函数,比如限制堆栈深度……我们将核心例程设为私有,但是用导出的函数包装它。

 // 运行将值转换为 lisp 样式的字符串  
 func Run(v interface{}) 节点列表 {  
 返回运行(v)  
 }  
 // run 是主力函数,它被递归调用并返回一个lisp节点堆栈  
 func run(v interface{}) []string {  
 // 代码在这里...  
 }

我强烈建议为代码的内部工作提供这些小间接。该函数的用法如下

 fmt.Println(lisper.Run(i))  
 (结构“你好”1.23(内部“世界”))

基于反射的实现

首先,如果类型有我们的“lisper”接口的定义,我们将递归短路。这个逃生舱口允许实用程序的用户在需要时定义他们自己的行为。

 // 检查是否实现了 Lisper 接口。  
 // 如果是,则返回自定义节点堆栈  
 如果 l, ok := v.(Lisper);好的 {  
 返回 l.LispString()  
 }

Go 最令人满意的部分之一是这样看似简单的代码解决了很多复杂性。

接下来,我们对提供的值使用反射来获取运行时类型和类型的值。

 val := reflect.Indirect(reflect.ValueOf(v))  
 类型 := val.Type()

这里的“间接”非常重要,因为它允许调用者将值或指针传递给函数,我们可以忽略细节。接下来,我们通过查看类型的“种类”将类型传递给类型开关。您会注意到,我们必须在这里处理的种类很少,对于其他所有内容,我们定义了一个将“值”转换为字符串的默认实现。最后,您会注意到我们对字符串类型进行了专门化,因为我们希望确保始终引用该值。否则,我们只引用符合我们的 RegEx 的值。

 切换类型。种类(){  
 案例反映。 _界面_ , 反映。 _结构_ :  
 {  
 // 代码  
 }  
 案例反映。 _片_ :  
 {  
 // 代码  
 }  
 案例反映。 _地图_ :  
 {  
 // 代码  
 }  
 案例反映。 _细绳_ :  
 {  
 节点 := fmt.Sprint(v)  
 返回 []string{quoted(node, true)}  
 }  
 默认:  
 {  
 // 使用默认的字符串方法  
 节点 := fmt.Sprint(v)  
 返回 []string{quoted(node, false)}  
 }  
 }

处理切片相对容易,

 案例反映。 _片_ :  
 {  
 水库:= []字符串{“[”}  
 对于我:= 0;我 < val.Len();我++ {  
 elemValue := val.Index(i)  
 res = append(res, run(elemValue.Interface())...)  
 }  
 资源 = 追加(资源,“]”)  
 返回资源  
 }

我们首先选择了一个起始字符,在我们的例子中是“[”,然后遍历每个切片元素,递归调用我们的“run”方法。接下来,我们使用省略号将调用结果'splat' 到我们的返回数据结构中,并以右括号“]”结束。

侧边栏,我们使用字符串切片(我们的 NodeList)来构建最终字符串,然后使用 strings.Join 函数将它们连接成最后一个字符串。这避免了我们不断地重新分配和复制字符串。另一种方法是使用strings.Builder。

我们以与切片相同的方式对 Struts 和 Interfaces 进行建模。但是,我们必须注意不要询问未导出字段的值。

 案例反映。 _界面_ , 反映。 _结构_ :  
 {  
 res := []字符串{"("}  
 res = append(res, typ.Name())  
 对于我:= 0; i < typ.NumField();我++ {  
 如果 typ.Field(i).IsExported() {  
 fieldValue := val.Field(i)  
 res = append(res, run(fieldValue.Interface())...)  
 }  
 }  
 资源 = 追加(资源,“)”)  
 返回资源  
 }

最后,我们处理地图; map 的简单实现看起来与 Slice 非常相似。 go map 的一个问题是键的顺序是不确定的,这通常是一件好事。我选择订购钥匙,以便我们的数据打印始终保持一致并有助于测试。

 案例反映。 _地图_ :  
 {  
 // 键是从地图中随机排列的,我们对输出进行排序以确保稳定性  
 键入 kv 结构 {  
 键 [] 字符串  
 值 [] 字符串  
 }  
 kvs := make([]kv, val.Len())  
 对于我,键 := 范围 val.MapKeys() {  
 值 := val.MapIndex(key)  
 kvs[i] = kv{key: run(key.Interface()), value: run(value.Interface())}  
 }  
 sort.Slice(kvs, func(i, j int) bool {  
 返回比较(kvs[i].key,kvs[j].key)  
 })  
 水库:= []字符串{“{”}  
 对于 _, kv := 范围 kvs {  
  
 资源 = 追加(资源,“(”)  
 res = append(res, kv.key...)  
 资源 = 追加(资源,“:”)  
 res = append(res, kv.value...)  
 资源 = 追加(资源,“)”)  
 }  
 资源 = 追加(资源,“}”)  
 返回资源  
 }

如果你仔细观察你会注意到一个自定义比较函数的排序,这看起来像

 //比较两个字符串数组  
 func compare(l []string, r []string) bool {  
 对于 i, j := 范围 l {  
 如果我 > len(r) {  
 返回真  
 }  
 如果 j > r[i] {  
 返回假  
 }  
 }  
 返回真  
 }

我们这样做是因为 Go 地图可以有其他类型的非字符串键,包括结构和固定大小的数组。我们已将这些键转换为 NodeList,并且需要能够比较这些字符串切片。

这就是实用程序的全部内容;下面是完整的可运行代码,但首先,这是一个作为测试的 go 示例。

 // lisper_test.go  
 包 lisper_test  
  
 进口 (  
 “fmt”  
 “表面/lisper”  
 )  
  
 //https://go.dev/blog/examples  
  
 类型自定义字符串  
  
 func (c 自定义) LispString() []string {  
 return []string{fmt.Sprintf("前缀:%s", c)}  
 }  
  
 函数示例(){  
 // 创建一些简单的标量值  
 fmt.Println(lisper.Run("一个字符串"))  
 fmt.Println(lisper.Run(6.45))  
 fmt.Println(lisper.Run("A \"quoted\" String"))  
  
 // 在结构体中创建结构体  
 类型内部结构{  
 InnerString 字符串  
 }  
 类型结构结构{  
 StringField 字符串  
 数字字段 float64  
 内场内场  
 }  
  
 // 转储空结构  
 fmt.Println(lisper.Run(Struct{}))  
  
 // 用值转储结构  
 fmt.Println(lisper.Run(Struct{  
 字符串字段:“你好”,  
 数字字段:1.23,  
 内场:内{  
 内部字符串:“世界”,  
 },  
 }))  
  
 // 自定义处理程序  
 fmt.Println(lisper.Run(Custom("Suffix")))  
 // 字符串列表  
 fmt.Println(lisper.Run([]string{"hello", "world"}))  
 // 浮点到字符串的映射  
 fmt.Println(lisper.Run(map[string]float64{"a": 1.23, "x": 7.68, "b": 4.56}))  
 // 输出:  
 //“一个字符串”  
 //6.45  
 //"一个\"引用\"字符串"  
 //( 结构 "" 0 ( 内部 "" ) )  
 //( Struct "Hello" 1.23 (Inner "World") )  
 //前缀:后缀  
 //[ “你好世界” ]  
 //{ ( "a" : 1.23 ) ( "b" : 4.56 ) ( "x" : 7.68 ) }  
 }

我发现使用带有测试的示例是一种记录实用程序(例如这个实用程序)的好方法。这是内置 Go 测试的一个经常被忽视的功能,应该更多地使用。

[

Go 中的可测试示例 - Go 编程语言

Andrew Gerrand 2015 年 5 月 7 日 Godoc 示例是显示为包文档的 Go 代码片段,并且...

去开发

](https://go.dev/blog/examples)

最后,代码

 // lisper.go  
  
 // 包 lisper 将 Go 结构转换为 lisp 样式字符串,这是默认的替代方案  
 // stringer 接口并适用于类型,而无需手动滚动输出。对于某些用例  
 // 这使得输出更有用。  
 包 lisper  
  
 进口 (  
 “fmt”  
 “反映”  
 “正则表达式”  
 “种类”  
 “字符串”  
 )  
  
 // NodeList 一个节点列表转换为字符串  
 键入节点列表 [] 字符串  
  
 func (nl NodeList) String() 字符串 {  
 返回字符串。加入(nl,“”)  
 }  
  
 // Lisper 实现时返回一个节点切片并覆盖默认转换为 lisp 节点  
 // 这类似于 stringer 接口的行为  
 类型 Lisper 接口 {  
 LispString() []字符串  
 }  
  
 // 运行将值转换为 lisp 样式的字符串  
 func Run(v interface{}) 节点列表 {  
 返回运行(v)  
 }  
  
 //比较两个字符串数组  
 func compare(l []string, r []string) bool {  
 对于 i, j := 范围 l {  
 如果我 > len(r) {  
 返回真  
 }  
 如果 j > r[i] {  
 返回假  
 }  
 }  
 返回真  
 }  
  
 var quote = regexp.MustCompile("[,\"]")  
  
 //quoted 用转义引号替换所有引号并将字符串用引号括起来  
 func 引用(n 字符串,总是布尔值)字符串 {  
 如果报价。匹配([]字节(n)){  
 返回 "\"" + 字符串。ReplaceAll(n, "\"", "\\\"") + "\""  
 } 否则如果总是 {  
 返回 "\"" + n + "\""  
 } 别的 {  
 返回 n  
 }  
 }  
  
 // run 是主力函数,它被递归调用并返回一个lisp节点堆栈  
 func run(v interface{}) []string {  
 // 检查是否实现了 Lisper 接口。如果是,则返回自定义节点堆栈  
 如果 l, ok := v.(Lisper);好的 {  
 返回 l.LispString()  
 }  
 // 创建一个不担心传入的值是否为指针的值引用  
 val := reflect.Indirect(reflect.ValueOf(v))  
 类型 := val.Type()  
 切换类型。种类(){  
 案例反映。 _界面_ , 反映。 _结构_ :  
 {  
 res := []字符串{"("}  
 res = append(res, typ.Name())  
 对于我:= 0; i < typ.NumField();我++ {  
 如果 typ.Field(i).IsExported() {  
 fieldValue := val.Field(i)  
 res = append(res, run(fieldValue.Interface())...)  
 }  
 }  
 资源 = 追加(资源,“)”)  
 返回资源  
 }  
 案例反映。 _片_ :  
 {  
 水库:= []字符串{“[”}  
 对于我:= 0;我 < val.Len();我++ {  
 elemValue := val.Index(i)  
 res = append(res, run(elemValue.Interface())...)  
 }  
 资源 = 追加(资源,“]”)  
 返回资源  
 }  
 案例反映。 _地图_ :  
 {  
 // 键是从地图中随机排列的,我们对输出进行排序以确保稳定性  
 键入 kv 结构 {  
 键 [] 字符串  
 值 [] 字符串  
 }  
 kvs := make([]kv, val.Len())  
 对于我,键 := 范围 val.MapKeys() {  
 值 := val.MapIndex(key)  
 kvs[i] = kv{key: run(key.Interface()), value: run(value.Interface())}  
 }  
 sort.Slice(kvs, func(i, j int) bool {  
 返回比较(kvs[i].key,kvs[j].key)  
 })  
 水库:= []字符串{“{”}  
 对于 _, kv := 范围 kvs {  
  
 资源 = 追加(资源,“(”)  
 res = append(res, kv.key...)  
 资源 = 追加(资源,“:”)  
 res = append(res, kv.value...)  
 资源 = 追加(资源,“)”)  
 }  
 资源 = 追加(资源,“}”)  
 返回资源  
 }  
 案例反映。 _细绳_ :  
 {  
 节点 := fmt.Sprint(v)  
 返回 []string{quoted(node, true)}  
 }  
 默认:  
 {  
 // 使用默认的字符串方法  
 节点 := fmt.Sprint(v)  
 返回 []string{quoted(node, false)}  
 }  
 }  
 }

如果您喜欢此演练,请点赞并订阅,如果您想要更多此类内容。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/12446/38420412

标签:返回,val,res,fmt,打印,字符串,func,Go,Expressions
From: https://www.cnblogs.com/amboke/p/16654868.html

相关文章