首页 文章详情

Go 2 中你最期待什么功能?

Go语言精选 | 254 2022-02-12 10:38 0 0 0
UniSMS (合一短信)
本文由小土翻译自 What I'd like to see in Go 2.0[1],翻译不当之处,烦请指出。

阅读完文章,如果你也有想在Go2中看到的特性,欢迎留言讨论。

目录

  • 关于作者

  • 现代模板引擎

  • 改进 range,以免Copy值

  • 确定性的`select`

  • 结构化日志记录接口

  • 多错误处理

  • 针对 JSON Marshal的error

  • 标准库中不再有公共变量

  • 对缓冲渲染器的本地支持

  • 结束语

Go是我最喜欢的编程语言之一,但它仍然远非完美。在过去的10年里,我用Go来构建小型的辅助项目和大型的应用程序。虽然这门语言与2009年最初发布时相比已经有了很大的发展,但这篇文章强调了我认为Go仍有改进空间的一些领域。

在我们开始之前,我想明确一点:我不是在批评个人或他们的贡献。我唯一的意图是努力使Go成为最好的编程语言。

关于作者

Seth Vargo是谷歌的工程师。此前他曾在HashiCorp、Chef Software、CustomInk和一些匹兹堡的创业公司工作。他是《Learning Chef[2]》一书的作者,热衷于减少技术上的不平等。当他没有写作、从事开源工作、教学或在会议上发言时,Seth 喜欢与他的朋友共度时光并为非营利组织提供建议。

现代模板引擎

Go 标准库有两个模板包:`text/template`[3]`html/template`[4].它们使用大致相同的语法,但html/template处理实体转义和其他一些特定于 Web 的结构。不幸的是,这两个软件包都不适合或不够强大,在没有大量开发人员投资的情况下,可以满足足够高级的使用情况。

  • 编译时错误。与 Go 本身不同,Go 模板包会很乐意让您将整数作为字符串传递,但会在运行时呈现错误。这意味着开发人员需要严格测试其模板中所有可能的输入,而不是能够依赖类型系统。Go 的模板包应该支持编译时类型检查。

  • 与 Go 匹配range子句。 我仍然把Go模板中的范围子句的顺序弄得一团糟,因为它有时与Go本身的顺序是相反的。对于两个参数,模板引擎与标准库相匹配。

    {{ range $a, $b := .Items }} // [$a = 0, $b = "foo"]
    for a, b := range items { // [a = 0, b = "foo"]

    但是,只有一个参数,模板引擎产生的是值,而 Go 渲染产生的是索引:

    {{ range $a := .Items }} // [$a = "foo"]
    for a := range items { // [a = 0]

    Go 的模板包应该符合标准库的工作方式。

  • 开箱即用(Batteries included),反射是可选的。作为一般规则,我认为大多数开发人员不应该需要与反射进行交互。但是,如果你想做任何基本的加法和减法之外的事情,Go的模板包将迫使你使用反射。内置函数非常小,而且只能满足一小部分用例。

    在我写完Consul Template[5]之后,很明显,标准的Go模板功能不足以满足用户的需求。超过一半的问题是关于尝试使用Go的模板语言。今天,Consul Template有超过[超过50个"辅助"函数](https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md "超过50个"辅助"函数"),其中绝大部分确实应该在标准模板语言中使用。

    Consul Template在这里并不孤单。Hugo[6]还有一个相当广泛的辅助函数列表[7],同样,其中绝大多数应该真正使用标准模板语言。即使在我最近的项目中,Exposure Notification[8]我们也无法逃脱反射[9].

    Go的模板语言确实需要具有更广泛的函数表面积。

  • 短路评估。

    编辑: 正如许多人所指出的那样,这个特性将出现在Go 1.18[10].

    Go的模板语言总是在子句中评估整个条件,这会产生一些非常有趣的bug(直到运行时才会再次出现)。请考虑以下情况,其中$foo可能为零:

    {{ if (and $foo $foo.Bar) }}

    看起来这似乎很好,但是将评估这两个条件 - 表达式中没有短路逻辑。这意味着$foo如果为 nil,这将引发运行时异常。

    要解决此问题,你必须分离条件子句:

    {{ if $foo }}
      {{ if $foo.Bar }}
    {{ end }}

    Go的模板语言应该像标准库一样工作,在第一个真值时停止执行条件。

  • 对特定web工具的投资。我当 Ruby on Rails 开发人员已经很多年了,我真的很喜欢建立漂亮的网络应用程序是如此简单。使用Go的模板语言,即使是最简单的任务 - 例如将一个项目列表打印成一个句子 - 对于初学者来说也是难以企及的,特别是与Rails的Enumerable#to_sentence的相比。

改进range,以免Copy值

虽然它有很好的文档记录,但总是意外地复制范围子句中的值。例如,请思考以下代码:

type Foo struct {
  bar string
}

func main() {
  list := []Foo{{"A"}, {"B"}, {"C"}}

  cp := make([]*Foo, len(list))
  for i, value := range list {
    cp[i] = &value
  }

  fmt.Printf("list: %q\n", list)
  fmt.Printf("cp: %q\n", cp)
}

cp的价值是什么?如果你说[A B C],可悲的是你是错误的。而实际上是:

[C C C]

这是因为 Go 在子句中使用了值的副本,而不是值本身。在 Go 2.0 中,Range子句应通过引用传递值。在这个领域已经有一些关于Go 2.0的建议,包括改善 for-loop 人体工程学设计[11]在每次迭代中重新定义范围循环变量[12],所以我对此抱有谨慎的希望。

确定性的select

在select语句的多个条件为真的情况下,会通过统一的伪随机来选择case的[13].这是一个非常微妙的错误来源,并且它被看起来外观相似的switch语句而加剧,该语句确实按其编写的顺序进行评估。

考虑一下下面的代码,我们希望它的行为是 "如果系统停止了,什么都不做。否则等待新的工作,最多5秒,然后超时"。:

for {
  select {
  case <-doneCh: // or <-ctx.Done():
    return
  case thing := <-thingCh:
    // ... long-running operation
  case <-time.After(5*time.Second):
    return fmt.Errorf("timeout")
  }
}

如果在输入语句时满足多个条件(例如 doneCh已关闭且已超过 5 秒),那么哪个路径将被执行是不确定的行为。这使得编写正确的取消代码变得恼人的冗长:

for {
  // Check here in case we've been CPU throttled for an extended time, we need to
  // check graceful stop or risk returning a timeout error.
  // 优雅地停止关闭
  select {
  case <-doneCh:
    return
  default:
  }

  select {
  case <-doneCh:
    return
  case thing := <-thingCh:
    // Even though this case won, we still might ALSO be stopped.
    // 尽管选中这个case,我们也会被停止
    select {
    case <-doneCh:
      return
    default:
    }
    // ...
  default <-time.After(5*time.Second):
    // Even though this case won, we still might ALSO be stopped. 
    // 尽管超时,我们也会被停止
    select {
    case <-doneCh:
      return
    default:
    }
    return fmt.Errorf("timeout")
  }
}

如果select被更新为确定性的,原来的代码(在我看来,它更简单,更容易达到)将按原定计划工作。然而,由于select的非确定性,我们必须不断检查主导条件。

与此相关的是,我很想看到一种 "如果这个通道包含任何消息,就从这个通道读取,否则继续 "的速记语法。目前的语法是冗长的。

select {
case <-doneCh:
  return
default:
}

我很想看到这个检查的更简洁的版本,也许是这样的语法:

select <-?doneCh: // not valid Go 不过在Go是无效的

结构化日志记录接口

Go 的标准库包括`log`[14]包,这对于基本用途来说是不错的。但是,大多数生产系统都希望进行结构化日志记录,Go 中不乏结构化日志库[15]

  • apex/log[16]
  • go-kit/log[17]
  • golang/glog[18]
  • hashicorp/go-hclog[19]
  • inconshreveable/log15[20]
  • rs/zerolog[21]
  • sirupsen/logrus[22]
  • uber/zap[23]

Go在这一领域缺乏主见,导致了这些包的泛滥,其中大部分都有不兼容的函数和签名。因此,一个库的作者不可能发出结构化的日志。例如,我希望能够在 go-retry[24]go-envconfig[25]、或 go-githubactions[26]中发射结构化日志,但这样做需要与这些库中的一个紧密耦合。理想情况下,我希望我的库的用户可以选择他们的结构化日志解决方案,但由于缺乏一个通用的结构化日志接口,这一点非常困难。

Go 标准库需要定义一个结构化的日志接口, 所有这些现有的上游包都可以选择实现该接口。然后,作为库作者,我可以选择接受一个接口log.StructuredLogger,实现者可以做出自己的选择:

func WithLogger(l log.StructuredLogger) Option {
  return func(f *Foo) *Foo {
    f.logger = l
    return f
  }
}

我把这样一个接口的草图快速勾勒了出来。

// StructuredLogger is an interface for structured logging.
type StructuredLogger interface {
  // Log logs a message.
  Log(message string, fields ...LogField)

  // LogAt logs a message at the provided level. Perhaps we could also have
  // Debugf, Infof, etc, but I think that might be too limiting for the standard
  // library.
  LogAt(level LogLevel, message string, fields ...LogField)

  // LogEntry logs a complete log entry. See LogEntry for the default values if
  // any fields are missing.
  LogEntry(entry *LogEntry)
}

// LogLevel is the underlying log level.
type LogLevel uint8

// LogEntry represents a single log entry.
type LogEntry struct {
  // Level is the log level. If no level is provided, the default level of
  // LevelError is used.
  Level LogLevel

  // Message is the actual log message.
  Message string

  // Fields is the list of structured logging fields. If two fields have the same
  // Name, the later one takes precedence.
  Fields []*LogField
}

// LogField is a tuple of the named field (a string) and its underlying value.
type LogField struct {
  Name  string
  Value interface{}
}

围绕着实际的接口会是什么样子,如何最小化分配,以及如何最大化兼容性,有很多讨论,但目标是定义一个其他日志库可以轻松实现的接口。

在我的Ruby时代,有大量的Ruby版本管理器,每个都有自己的dotfile名称和语法。Fletcher Nichol设法说服了这些Ruby版本管理器的所有维护者,使其标准化为.ruby-version,只需写一个gist[27]。我希望我们能在Go 社区中使用结构化日志记录做类似的事情。

多错误处理

有很多情况,特别是对于后台工作或周期性任务,系统可能会并行处理一些事情或在出现错误时继续处理。在这些情况下,返回一个多重错误是有帮助的。在标准库中没有对处理错误集合的内置支持。

围绕多错误处理有清晰简洁的标准库定义,可以统一社区,减少错误处理不当的风险,正如我们看到的错误包装(wrap)和解包(unwrap)。

针对 JSON Marshal的error

说到error,你知道吗,将error类型嵌入到一个结构字段中,然后将该结构作为JSON进行marshal,将 "error"字段marshal为{}?

// https://play.golang.org/p/gl7BPJOgmjr
package main

import (
  "encoding/json"
  "fmt"
)

type Response1 struct {
  Err error `json:"error"`
}

func main() {
  v1 := &Response1{Err: fmt.Errorf("oops")}
  b1, err := json.Marshal(v1)
  if err != nil {
    panic(err)
  }

  // got: {"error":{}}
  // want: {"error": "oops"}
  fmt.Println(string(b1))
}

至少对于内置的errorString类型,Go应该为.Error()的结果进行marshal。另外,对于Go 2.0来说,当试图marshal一个没有实现自定义marshal逻辑的error类型时,JSON marshal会返回一个错误。

标准库中不再有公共变量

仅举一个例子,http.DefaultClienthttp.DefaultTransport都是具有共享状态的全局变量。http.DefaultClient没有配置超时,这使得DOS你自己的服务和创造瓶颈变得很容易。许多软件包都会突变,http.DefaultClienthttp.DefaultTransport,这可能会浪费开发者数天的资源来追踪错误。

Go 2.0应该把这些东西变成私有的,并通过一个函数调用来公开它们,返回有关变量的唯一分配。另外,Go 2.0还可以实现 "冻结 "的全局变量,这样它们就不能被其他包所改变。

从软件供应链的角度来看,我也担心这类问题。如果我可以开发一个有用的包,秘密地修改http.DefaultTransport,使用一个自定义的RoundTripper,将你的所有流量通过我的服务器输送出去,那将是一个非常糟糕的瞬间。

对缓冲渲染器的本地支持

这更像是一个"不为人所知或有据可查的事情"。大多数示例(包括 Go 文档中的示例)都鼓励执行以下操作,以便通过 Web 请求对 JSON 进行marshal或渲染 HTML:

func toJSON(w http.ResponseWriter, i interface{}) {
  if err := json.NewEncoder(w).Encode(i); err != nil {
    http.Error(w, "oops", http.StatusInternalServerError)
  }
}

func toHTML(w http.ResponseWriter, tmpl string, i interface{}) {
  if err := templates.ExecuteTemplate(w, tmpl, i); err != nil {
    http.Error(w, "oops", http.StatusInternalServerError)
  }
}

然而,对于这两种情况,如果i足够大,有可能在发送第一个字节(和200状态码)后,编码/执行失败。在这一点上,请求是无法恢复的,因为你无法改变响应代码。

大体上被接受的缓解方案是先渲染,然后复制到w。这仍然会有很小的出错空间(由于连接问题导致向w写入失败),但它确保在发送第一个字节之前,编码/执行是成功的。然而,在每个请求中分配一个字节片是很昂贵的,所以你通常会使用缓冲池[28]

这种方法非常冗长,并将许多不必要的复杂性推给实现者。相反,如果 Go能自动处理此缓冲池管理,可能会使用EncodePooled等函数,那就更好了。

结束语

Go仍然是我最喜欢的编程语言之一,这就是为什么我觉得可以强调这些批评的原因。与任何编程语言一样,Go也在不断发展。你认为这些是好主意吗?还是说它们是糟糕的建议?请在Twitter[29]上告诉我。

参考资料

[1]

What I'd like to see in Go 2.0: https://www.sethvargo.com/what-id-like-to-see-in-go-2

[2]

Learning Chef: https://www.amazon.com/Learning-Chef-Configuration-Management-Automation/dp/1491944935

[3]

text/template: https://pkg.go.dev/text/template

[4]

html/template: https://pkg.go.dev/html/template

[5]

Consul Template: https://github.com/hashicorp/consul-template

[6]

Hugo: https://gohugo.io/

[7]

相当广泛的辅助函数列表: https://gohugo.io/functions/

[8]

Exposure Notification: https://g.co/ens

[9]

无法逃脱反射: https://github.com/google/exposure-notifications-verification-server/blob/0ec489ba95137d5be10e1617d1dcdc2d1ee6e5e9/pkg/render/renderer.go#L232-L280

[10]

Go 1.18: https://tip.golang.org/doc/go1.18#text/template

[11]

改善 for-loop 人体工程学设计: https://github.com/golang/go/issues/24282

[12]

在每次迭代中重新定义范围循环变量: https://github.com/golang/go/issues/20733

[13]

会通过统一的伪随机来选择case的: https://golang.org/ref/spec#Select_statements

[14]

log: https://pkg.go.dev/log

[15]

Go 中不乏结构化日志库: https://www.client9.com/logging-packages-in-golang/

[16]

apex/log: https://github.com/apex/log

[17]

go-kit/log: https://github.com/go-kit/kit/tree/master/log

[18]

golang/glog: https://github.com/golang/glog

[19]

hashicorp/go-hclog: https://github.com/hashicorp/go-hclog

[20]

inconshreveable/log15: https://github.com/inconshreveable/log15

[21]

rs/zerolog: https://github.com/rs/zerolog

[22]

sirupsen/logrus: https://github.com/sirupsen/logrus

[23]

uber/zap: https://github.com/uber-go/zap

[24]

go-retry: https://github.com/sethvargo/go-retry

[25]

go-envconfig: https://github.com/sethvargo/go-envconfig

[26]

go-githubactions: https://github.com/sethvargo/go-githubactions

[27]

gist: https://gist.github.com/fnichol/1912050

[28]

使用缓冲池: https://github.com/google/exposure-notifications-verification-server/blob/08797939a56463fe85f0d1b7325374821ee31448/pkg/render/html.go#L65-L91

[29]

Twitter: https://twitter.com/sethvargo



推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter