惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
The GitHub Blog
The GitHub Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recorded Future
Recorded Future
Y
Y Combinator Blog
Cloudbric
Cloudbric
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Schneier on Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cybersecurity and Infrastructure Security Agency CISA
TaoSecurity Blog
TaoSecurity Blog
Security Latest
Security Latest
N
News and Events Feed by Topic
Hacker News - Newest:
Hacker News - Newest: "LLM"
D
DataBreaches.Net
Security Archives - TechRepublic
Security Archives - TechRepublic
H
Hacker News: Front Page
C
Cisco Blogs
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
Recent Commits to openclaw:main
Recent Commits to openclaw:main
V
Vulnerabilities – Threatpost
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
A
About on SuperTechFans
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Jina AI
Jina AI
C
CXSECURITY Database RSS Feed - CXSecurity.com
Schneier on Security
Schneier on Security
T
Tenable Blog
N
News and Events Feed by Topic
W
WeLiveSecurity
有赞技术团队
有赞技术团队
AI
AI
爱范儿
爱范儿
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
T
The Blog of Author Tim Ferriss
S
Security Affairs
Know Your Adversary
Know Your Adversary
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
Google DeepMind News
Google DeepMind News
The Cloudflare Blog

文章列表 on polarisxu

通过一个例子让你彻底掌握 Go 工作区模式 这道题正确率竟然只有 22% Go 开源项目推荐:一个简单的 Go 练手项目 go test 如何禁用缓存? Go1.18 快讯:constraints 包被移除标准库 跟着 Go 作者学泛型 「2022 版」轻松搞定 Go 开发环境 鹅厂小哥用行动反对加班:程序员加班能被终结吗? Go 为什么选择 Gopher 作为吉祥物? 本地如何配置多个 GitHub/Gitee 账号? 为 Java 开发者准备的 Go 教程 02:Java 有而 Go 无 GitHub 发现了 studygolang 项目依赖的漏洞 Typora 宣布收费后,这款开源 Markdown 编辑器火了 为 Java 开发者准备的 Go 教程 01:漫游了 我的 2021 年终总结 泛型版 singleflight:Go 中如何防止缓存击穿? 2021 年你写了多少代码?这个 Go 工具帮你统计 Go1.18 新特性:TryLock Go 泛型入门教程 程序员瑞士军刀:各种结构的转换工具 终于有 Go 版的 Elasticsearch 了 Go泛型系列:maps 包讲解 Go 1.18 中的 any 是什么? Go1.18 快讯:新的 IP 包 Go编程模式:详解函数式选项模式 Go泛型系列:slices 包讲解 Nginx 竟然也有 playground — Go 语言构建的 Go泛型系列:Go1.18 类型约束那些事 Go1.18 快讯:Module 工作区模式 Go1.18 快讯:新增的 Cut 函数太方便了 Go1.18 快讯:废弃了这个 API Go1.18 快讯:新增字符串 Clone API Go:如何获得项目根目录? Go 如何获取和设置环境变量 重磅变动:更快找到微信公众号里的Go文章 Go 这样设置版本号:我们的项目也可以 假期结束,推荐 2 本 manning 出的 Go 图书 Go Fiber 框架系列教程 04:测试应用 Go Fiber 框架系列教程 03:中间件 Go泛型系列:提前掌握Go泛型的基本使用 推荐一个 Go GUI 实战项目 Go Fiber 框架系列教程 02:详解相关 API 的使用 Go Fiber 框架系列教程 01: 和 Express 对比学习 Go Module:私有不合规库怎么解决引用问题 Go1.17 新特性:go get 变了 Go1.17 新特性:testing 包的相关变化 StackOverflow 上关于 Go select 死锁的问题 新书推荐:用 Gin 框架构建分布式应用 Go 官网要变天。。。 Gin 这是要成为 Go 官方框架? 扬眉吐气:刚刚,Go 已经默认支持泛型了 Go 1.17 新特性:Module 有哪些变化? Go 第三方库推荐:类型转换如此简单 担心密码提交到 GitHub?建议使用这个 Go 开源工具 Go 的时间格式化为什么是 2006-01-02 15:04:05? GitHub 为 Go 社区带来安全支持 Go开源项目推荐:500行代码确认请求时间花在哪 一道关于 len 函数的诡异 Go 面试题解析 厉害了我的 Go:推荐系统都有开源实现 Go1.17 新特性:新版构建约束 这个工具真好:看看你的Go项目依赖有无漏洞 网友很强大,发现了Go并发下载的Bug Rust 劝退系列 09:函数 Go项目实战:一步步构建一个并发文件下载器 这本 Go 新书挺期待的:100 个常见错误 Go 启用新的官方问答社区 这是要干嘛?!微软招 Go 编译器全职开发人员 Go1.17 快报之标准库越来越注重易用性 Go1.17 新特性之切片变数组 回顾 Go 官网的演变史 站长8年前的Go代码竟然进入大厂的项目里了 Go图书翻译:一个好消息,一个坏消息 不怕烂尾!决定组织翻译这本 Go 图书 Rust 劝退系列 08:模式匹配 一本花了2.5年写成的Go免费在线图书 被黑惨了:一句话,说明自己会 Go,咋整? Go1.16 中的新函数 signal.NotifyContext 怎么用? Go 1.17 新特性提前学之测试执行随机化 Rust 劝退系列 07:流程控制 官方的 Go 多版本管理:使用和原理 Rust新书:给你一个劝退的理由 周刊题解:常量表达式这个规则应该了解下 盘点那些使用 Go 语言的国外公司 编写了50万行Go代码是一种什么体验 Rust 劝退系列 06:常量 Uber 使用 Go 的规模这么大?!都自己定制的 Go 编译器了 Go Team Leader — rsc 大神新开源了一个库,增强模板功能 再一次看到了 Go 的节制:int128 类型要不要支持? Rust 劝退系列 05:复合数据类型 「卷」有理论依据:海勒姆定律—Go又是怎么卷的 Go 真的也可以进行 GUI 开发:还有这样的图书呢 Rust 劝退系列 04:基本数据类型 我的 Go 语言书单 Rust 劝退系列 03:变量 我又来推荐免费 Go 新书了:一本用 Go 讲架构的书 注释竟然还有特殊用途?一文解惑 //go:linkname 指令 这个功能,公众号、微信群会不会被玩坏? 一道 Go 闭包题,面试官说原来自己答错了:面别人也涨知识 Rust 劝退系列 02:第一个 Rust 程序 Rust 劝退系列 01:打造开发环境
Go 格式化 API — CockroachDB errors 库(第2篇)
2020-10-24 · via 文章列表 on polarisxu

这篇文章是关于 “CockroachDB errors 库” 的系列文章的第 2 篇,“CockroachDB errors 库”实际上是 Go 的标准 errors 包的通用、开放源码的替代品。

以下面的代码为例:

import "fmt"

type T struct {
   x int
}

func main() {
   v := T{123}
   fmt.Println(v)
}

这个程序打印 {123},尽管我们没有教 Go 如何打印我们的 T 类型。它是如何做到这一点的?

printer 的等效性

fmt 包中的逻辑在所有 printer 之间共享,因此以下调用都保证等效:

  • fmt.Print(x)
  • fmt.Printf("%v", x)
  • os.Stdout.Write([]byte(fmt.Sprint(x)))
  • os.Stdout.Write([]byte(fmt.Sprintf("%v", x)))

换句话说,fmt.Print 的逻辑始终与将 Printf 与动词 %v 一起使用相同,前者实际使用后者作为实现。

同样,fmt.Println 使用 fmt.Print,外加 %v 动词,fmt.Sprintlnfmt.Sprint 是同样的道理。

fmt.Stringer 和 fmt.Formatter

现在,在上面的代码底部添加以下内容:

func (t T) String() string { return "boo" }

再次运行程序。会发生什么?它打印 boo。值 123 没有了。

这里发生的情况是,方法 String() 返回字符串实现了标准接口 fmt.Stringer,fmt 中的函数如果发现有它就会使用它。另外,尝试删除上面的 String() 函数定义,并将其替换为:

func (t T) Format(s fmt.State, _ rune) {
  fmt.Fprint(s, "baa")
}

现在会发生什么?程序打印 baa。值 123 依然没有了。

如果这两个方法都可用怎么办?程序会打印 baa: fmt.Formatter 优先 fmt.Stringer。

当两个方法都不可用时,fmt 的逻辑会"回"在它自己的内部显示代码上,从而在表示值方面尽最大努力。

fmt 对 error 知道多少?

Go 的标准 error 接口只提供返回字符串的 Error() 方法,而没有别的。

fmt 的逻辑知道 error,并知道如何使用其 Error() 方法,扩展上面解释的偏好规则:

  • fmt.Formatter 优先级最高;
  • 如果 fmt.Formatter 不存在,但是 error,则会使用 Error() 方法;
  • 否则如果存在 fmt.Stringer,则使用它。

%s、%v、%q 和 %x/%X 的关系

到目前为止,我们已经看到了,针对 %v 动词,fmt 的逻辑是如何可选地使用 fmt.Formatter、error 和 fmt.Stringer。

然而,在 Go 代码中使用的更常见的动词可能是 %s。%s 与 %v 的关系如何?

通常,%s 使用的逻辑与 %v 大致相同:如果 fmt.Stringer、error 或 fmt.Formatter 存在,将使用相同的偏好使用它。

当对象既不实现 String(),Error() 也不实现 Format() 时,就会出现区别。在这种情况下,%v 具有一些预定义的表示形式(例如,上例中的{123}),而 %s 会提示“参数的类型错误”并且无法表示任何内容。

这就是为什么除非代码使用特定类型的字符串操作值,否则 Go 习惯用法通常是使用 %v 而不是 %s。

附加动词 %q 和 %x/%X 是 %s 的变体(当 String(),Error() 和 Format() 都不可用时具有相同的限制):

  • %q 用引号引起来字符串,所以 fmt.Printf("%q", he said "hi") 打印出 he said "hi"
  • %x/%X 显示字符串中字节的十六进制表示形式。在实践中很少使用该方法(该方法更多用于整数)。

打印值,指针方法集

现在考虑上面的程序,以及以下实现的组合(注意接收器的类型):

func (t T) String() string { return "boo" }
func (t *T) Format(s fmt.State, _ rune) { fmt.Fprint(s, "baa") }

现在打印的是 boo。为什么是这样?上面的代码按值传递 T 实例。根据方法集的概念,只实现了 fmt.Stringer 接口,因此输出 boo。如果现在改为这样:

func (t *T) String() string { return "boo" }
func (t *T) Format(s fmt.State, _ rune) { fmt.Fprint(s, "baa") }

输出什么?这次再次输出:{123},因为 fmt 的逻辑“看不到”上面的方法。

因此有如下规则:如果对象按值打印,则只考虑其按值方法。(其实就是方法集问题)

打印引用,值方法集

现在,让我们用以下主程序:

func main() {
   v := &T{123}
   fmt.Println(v)
}

现在考虑以下程序变体:

  • 变体 A:

    func (t T) String() string { return "boo" }
    func (t T) Format(s fmt.State, _ rune) { fmt.Fprint(s, "baa") }
    
  • 变体 B:

    func (t T) String() string { return "boo" }
    func (t *T) Format(s fmt.State, _ rune) { fmt.Fprint(s, "baa") }
    
  • 变体 C:

    func (t *T) String() string { return "boo" }
    func (t T) Format(s fmt.State, _ rune) { fmt.Fprint(s, "baa") }
    
  • 变体 D:

    func (t *T) String() string { return "boo" }
    func (t *T) Format(s fmt.State, _ rune) { fmt.Fprint(s, "baa") }
    

针对这些情况,上面的程序输出什么?都是 “baa”。

指针接收器 *T 的方法集包含 T 和 *T 的方法集。(原文作者写的不对,说变体 C 输出 “boo”)。

使用 %+v 动词打印

数字类型的 + 标志强制显示正值的加号,以便始终显示符号位。

然而,与 v 结合,它会触发"详细打印"。

根据 fmt 的默认逻辑,这会将字段的名称添加到结构中。

如果实现了 fmt.Stringer 接口,+ 不会对结果有任何改变, 如果实现了 fmt.Formatter 接口,根据约定,Format() 方法输出的信息比未指定 + 时的信息更详细。

Go 库没有规定应如何实现:不同的包往往以不同的方式实现。然而,缺乏规范不是问题;在这两种情况下,输出都供人眼使用,因此小显示不一致并不被视为问题。

Go 语法表示和 %#v 动词

最后,将原来的主程序改为使用 %#v 动词:

func main() {
   v := T{123}
   fmt.Printf("%#v\n", v)
}

这会打印什么?

  • 如果 String() 方法可用,会忽略它;
  • 如果 Format() 方法可用,则使用它;
  • 如果 GoString() 方法可用(fmt.GoStringer 接口),则使用它;
  • 否则,将使用 Go 语法结构的打印输出。

这里发生的情况是,%#v 说明符打算打印值的 “Go表示”,而不是它的“人类可读表示”。fmt 逻辑知道如何这样做,但是自定义类型可以用 fmt 自定义这种行为:即实现 fmt.Formatter 或 GoStringer 接口。

注意,出于完整性考虑,上面解释了 GoStringer,但在实践中,发现它很少被使用。

我个人推荐 https://github.com/kr/pretty 这个工具,它比 Go 的标准库更清晰地打印 Go 语法表示。例如:fmt.Printf("%# v", pretty.Formatter(x))

格式化动词、标识和修改器

到目前为止,我们已经看到 %v 与 %s 在意图和目的上的不同,以及例 %v 与 %+v 的不同。

如果我们想用不同的结果来定义我们自己的定制呢?

对于上面三种情况,可靠的定制机制是 fmt.Formatter 接口:

package fmt

// Formatter can be implemented by your custom types.
type Formatter interface {
     Format(s State, verb rune)
}

// An object of type State is provided by the fmt
// logic to your custom Format() method.
type State interface {
     io.Writer // inherits the Write() method

     Flag(int) bool

     Width() (int, bool)
     Precision() (int, bool)
}

最让我感兴趣的是:

  • 参数 verb 直接传递给我们自定义的 Format() 方法。这表示主“格式化动词”:对于 %v,verb == ‘v’。对于 %#v,依然是 verb == ‘v’。对于 %s,verb 是 s,以此类推。
  • 有 Flag() 方法的 fmt.State 作为参数传递给 Format() 方法。如果设置了相应的格式化标志,Flag() 返回 true。例如,对于 %v,Flag('#') == false,而对于 %#v,Flag('#') == true
  • 此外,fmt.State 也实现了 io.Writer 接口。这样就可以直接将状态变量作为第一个参数传递给另一个对 fmt.Fprint 的调用,进一步简化了自定义 Format() 方法的实现。

fmt.State 上的 Width() 和 Precision() 方法也很有趣,因为它们允许访问格式化字符串中的附加数值参数或修饰符。例如,在 %3.2f 中,我们有宽度 3 和精度 2。然而,这些在实践中很少被使用。

下面是一个符合 Go 习惯的例子:

type Response struct {
     code int
     msg string
}

func (r *Response) Format(s fmt.State, verb rune) {
   switch verb {
   case 'v':
       if s.Flag('+') {
          // With %+v, we print both the message and the code.
          fmt.Fprintf(s, "%s (%d)", r.msg, r.code)
       }
       fallthrough
   case 's':
       // For %s, or %v without +, we just print the message.
       fmt.Fprint(s, r.msg)
   }
}

// String is provided for convenience.
func (r *Response) String() string { return fmt.Sprint(r) }

简单解释下:

  • 以上实现中,对于 %+v,它将同时输出 msg 和 code。当只有 %v/%s,它只打印 msg。
  • 为了让类型兼容 fmt.Stringer 接口,以便用于其他需要 String() 方法的地方,通过调用 fmt.Sprint 实现 String()。

下面会进一步讨论。

上面代码一个有趣的点是,它不处理 %q/%x/%x。对于这些动词,它不输出任何内容。

它也不支持除了 + 之外的其他标志,例如,它对待 %#v 和 %v 是相同的。

事实上,Go API 并没有使实现与自身内部逻辑一样通用和强大的自定义 Format() 变得容易,而且“野生的” Go 包常常包含像上面那样的不完整实现。

实践中自定义 Formatter

我在实践中发现,在整个生态系统的包经常按如下方式处理:

  • 自定义 Format() 方法总是为 v 动词做一些有效和有用的事情,而不考虑提供的标志。
  • 带有动词 v 但没有标志的 Format() 的行为(即一个简单的 %v),通常与 String() 的行为保持一致。
  • 如果自定义格式化程序同时具有“简单”和“详细”模式,那么它通常将 + 识别为访问详细模式的标志。
  • 如果 %s 和 %v(没有标志)都被识别,它们通常输出相同的内容。
  • 在自定义 Format() 方法中正确处理 %q、%x 和 %X 的情况并不常见。
  • 非数值类型的自定义格式化程序几乎从不处理宽度和精度修饰符。

最后一点特别说明了为什么关心固定宽度字符串格式的代码应该在以下两个步骤中拼写输出:

s := fmt.Sprint(v)
fmt.Printf("%30s", s)  // instead of printing v directly

在 fmt.Stringer、fmt.Formatter 和 error 之间重用代码

上面的一个例子是通过调用 fmt.Sprint 实现 String()。它又在同一类型上使用 Format() 方法。简化为:

type T struct { msg string }

func (r *T) Format(s fmt.State, _ rune) {
   fmt.Fprint(s, r.msg)
}

func (r *T) String() string {
   // This causes fmt to call Format() above and ultimately
   // print r.msg.
   return fmt.Sprint(r)
}

在这种情况下,为什么人们会选择通过返回 fmt.Sprint(r) 而不是返回 r.msg 来实现 String() 呢?

这是遵循 DRY 原则的实例:如果以后逻辑需要更改为"打印更多内容",则只需修改 Format() 方法;String() 方法会自动从中受益。

这种模式比较常见。以下是另一种形式:

type T struct { msg string }

func (r *T) String() string {
   return r.msg
}

func (r *T) Format(s fmt.State, _ rune) {
   fmt.Fprint(s, r.String()) // or: s.Write([]byte(r.String()))
}

同样,一个方法实现"使用另一个",因此一个方法只需要更改其中任何一个,在两者中获得相同的行为。

同样,如果涉及 error 接口,我们会在实践中看到所有重用组合:

type T struct { msg string }

func (r *T) Error() string { return r.msg }
func (r *T) String() string { return r.Error() }
func (r *T) Format(s fmt.State, _ rune) { fmt.Fprint(s, r.Error()) }

type U struct { msg string }

func (r *U) String() string { return r.msg }
func (r *U) Error() string { return r.String() }
func (r *U) Format(s fmt.State, _ rune) { fmt.Fprint(s, r.String()) }

type V struct { msg string }

func (r *V) String() string { return fmt.Sprint(r) }
func (r *V) Error() string { return fmt.Sprint(r) }
func (r *V) Format(s fmt.State, _ rune) { fmt.Fprint(s, r.msg) }

Q:为什么我们看到如此多样性?

A:我不太确定,但我责怪 Go 库文档中缺乏方案。另请参阅下面的两个答案。

Q:既然我们在每个情况下都得到相同的结果,这有关系吗?

A:从功能的角度来看,这些示例都是等效的。从性能角度来看,应考虑在程序中更经常使用哪些变体。如果常用 String() 方法,比打印出对象更是如此,那么让 String() 包含最简单的实现可能会产生更好的性能。这是因为 fmt 包中的逻辑有点重量级。然而请注意,在实践中,我并没有发现这种情况经常发生,所以我要说,这并不重要。

Q:我正在实现自己的自定义类型。我应该瞄准什么模式?

A:如果您的类型只有一个表示形式,直接使用 String() 即可;如果您实现错误类型,自然使用 Error() 更合适,一般都不需要实现 Format()。但如果需要区分"简单"和"详细"显示,则首先实现 Format() 然后从中派生 String() 或 Error()。

总结

Go 在其标准 fmt 包中提供了通用格式 API。

该 API 中的所有函数都由通用逻辑提供支持,这是 Print/Sprintf 在引擎下使用的逻辑:每个对象都显示在某种格式"动词"的上下文中。

最常见和可靠的动词是 v(提示:它是 “v”,如"value"),也被 Print() 和 Println() 使用。它可以打印几乎任何东西,并不挑剔的值是零或实现一个特定的接口。

同时,在实现自己的类型时,可以通过实现某些接口自定义 fmt 的行为:

  • fmt.Stringer,一个简单的 String() string 方法;
  • error,一个简单的 Error() string 方法;
  • fmt.Formatter,一个 Format() 方法。当通过 %v 与 %+v 以及其他动词和标志组合使用时,这可用于显示不同的东西。

在实践中,我们看到同时提供 String() 和 Format() 方法或 Error() 和 Format() 方法的包。一个通常是通过调用另一个来实现的,以避免代码重复。Go 的标准库允许所有的组合重用,实际上我们可以在生态系统中找到所有变体。

原文链接:https://dr-knz.net/go-formatting-apis.html

本文作者:Raphael ‘kena’ Poss

译者:polarisxu