Go: 提议:字符串插值

创建于 2019-09-08  ·  67评论  ·  资料来源: golang/go

介绍

对于那些不知道它是什么的人:

迅速

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"

科特林

var age = 21

println("My Age Is: $age")

C

```c#
字符串名称=“标记”;
var date = DateTime.Now;

Console.WriteLine($"你好,{name}!今天是 {date.DayOfWeek},现在是 {date:HH:mm}。");

# Reasoning of string interpolation vs old school formatting

I used to think it was a gimmick but it is not in fact. It is actually a way to provide type safety for string formatting. I mean compiler can expand interpolated strings into expressions and perform all kind of type checking needed.

### Examples

变量:=“变量”
res := "123{variable}321" // res := "123" + variable + "321"


return errors.New("opening config file: \{err}") // return errors.New("opening config file:" + err.Error())


var status fmt.Stringer

msg := "exit status: {status}" // msg := "exit status:" + status.String()


v := 123
res := "value = {v}" // res := "value = " + someIntToStringConversionFunc(v)

# Syntax proposed

* Using `$` or `{}` would be more convenient in my opinion, but we can't use them for compatibility reasons
* Using Swift `\(…)` notation would be compatible but these `\()` are a bit too stealthy

I guess  `{…}` and `\(…)` can be combined into `\{…}`

So, the interpolation of variable `variable` into some string may look like

"{多变的}"

Formatting also has formatting options. It may look like

"{多变的[:]}"

#### Examples of options

v := 123.45
fmt.Println("value={v:04.3}") // value=0123.450


v := "价值"
fmt.Println("value='{v:a50}'") // value='<45 个空格>value'

# Conversions

There should be conversions and formatting support for built in types and for types implementing `error` and `fmt.Stringer`. Support for types implementing

类型格式化程序接口 {
格式(格式字符串)字符串
}
```

可以稍后介绍处理插值选项

与传统格式相比的优缺点

优点

  • 类型安全
  • 性能(取决于编译器)
  • 自定义格式选项支持用户定义的类型

缺点

  • 编译器的复杂性
  • 支持较少的格式化方法(没有%v (?)、 %T等)
Go2 LanguageChange Proposal

最有用的评论

对我来说,类型检查作为在语言中包含字符串插值的一个理由并不是那么引人注目。 还有一个更重要的原因,这与您在fmt.Printf中写入变量的顺序无关。

让我们从提案描述中举一个例子,并用 Go 编写有和没有字符串插值的例子:

  • 没有字符串插值(当前 Go)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %v! Today is %v, it's %02v:%02v now.", name, date.Weekday(), date.Hour(), date.Minute())
  • 具有字符串插值的等效功能(以及混合@bradfitz注释,需要表达格式化选项)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %{name}! Today is %{date.Weekday()}, it's %02{date.Hour()}:%02{date.Minute()} now.")

对我来说,第二个版本更容易阅读和修改,而且更不容易出错,因为我们不依赖于我们编写变量的顺序。

阅读第一个版本时,您需要重新阅读该行数次以检查是否正确,来回移动眼睛以在“%v”的位置与您需要放置的位置之间建立心理映射变量。

我一直在修复几个应用程序中的错误(不是用 Go 编写的,但有相同的问题),其中数据库查询已经用大量“?”编写。 而不是命名参数( SELECT * FROM ? WHERE ? = ? AND ? != false ... )和进一步的修改(甚至在 git 合并期间无意中)翻转了两个变量的顺序😩

因此,对于一种目标是长期简化可维护性的语言,我认为这个原因值得考虑使用字符串插值

关于这个的复杂性:我不知道 Go 编译器的内部结构,所以我的意见是一粒盐,但是_难道不能把上面例子中显示的第二个版本翻译成第一个吗?_

所有67条评论

为什么我们不能只使用 ${...}? Swift 已经有一种语法,JavaScript 和 Kotlin 有另一种,C# 还有另一种,还有 Perl……为什么还要发明一种变体? 坚持使用最易读且已经存在的不是更好吗?

为什么我们不能只使用 ${...}? Swift 已经有一种语法,JavaScript 和 Kotlin 有另一种,C# 还有另一种,还有 Perl……为什么还要发明一种变体? 坚持使用最易读且已经存在的不是更好吗?

因为我们可能有带有${…}的现有字符串。 例如,我愿意

现在不允许使用\{

与调用fmt.Sprintf相比,这似乎没有太大优势。

与调用fmt.Sprintf相比,这似乎没有太大优势。

是的,除了完整的类型安全性、更好的性能和易用性之外,它还没有。 为具有静态类型的语言正确格式化。

据我所知,这个提议并不比使用fmt.Sprintf%v更安全。 它在类型安全方面基本相同。 我同意在许多情况下,仔细的实施可能会有更好的表现。 我不知道易用性。

为了实现这一点,我们必须在语言规范中编写用于所有类型的确切格式。 我们必须决定并记录如何格式化切片、数组、地图。 一个界面。 一个频道。 这将是语言规范的重要补充。

我认为这是两种方式都有权存在的问题之一,这取决于决策者来决定。 在围棋中,很久以前就已经做出了决定,而且它有效,而且是惯用的。 那是fmt.Sprintf%v

从历史上看,有些语言从一开始就存在字符串内插值。 值得注意的是 Perl。 这就是 Perl 如此受欢迎的原因之一,因为与 C 等人中的sprintf()相比,它非常方便。 那时还没有发明%v 。 然后有些语言存在插值,有点,但在语法上不方便,想想"text" + v1 + "text" 。 然后 JavaScript 引入了多行反引号文字,并支持在${...}内插入任意表达式,这与"text" + v1 + "text"相比有了巨大的改进。 Go 也有反引号多行文字,但没有${...} 。 谁抄谁我不知道。

我不同意@ianlancetaylor对此的支持需要大量努力。 事实上, fmt.Sprintf%v正是这样做的,不是吗? 在我看来,它就像一个不同的语法包装器,与引擎盖下的完全相同的东西。 我对吗?

但是__我同意__@ ianlancetaylor的观点,即使用fmt.Sprintf%v一样方便。 它在屏幕上的长度也完全相同,而且,恕我直言,非常重要的是 - 对于 Go 来说已经是惯用的了。 它有点让Go Go。 如果我们从其他所有语言中复制实现所有其他功能,那么它将不再是 Go,而是所有其他语言。

还有一件事。 根据我对 Perl 的长期经验,字符串插值从一开始就存在,我可以说它并不是那么完美。 这是有问题的。 对于简单而琐碎的程序,可能对于所有程序的 90%,变量的字符串插值工作得很好。 但是,偶尔,您会得到一个var=1.99999999999 ,并希望将其打印为1.99 ,而您 __can't__ 使用标准字符串插值来做到这一点。 您要么需要事先进行一些转换,要么...查看文档,并重新学习早已被遗忘的sprintf()语法。 这里 __is__ 的问题 - 使用字符串插值让您忘记如何使用类似 sprintf() 的语法,并且可能它非常存在。 然后在需要的时候——你花费太多时间和精力去做最简单的事情。 我在 Perl 上下文中谈论,这是语言设计者的决定。

但是在围棋中,做出了不同的决定。 带有%vfmt.Sprintf $ 已经在这里了,它 __ 和插值字符串一样方便,也很短 __,而且它是 __idiomatic__ ,因为它在文档中,在示例中,无处不在。 而且它不会遇到最终忘记如何将 1.99999999999 打印为 1.99 的问题。

引入提议的语法将使 Go 更像 Swift 和/或更像 JavaScript,有些人可能会喜欢这样。 但我认为这种特殊的语法不会让它变得更好,甚至更糟。

我认为现有的打印方式应该保持原样。 如果有人需要更多 - 那么有模板。

如果这里的部分论点是编译时的安全性,我不同意这是一个令人信服的论点; go vet以及扩展名go test已经标记了fmt.Sprintf的不正确使用一段时间。

如果您真的愿意,也可以通过go generate来优化今天的性能。 在大多数情况下,这是一个不值得的权衡。 我觉得这同样适用于大大扩展规范; 这种权衡通常是不值得的。

允许在插值字符串中调用函数将是不幸的——太容易错过了。
不允许它们是另一个不必要的特殊情况。

如果这里的部分论点是编译时的安全性,我不同意这是一个令人信服的论点; go vet 和 go test 一直在标记 fmt.Sprintf 的错误使用一段时间。 如果您真的愿意,也可以通过 go generate 优化今天的性能。 在大多数情况下,这是一个不值得的权衡。 我觉得这同样适用于大大扩展规范; 这种权衡通常是不值得的。

但是类型安全应该由编译器来保证,而不是由工具来保证。 这是语义; 这就像说应该有一个工具来验证您忘记在哪里进行空检查,而不是使用可选项或显式声明可空值。

除此之外 - 唯一安全的方法是使用依赖类型。 对于与fmt.Sprintf相同的东西,字符串插值只是更多的语法糖,虽然我都赞成一些好的糖,但整个围棋社区似乎并不支持。

或者也许像修改语言以更好地与 fmt.Printf 和朋友一起工作。

就像 fmt 支持%(foo)v%(bar)q之类的东西,然后说如果在调用可变参数函数/方法时使用了包含%(<ident>)的字符串文字,那么所有引用的符号会自动附加到可变参数列表中。

例如这段代码:

name := "foo"
age := 123
fmt.Printf("The gopher %(name)v is %(age)2.1f days old.")

真的会编译成:

name := "foo"
age := 123
fmt.Printf("The gopher %(name)v is %(age)2.1f days old.", name, age)

fmt可以跳过不必要的(name)(age)位。

不过,这是一个非常特殊的语言更改。

对我来说,类型检查作为在语言中包含字符串插值的一个理由并不是那么引人注目。 还有一个更重要的原因,这与您在fmt.Printf中写入变量的顺序无关。

让我们从提案描述中举一个例子,并用 Go 编写有和没有字符串插值的例子:

  • 没有字符串插值(当前 Go)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %v! Today is %v, it's %02v:%02v now.", name, date.Weekday(), date.Hour(), date.Minute())
  • 具有字符串插值的等效功能(以及混合@bradfitz注释,需要表达格式化选项)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %{name}! Today is %{date.Weekday()}, it's %02{date.Hour()}:%02{date.Minute()} now.")

对我来说,第二个版本更容易阅读和修改,而且更不容易出错,因为我们不依赖于我们编写变量的顺序。

阅读第一个版本时,您需要重新阅读该行数次以检查是否正确,来回移动眼睛以在“%v”的位置与您需要放置的位置之间建立心理映射变量。

我一直在修复几个应用程序中的错误(不是用 Go 编写的,但有相同的问题),其中数据库查询已经用大量“?”编写。 而不是命名参数( SELECT * FROM ? WHERE ? = ? AND ? != false ... )和进一步的修改(甚至在 git 合并期间无意中)翻转了两个变量的顺序😩

因此,对于一种目标是长期简化可维护性的语言,我认为这个原因值得考虑使用字符串插值

关于这个的复杂性:我不知道 Go 编译器的内部结构,所以我的意见是一粒盐,但是_难道不能把上面例子中显示的第二个版本翻译成第一个吗?_

@ianlancetaylor指出我上面的草图(https://github.com/golang/go/issues/34174#issuecomment-532416737)不是严格向后兼容的,因为可能有很少的程序会改变它们的行为。

向后兼容的变体是添加一种新类型的“格式化字符串文字”,前缀为f

例如这段代码:

name := "foo"
age := 123
fmt.Printf(f"The gopher %(name)v is %(age)2.1f days old.")

真的会编译成:

name := "foo"
age := 123
fmt.Printf(f"The gopher %(name)v is %(age)2.1f days old.", name, age)

但是随后双fPrintf中的一个,后跟新类型字符串文字之前的f )将是口吃。

我也不了解编译器的内部工作原理,所以我(也许是愚蠢地)也假设这可以在编译器中实现,所以像
fmt.printf("Hello %s{name}. You are %d{age}")

将编译为等效的当前公式。

字符串插值具有更高的可读性(Go 的核心设计决策)的明显优势,并且随着处理的字符串变得更长和更复杂(Go 的另一个核心设计决策),它也可以更好地扩展。 另请注意,使用 {age} 会给出字符串上下文,如果您只是略读字符串(当然忽略指定的类型),则字符串可能会以“You are tall”结尾, “你在 [在 XXX 位置]”,“你工作太努力了”,除非你投入精神能量将格式方法映射到每个插值实例,否则不会立即清楚应该去哪里。 通过消除这个(诚然很小的)心理障碍,程序员可以专注于逻辑而不是代码。

编译器实现语言规范。 语言规范目前对 fmt 包一无所知。 它不必。 您可以编写根本不使用 fmt 包的大型 Go 程序。 将 fmt 包文档添加到语言规范中会使它明显变大,这是另一种说法,它使语言变得更加复杂。

这并没有使该提案无法通过,但这是一个巨大的成本,我们需要一个巨大的收益来超过这个成本。

或者我们需要一种方法来讨论字符串插值而不涉及 fmt 包。 这对于字符串类型的值,甚至[]byte类型的值非常清楚,但对于其他类型的值则不太清楚。

我不赞成这个提议,部分原因是@IanLanceTaylor上面所说的,部分原因是当您尝试使用格式化选项插入复杂的表达式时,任何可读性优势都会消失。

此外,有时会忘记在fmt.Print (和Println )系列函数中包含可变参数的能力已经启用了一种插值形式。 我们可以很容易地使用以下代码重现前面引用的一些示例,在我看来,这些代码同样具有可读性:

multiplier := 3
message := fmt.Sprint(multiplier, " times 2.5 is ", float64(multiplier) * 2.5)

age := 21
fmt.Println("My age is:", age)

name := "Mark"
date := time.Now()
fmt.Print("Hello, ", name, "! Today is ", date.Weekday(), ", it's ", date.String()[11:16], " now.\n")

name = "foo"
days := 12.312
fmt.Print("The gopher ", name, " is ", fmt.Sprintf("%2.1f", days), " days old\n.")

仍然在语言中添加和 __have__ 它的另一个原因: https ://github.com/golang/go/issues/34403#issuecomment -542560071

我们在https://github.com/golang/go/issues/34174#issuecomment -540995458 中发现@alanfo的评论令人信服:您可以使用fmt.Sprint进行简单的字符串插值。 语法可能不太方便,但是在这方面的任何方法都需要一个特殊的标记,以便在任何情况下都可以插入变量。 并且,如前所述,这允许对要插值的值进行任意格式设置。

如上所述,有一种现有的方法可以近似地做到这一点,甚至允许对单个变量进行格式化。 因此,这是一个可能的下降。 开放四个星期以供最终评论。

我经常面临构建包含 50 多个要插入的变量的文本块。 在少数情况下超过 70。 使用 Python 的 f 字符串(类似于上面提到的 C#),这将很容易维护。 但是出于几个原因,我在 Go 而不是 Python 中处理这个问题。 用于管理这些块的fmt.Sprintf的初始设置是...好的。 但是上帝禁止我必须以任何涉及移动或删除%anything标记及其位置相关变量的方式来修复错误或修改文本。 手动构建地图以传递给template或设置os.Expand也不是一个好选择。 我将在一周中的任何一天采用fmt.Sprintf的速度(设置)和 f-strings 的易维护性。 不, fmt.Sprint不会有很大的好处。 在这种情况下,比fmt.Sprintf更容易设置。 但它在视觉上失去了很多意义,因为你跳进跳出字符串。 "My {age} is not {quality} in this discussion"不像 $#$ "My ", age, " is not ", quality, " in this discussion" $#$ 那样跳进跳出字符串。 尤其是在数十次参考的过程中。 移动文本和引用只是使用 f 字符串复制和粘贴。 删除只是选择和删除。 因为你总是在字符串中。 使用fmt.Sprint时情况并非如此。 很容易意外(或必然)选择非字符串逗号或双引号字符串终止符并移动它们以破坏格式并需要编辑以_将其重新定位_。 在这些情况下, fmt.Sprintfmt.Sprintf比任何类似 f 字符串的方法都更耗时且更容易出错。

这听起来像是一个非常可怕的任务,但是你这样做!

如果我遇到这样的事情,那么我最初肯定会考虑text/template ,或者,如果将变量放入结构或映射太尴尬,我可能更喜欢fmt.Sprintfmt.Sprintf但按以下方式排列代码:

s := fmt.Sprint(
    text1, var1,     // comment 1
    text2, var2,     // comment 2
    ....,
    text70, var70,   // comment 70
    text71,
)

尽管这会占用大量屏幕空间,但更改、删除或移动内容相对容易,而且不会有太大的出错风险。

有一些 go 字符串插值库,但没有联合类型或泛型等语言特性,它们不像其他语言那样灵活和流畅:

package main

import (
    "fmt"

    "github.com/imkira/go-interpol"
)

func main() {
    m := map[string]string{
        "foo": "Hello",
        "bar": "World",
    }
    str, err := interpol.WithMap("{foo} {bar}!!!", m)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Println(str)
}

或者, go.uber.org/yarpc/internal/interpolate

再一次,构建一个地图,以便我可以使用template/text ,一个第 3 方库或os.Expand的最小系统,当您没有 50 个或更多需要为变量创建的键时,这是很棒的对于相关代码的其余部分,它们已经存在。 反对此功能的唯一真正论据是“您可能会忘记如何正确使用 % 格式”,并且可能需要多花 2 分钟使用 Google 来查找您绝对需要效率、特异性或其他任何实例的格式fmt.Sprintf ? 另一方面,也许你几乎总是需要fmt.Sprintf的效率、特异性或其他任何东西,并且永远不要忘记格式化,因为你总是使用它。 我没有看到问题。

另一个论点是它使语言更复杂? 是的,它确实。 从技术上讲,对语言的任何添加都会增加其复杂性。 而且我完全赞成不要因为琐碎的原因而使语言变得更复杂。 我_真的_希望看到in在 Python 中的所有使用方式。 但我会满足于将:= range替换为in 。 在过去的一年里我几乎没有接触过 Python,并且只使用了两年。 但是十有八九我先输入in然后用:= range修复它。 但我不会为那个而争吵。 := range足以对您之前编写的代码进行推理。 但是 f 弦和它们的种类是完全不同的。 那是核心功能、可用性和可维护性。 它绝对应该成为 Go 规范的一部分。 如果有任何设施可以作为第 3 方图书馆,我会建造图书馆并完成它。 但据我所知,如果不对 Go 语言进行额外扩展,这甚至是不可行的。 所以至少我认为应该对语言进行扩展。

@runeimp我想明确表示,您关于我们可以开始支持"My {age} is not {quality} in this discussion"的建议是行不通的。 这在今天是一个有效的 Go 字符串,如果我们期望插入agequality会破坏现有的 Go 程序。 它必须更像"My \{age} is not \{quality} in this discussion" 。 格式化非字符串值的问题仍然存在。

我很欣赏@ianlancetaylor。 为了清楚起见,Python f 字符串是f"My {age} is not {quality} in this discussion"F'My {age} is not {quality} in this discussion'或任何大小写f和单引号或双引号的任意组合。 如前所述,非常类似于 C# 的做法。 此外,虽然我更喜欢 Python f-string 或 C# 样式,但我绝对会接受有助于类似用法的_ANYTHING_。 InterPolationACTIVATE"My @$@age###^&%$ is not @$@quality###^&%$ in this discussion"INTERpolationDEACTVATEandNullify是可以接受的。 蹩脚,但可以接受。 我什至不是在开玩笑。 在一周中的任何一天,我都会以这个蹩脚的借口对fmt.Sprintf进行字符串插值。 仅维护利益就可以证明这一点。

我知道这将对您的特定情况有所帮助。

https://blog.golang.org/go2-here-we-come @griesemer写道,对语言的任何更改都应该

  1. 解决许多人的重要问题,
  2. 对其他人的影响最小,并且
  3. 提供清晰易懂的解决方案。

这对很多人来说是一个重要的问题吗?

我不知道,但我希望它是。 我认为对于那些只需要在短字符串上处理 5 个或更少引用的人来说,这个功能并不重要。 或者由于其他规范要求,总是不得不使用模板来管理它。 但对于我们这些确实使用它的人来说。 不存在时非常想念它。 对于那些从未有过选择的人(在 Go 之前只使用过 C、C++ 等),可能很难从纯理论层面理解这些好处,因为他们经验中的示例可能很快就会被遗忘或只被认为是他们为了忘记痛点而不得不处理的最糟糕的项目。 除非大多数 Go 开发人员来自实际支持局部变量字符串插值的语言,否则我怀疑您是否会看到大多数人对此问题的回应。

自从我 20 多年前成为一名专业的 Web 开发人员以来,我一直遇到这个问题。 在大多数情况下,模板在前端和后端都很常见。 在我职业生涯的最后阶段,我最终在 Ruby on Rails 中工作,并立即爱上了 Ruby 的"My #{age} is not #{quality} in this discussion"字符串插值。 尽管英镑花括号语法最初让我觉得很奇怪。 当我过渡到集成工程时,我主要使用 Python 3,并且使用它的新str.format()系统而不是旧的 % 格式化字符串更快乐。 有了那个,你会做类似"My {} is not {} in this discussion".format(age, quality)的事情。 因此,对于 90% 的用例,至少使用类型无关的引用无关紧要。 哪个更容易理解,只关心索引。 也命名为"My {age} is not {quality} in this discussion".format(age=my_age_var, quality=my_quality_var)之类的引用。 现在,如果同一个 var 被引用 30 次,您只需在参数中指定一次,并且很容易跟踪、复制和粘贴或删除。 命名参数(作为输入)是我在 Go 中错过的另一个 Python 特性。 但如果需要,我可以没有它。 但在我看到 f-strings(在 Python 3.6 中引入)的那一刻,我再次爱上了它。 字符串插值总是让我的生活更轻松。

@runeimp您能否发布这些 50 多个变量字符串插值之一的示例(以您拥有的任何语言)? 我认为有问题代码的实际示例可能有助于讨论。

好的,这不是最终代码,只是一个简单的示例,只有 50 个引用,并且变量名已更改以保护无辜。

转到 fmt.Sprintf

func (dt DataType) String() string {
    MMMCodeTail := " "
    if len(pn.MMMCode) > 0 {
        MMMCodeTail = "\n\t"
    }

    MMMVCTail := " "
    if len(pn.MMMVoipCommunication) > 0 {
        MMMVCTail = "\n\t"
    }

    MMMCCTail := " "
    if len(pn.MMMCombatConditions) > 0 {
        MMMCCTail = "\n\t"
    }

    MMMSRTail := " "
    if len(pn.MMMSecurityReporting) > 0 {
        MMMSRTail = "\n\t"
    }

    MMMLKTail := " "
    if len(pn.MMMLanguagesKnown) > 0 {
        MMMLKTail = "\n\t"
    }

    return fmt.Sprintf(`ThingID=%6d, ThingType=%q, PersonID=%d, PersonDisplayName=%q, PersonRoomNumber=%q,
    DateOfBirth=%s, Gender=%q, LastViewedBy=%q, LastViewDate=%s,
    SaleCodePrior=%q, SpecialCode=%q, Factory=%q,
    Giver=%s, Manager=%q, ServiceDate=%s, SessionStart=%s, SessionEnd=%s, SessionDuration=%d,
    HumanNature=%q, VRCatalog=%v, AdditionTime=%d, MeteorMagicMuscle=%v,
    VRQuest=%q, SelfCare=%v, BypassTutorial=%q, MultipleViewsSameday=%v,
    MMMCode=%q,%sMMMVoipCommunication=%q,%sMMMCombatConditions=%q,%sMMMSecurityReporting=%q,%sMMMLanguagesKnown=%q,%sMMMDescription=%q,
    SaleCodeLatest=%q, HonoraryCode=%q, LegalCode=%q, CharacterDebuffs=%q,
    MentalDebuffs=%q, PhysicalDebuffs=%q,
    CharacterChallenges=%q,
    CharacterChallengesOther=%q,
    CharacterStresses=%q,
    RelationshipGoals=%q, RelationshipGoalsOther=%q,
    RelationshipLobsters=%q,
    RelationshipLobstersOther=%q,
    RelationshipLobsterGunslingerDoublePlus=%q,
    RelationshipLobsterGunslingerPlus=%q,
    RelationshipLobsterGunslingerGains=%q,
    PersonAcceptsRecognition=%q,
    PersonAcceptsRecognitionGunslinger=%q,
    BenefitsFromChocolate=%v, DinnerForLovelyWaterfall=%v, ModDinners=%q, ModDinnersOther=%q,
    FlexibleHaystackList=%q, FlexibleHaystackOther=%q,
    ModDiscorseSummary=%q,
    MentallySignedBy=%q, Overlord=%q, PersonID=%d,
    FactoryID=%q, DeliveryDate=%s, ManagerID=%q, ThingReopened=%v`,
        dt.ThingID,
        dt.ThingType,
        dt.PersonID,
        dt.PersonDisplayName,
        // dt.PersonFirstName,
        // dt.PersonLastName,
        dt.PersonRoomNumber,
        dt.DateOfBirth,
        dt.Gender,
        dt.LastViewedBy,
        dt.LastViewDate,
        dt.SaleCodePrior,
        dt.SpecialCode,
        dt.Factory,
        dt.Giver,
        dt.Manager,
        dt.ServiceDate,
        dt.SessionStart,
        dt.SessionEnd,
        dt.SessionDuration,
        dt.HumanNature,
        dt.VRCatalog,
        dt.AdditionTime,
        dt.MeteorMagicMuscle,
        dt.VRQuest,
        dt.SelfCare,
        dt.BypassTutorial,
        dt.MultipleViewsSameday,
        dt.MMMCode, MMMCodeTail,
        dt.MMMVoipCommunication, MMMVCTail,
        dt.MMMCombatConditions, MMMCCTail,
        dt.MMMSecurityReporting, MMMSRTail,
        dt.MMMLanguagesKnown, MMMLKTail,
        dt.MMMDescription,
        dt.SaleCodeLatest,
        dt.HonoraryCode,
        dt.LegalCode,
        dt.CharacterDebuffs,
        dt.MentalDebuffs,
        dt.PhysicalDebuffs,
        dt.CharacterChallenges,
        dt.CharacterChallengesOther,
        dt.CharacterStresses,
        dt.RelationshipGoals,
        dt.RelationshipGoalsOther,
        dt.RelationshipLobsters,
        dt.RelationshipLobstersOther,
        dt.RelationshipLobsterGunslingerDoublePlus,
        dt.RelationshipLobsterGunslingerPlus,
        dt.RelationshipLobsterGunslingerGains,
        dt.PersonAcceptsRecognition,
        dt.PersonAcceptsRecognitionGunslinger,
        dt.BenefitsFromChocolate,
        dt.DinnerForLovelyWaterfall,
        dt.ModDinners,
        dt.ModDinnersOther,
        dt.FlexibleHaystackList,
        dt.FlexibleHaystackOther,
        dt.ModDiscorseSummary,
        dt.MentallySignedBy,
        dt.Overlord,
        dt.PersonID,
        dt.FactoryID,
        dt.DeliveryDate,
        dt.ManagerID,
        dt.ThingReopened,
    )
}

一个潜在的 F 弦示例

func (dt DataType) String() string {
    MMMCodeTail := " "
    if len(pn.MMMCode) > 0 {
        MMMCodeTail = "\n\t"
    }

    MMMVCTail := " "
    if len(pn.MMMVoipCommunication) > 0 {
        MMMVCTail = "\n\t"
    }

    MMMCCTail := " "
    if len(pn.MMMCombatConditions) > 0 {
        MMMCCTail = "\n\t"
    }

    MMMSRTail := " "
    if len(pn.MMMSecurityReporting) > 0 {
        MMMSRTail = "\n\t"
    }

    MMMLKTail := " "
    if len(pn.MMMLanguagesKnown) > 0 {
        MMMLKTail = "\n\t"
    }

    return fmt.Print(F`ThingID={dt.ThingID}, ThingType={dt.ThingType}, PersonID={dt.PersonID}, PersonDisplayName={dt.PersonDisplayName}, PersonRoomNumber={dt.PersonRoomNumber},
    DateOfBirth={dt.DateOfBirth}, Gender={dt.Gender}, LastViewedBy={dt.LastViewedBy}, LastViewDate={dt.LastViewDate},
    SaleCodePrior={dt.SaleCodePrior}, SpecialCode={dt.SpecialCode}, Factory={dt.Factory},
    Giver={dt.Giver}, Manager={dt.Manager}, ServiceDate={dt.ServiceDate}, SessionStart={dt.SessionStart}, SessionEnd={dt.SessionEnd}, SessionDuration={dt.SessionDuration},
    HumanNature={dt.HumanNature}, VRCatalog={dt.VRCatalog}, AdditionTime={dt.AdditionTime}, MeteorMagicMuscle={dt.MeteorMagicMuscle},
    VRQuest={dt.VRQuest}, SelfCare={dt.SelfCare}, BypassTutorial={dt.BypassTutorial}, MultipleViewsSameday={dt.MultipleViewsSameday},
    MMMCode={dt.MMMCode},{MMMCodeTail}MMMVoipCommunication={dt.MMMVoipCommunication},{MMMVCTail}MMMCombatConditions={dt.MMMCombatConditions},{MMMCCTail}MMMSecurityReporting={dt.MMMSecurityReporting},{MMMSRTail}MMMLanguagesKnown={dt.MMMLanguagesKnown},{MMMLKTail}MMMDescription={dt.MMMDescription},
    SaleCodeLatest={dt.SaleCodeLatest}, HonoraryCode={dt.HonoraryCode}, LegalCode={dt.LegalCode}, CharacterDebuffs={dt.CharacterDebuffs},
    MentalDebuffs={dt.MentalDebuffs}, PhysicalDebuffs={dt.PhysicalDebuffs},
    CharacterChallenges={dt.CharacterChallenges},
    CharacterChallengesOther={dt.CharacterChallengesOther},
    CharacterStresses={dt.CharacterStresses},
    RelationshipGoals={dt.RelationshipGoals}, RelationshipGoalsOther={dt.RelationshipGoalsOther},
    RelationshipLobsters={dt.RelationshipLobsters},
    RelationshipLobstersOther={dt.RelationshipLobstersOther},
    RelationshipLobsterGunslingerDoublePlus={dt.RelationshipLobsterGunslingerDoublePlus},
    RelationshipLobsterGunslingerPlus={dt.RelationshipLobsterGunslingerPlus},
    RelationshipLobsterGunslingerGains={dt.RelationshipLobsterGunslingerGains},
    PersonAcceptsRecognition={dt.PersonAcceptsRecognition},
    PersonAcceptsRecognitionGunslinger={dt.PersonAcceptsRecognitionGunslinger},
    BenefitsFromChocolate={dt.BenefitsFromChocolate}, DinnerForLovelyWaterfall={dt.DinnerForLovelyWaterfall}, ModDinners={dt.ModDinners}, ModDinnersOther={dt.ModDinnersOther},
    FlexibleHaystackList={dt.FlexibleHaystackList}, FlexibleHaystackOther={dt.FlexibleHaystackOther},
    ModDiscorseSummary={dt.ModDiscorseSummary},
    MentallySignedBy={dt.MentallySignedBy}, Overlord={dt.Overlord}, PersonID={dt.PersonID},
    FactoryID={dt.FactoryID}, DeliveryDate={dt.DeliveryDate}, ManagerID={dt.ManagerID}, ThingReopened={dt.ThingReopened}`,
    )
}

在接下来的几年中,您希望每月维护和添加、更新、删除这两者中的哪一个?

我可以两个都选吗? 两者在我看来都很可怕。

你的案子不能用fmt.Sprintf("%#v", dt)完成吗? 也就是下单有什么要求? 精确的格式(例如= vs :用于分隔符, %q vs. %v ,...)? 未打印的字段? 换行符?

是其他程序解析输出,还是供人类使用?

使用反射怎么样?

v := reflect.ValueOf(dt)
t := v.Type()
for i := 0; i < t.NumField(); i++ {
    f := t.Field(i)
    s += fmt.Sprintf("%s=%v", f.Name, v.Field(i))
    if t.Field(i).Type.Kind() == reflect.String && v.Field(i).Len() > 0 {
        s += "\n\t"
    } else {
        s += " "
    }
}

对于这个非常具体的示例,这两个选项都是合理的。 但这不会发生更复杂的事情。 或者从字面上看其他任何东西。 就像在一个字符串中多次使用相同的引用一样。 或者这些值不是单个结构或映射的一部分,而是在同一范围内生成的。 这只是一个封闭的例子。 不幸的是,我无法显示任何真实代码。 但实际上只要看看上面有数百个单词的任何网页。 打开源代码视图并想象 50 多个单词需要是动态的。 现在想象这不是针对网页的,出于任何其他原因,您不需要模板系统,但为了解决这个问题,所有变量已经在代码的其他部分和/或多个地方使用的范围内在此代码块中,此文本可能每 3 到 4 周以有时很大且非常重要的方式更改。 这可能是系统的一部分,_有时_需要生成任何或全部 PDF、CSV、SQL INSERT、电子邮件、API 调用等。

如果没有真实世界的例子,真的很难判断这个提议是否是解决问题的正确方法。 也许字符串插值确实是解决方案,或者我们遇到了XY 问题。 也许正确的解决方案已经在某处的语言中,或者解决方案是对包反射的更改,或者可能是包文本/模板。

首先,我怀疑这对许多人来说是否是一个重要问题。 这是我能记得看到的唯一严肃的讨论,表情符号投票并不意味着压倒性的支持。 第三方库也不多。

其次,值得指出的是,即使 fmt.Printf (和家庭)也可以重复参数:

fmt.Printf("R%s T%[1]s T%[1]s was a canine movie star\n", "in")

第三,我认为在其他语言中使用过字符串插值的人不一定希望在 Go 中看到它。 我以前在几种语言中使用过它,虽然当你想插入简单变量时它很好,但只要你尝试使用更复杂的表达式、函数调用、转义标志字符等,然后为所有这些添加格式细节(这不可避免地意味着“printf”风格的方法)整个事情很快就会变得难以理解。

最后,在我看来,当您有大量变量要插值时, text/template是最好的方法,因此我们应该寻找更方便使用变量的方法不是明确定义的结构的一部分,而不是将这种复杂性的东西集成到语言本身中。

或者,构建一个 DSL:

func (dt *DataType) String() string {
    var s strings.Builder
    _ = fields.Format(&s, 
        fields.PaddedInt("ThingID", 6, dt.ThingID),
        fields.String("ThingType", dt.ThingType),
        fields.String("PersonID", dt.PersonID),
        fields.Time("DateOfBirth", dt.DateOfBirth),
        ...
    )
    return s.String()
}

要么

func (dt *DataType) String() string {
    var s fields.Builder
    s.PaddedInt("ThingID", 6, dt.ThingID),
    s.String("ThingType", dt.ThingType),
    s.String("PersonID", dt.PersonID),
    s.Time("DateOfBirth", dt.DateOfBirth),
    return s.String()
}

我可以深刻理解@ianlancetaylor的观点。 我们知道我们可以像往常一样接受fmt. Printf

同时,我完全同意@alanfo观点

更容易阅读和修改,更不容易出错

这对于健壮的系统编程非常重要。

我认为采用其他语言的功能/想法一点也不丢人,因为它可以节省很多人的宝贵时间。 实际上,很多人都遭受字符串插值问题的困扰。
其他现代语言采用字符串插值是有原因的。

我不希望 Go 仍然比 C 语言好一点。
我认为 Go v2 是一个让 Go 变得更好的机会。

image

恕我直言,如果提供了字符串插值功能,我们大多数人都会选择使用它。 我们能感觉到。

附言。
不幸的是, @runeimp的情况太极端了。 我能感受到环境的痛苦。 但它模糊了提案的想法。 (没有冒犯的意思)

@doortts没有冒犯。 我总是欣赏诚实的谈话。

我知道发布真实代码可能会削弱争论。 而且我也知道,发布一个需要一点创造力来扩展精神的近似值可能只会使讨论变得混乱。 所以我赌了一把,因为我不希望其他人这样做。 但是共享真实代码不是一种选择,并且通过一个具体的示例将其以黑白方式显示所需的时间,但除此之外,我对它的使用为零,这只是我现在没有精力去做的事情。 由于我之前已经说过的原因,我已经很清楚这个论点是困难的。 我知道我不是看到此功能真正价值的十几个人中的一个。 但其中大多数几乎肯定离这条线还很远。 我来这里只是因为我碰巧在 Reddit 上看到了一篇关于它的帖子。 否则我几乎完全不知道这个过程。 我对 Go 的进展情况感到非常满意,而无需参与对话。 但我想帮助这个讨论,因为我知道不会有很多声音为它而战,这仅仅是因为每当建议对语言进行任何更改时都需要克服大量的惯性。 我想我从来没有见过如此强烈地反对改变一种语言。 这使得发帖支持新事物变得非常令人生畏。 我并不是说任何语言都应该接受所有新功能请求而无需审查。 只是在支持方面看起来很安静的原因并不总是因为没有欲望。 我要求任何对此功能感兴趣的人发布_something_。 任何东西,所以我们可以看到一些实数。

我认为因为是假期,所以总体上关注度较低。 从开发人员的角度来看,字符串插值非常有用和需要(这就是为什么它在这么多其他语言中),但似乎这个提议在这一点上对于如此大的变化来说还不够成熟,如果被拒绝,我不认为它意味着以后不值得重温。 使这一点显而易见的小的增量更改尚未到位。

目前,我认为不可能从范围内通过名称动态解析变量(尽管 yaegi Eval 似乎可以做到这一点?)。

我认为@runeimp引用的帖子是我在 r/golang https://www.reddit.com/r/golang/comments/d1199a/why_is_there_no_equivalent_to_f_strings_in_python/中的帖子
哪里有更多的讨论。

我只是想把我的帽子扔进戒指,并说变量插值对我有很大帮助,来自 python 世界的 fmt.sprintf 之类的东西让 Go 看起来非常笨重且难以阅读/维护。

Go 非常优雅,它使做它重视的事情变得非常简单和强大。 可读性和可维护性是其哲学的一些核心租户,阅读字符串并知道其中的内容真的不应该如此困难。 理解将要打印的内容的同时要记住哪些变量映射到该字符串末尾的列表中的哪些项目,会产生非常重要的开销。 我们根本不需要维持这种精神开销。

如果人们觉得 fmt.sprintf 适合他们,可以使用它。 我不认为这两种语言都会损害 Go 作为一门语言,因为该提案无疑比当前的方法更易读、直观且更易于维护。 这不需要通过 50 多个变量的插值来证明,这是一种改进,即使只有一个变量的插值。

是这样的

fmt.sprintf("I am %<type name here>{age} years old.")

# or
fmt.sprintf("I am %T{age} years old")

真的对语言有破坏性的前景吗? 我很高兴被证明是错误的,但老实说,我只看到了这个(或类似的)提案的好处,除了向后不兼容,这就是在新的 Go 的主要版本中实现这个是有意义的

是否可以考虑让这个讨论再开放一组 4 周,因为当提出截止日期时,社区中有相当一部分人正在感恩节、圣诞节和新年度假?

https://github.com/golang/go/issues/34174#issuecomment -558844640 以来,已经有更多的讨论,所以我把这个从最后的评论期收回。

我倾向于同意@randall77的观点,我还没有在这里看到一个令人信服的例子。 @runeimp ,感谢您发布示例代码,但无论哪种方式似乎都难以阅读且难以更改。 正如@egonelbre建议的那样,如果我们想让它更易于维护,第一步似乎是找到一种完全不同的方法。

@cyclingwithelephants fmt.sprintf("I am %T{age} years old")不在此处。 该语言不提供fmt.Sprintf可以用来解析age的任何机制。 Go 是一种编译语言,执行时没有局部变量名。 如果我们能想出某种方法来使这项工作发挥作用,这可能会更容易接受,也许是按照上面@bradfitz的建议。

感谢@ianlancetaylor解除了最后评论期的标签。 😀

我认为@bradfitz的想法很棒。 我猜想无论没有本地变量上下文,都有潜在的限制,但我很乐意接受这些限制,而不是没有字符串插值。 虽然我非常尊重 Go 中百分比格式的更新(我喜欢%q%v%#v的添加),但这种范式是古老的。 仅仅因为它是可敬的并不意味着它是最好的做事方式。 就像 Go 处理编译的方式一样,尤其是交叉编译比使用 C 或 C++ 完成的方式更好。 现在,Go 是否只是用健全的默认值隐藏了所有令人讨厌的编译器选项,而且它在幕后也同样丑陋。 我不知道具体,但我相信是这样的。 这很好。 这对我来说是完全可以接受的。 我不在乎编译器为使该功能起作用而做了什么黑暗仪式。 我只知道该功能使生活更轻松。 作为开发人员,我使用的每一种支持它的语言都让我的生活变得更轻松。 在不支持它的语言中总是很痛苦。 对于我需要的 98% 的字符串格式,它比如何在百分比格式中使用 30 多个特殊字符更容易记住。 并且说使用%v而不是“正确的百分比格式”与创建的易用性不同,甚至关闭了相同的维护易用性。

现在有更多时间,我将研究一个示例,看看我是否能找到一些比我以前更有启发性的文章,以帮助说明我们这些处理人机交互的人的工作效率的显着好处,文档和代码生成,以及定期的字符串操作。

这是一个可能导致可实施的想法的想法。 虽然我不知道这是个好主意。

添加一个新的字符串类型m"str" (可能与原始字符串相同)。 这种新的字符串文字的计算结果为map[string]interface{} 。 在地图中查找空字符串会为您提供字符串文字本身。 字符串文字可能包含大括号中的表达式。 大括号中的表达式被评估为好像它不在字符串文字中,并且值存储在映射中,键是出现在大括号中的子字符串。

例如:

    i := 1
    m := m"twice i is {i * 2}"
    fmt.Println(m[""])
    fmt.Println(m["i * 2"])

这将打印

twice i is {i * 2}
2

在字符串字面量中,大括号可以用反斜杠转义以表示一个简单的大括号。 不带引号的、不匹配的大括号是编译错误。 如果大括号内的表达式无法编译,也是编译错误。 表达式的计算结果必须恰好是一个值,否则不受限制。 同一个大括号字符串可能在字符串文字中出现多次; 它将被评估多次,但只有一个评估将存储在地图中(因为它们都将具有相同的键)。 具体存储哪一个是未指定的(如果表达式是函数调用,这很重要)。

就其本身而言,这种机制很奇特但毫无用处。 它的优点是可以明确指定并且可以说不需要对语言进行过多的添加。

使用附带附加功能。 新函数fmt.Printfm的工作方式与fmt.Printf完全相同,但第一个参数不是string而是map[string]interface{} 。 地图中的""值将是一个格式字符串。 除了通常的%之外,格式字符串还支持新的{str}修饰符。 使用此修饰符意味着将在映射中查找str而不是使用参数作为值,并且将使用该值。

例如:

    hi := "hi"
    fmt.Printfm(m"%20{hi}s")

将打印传递给 20 个空格的字符串hi

自然会有更简单的fmt.Printm来代替每个大括号表达式fmt.Print打印的包含值。

例如:

    i, j := 1, 2
    fmt.Printm(m"i: {i}; j: {j}")

将打印

i: 1; j: 2

这种方法的问题:在字符串文字前奇怪地使用了m前缀; 正常使用中重复的m ——括号前一个,括号后一个; 当不与期望一个函数一起使用时,m 字符串的一般无用性。

优点:不太难指定; 支持简单插值和格式化插值; 不限于 fmt 函数,因此可以使用模板或意外使用。

如果它是一个类型化映射(例如 runtime.StringMap),那么我们可以使用 fmt.Print 和 Println 而无需添加口吃的 Printfm

使用定义的类型是个好主意,但对fmt.Printfm没有帮助; 我们不能使用定义的类型作为fmt.Printf的第一个参数,因为它只需要一个string

@runeimp在线程前面提到但尚未完全讨论的一件事是os.Expand :-

package main

import (
    "fmt"
    "os"
)

func main() {
    name := "foo"
    days := 12.312
    type m = map[string]string
    f := func(ph string) string {
        return m{"name": name, "days": fmt.Sprintf("%2.1f", days)}[ph]
    }
    fmt.Println(os.Expand("The gopher ${name} is ${days} days old.", f))
    // The gopher foo is 12.3 days old.
}

虽然这对于简单的情况来说太冗长了,但当您有大量要插入的值时(尽管任何方法都存在 70 个值的问题!),它会更容易接受。 优点包括:-

  1. 如果对映射函数使用闭包,它可以很好地处理局部变量。

  2. 它还可以很好地处理任意格式,并将其排除在插值字符串本身之外。

  3. 如果在映射函数中不存在的插值字符串中使用占位符,它会自动替换为空字符串。

  4. 对映射函数的更改相对容易。

  5. 我们已经有了它——不需要更改语言或库。

@ianlancetaylor这个解决方案对我来说听起来像是一个不错的选择。 虽然我不明白为什么我们需要替代的 Print 方法。 我可能会忽略一些东西,但似乎使用interface{}和类型检查进行了简单的签名更改。 好的,刚刚意识到签名更改对于某些现有代码可能会带来很大的问题。 但是如果实现了基本机制并且我们还创建了一个stringlit类型,它代表stringm"str"或者如果m"str"也接受string这对于 Go v2 来说是一个可以接受的突破性变化吗? 它们都是“字符串文字”,只是其中一个本质上具有允许额外功能的标志,不是吗?

感谢@alanfo再次提出这个问题,这些都是很好的观点。 😃

我已经将os.Expand用于光模板,它在您需要构建值映射的情况下非常方便。 但是,如果不需要地图,并且您需要在几个不同的区域进行关闭以捕获您的(现在已复制多次)替换函数的局部变量,则完全忽略 DRY 并且可能会导致维护问题,只需添加更多的工作是插值字符串将“正常工作”,缓解这些维护问题,并且不需要构建地图来管理该动态字符串。

@runeimp我们无法更改fmt.Printf的签名。 这会破坏 Go 1 的兼容性。

stringlit类型的概念意味着改变 Go 的类型系统,这是一个更大的问题。 Go 有意有一个非常简单的类型系统。 我不认为我们想为这个功能复杂化。 即使我们这样做了, fmt.Printf仍然会采用string参数,并且我们无法在不破坏现有程序的情况下更改它。

@ianlancetaylor感谢您的澄清。 我很欣赏不破坏与fmt包或类型系统等基本功能的向后兼容性的愿望。 我只是希望可能存在一些隐藏的(对我而言)可能性,这可能是沿着这些路线的一种选择。 👼

我真的很喜欢 Ian 的实现方式。 泛型不会帮助解决fmt.Print问题吗?

contract printable(T) {
  T string, map[string]string // or the type Brad suggested "runtime.StringMap"
}

// And then change the signature of fmt.Print to:
func Print(type T printable) (str T) error { 
  // ...
}

这样,应该保留 Go 1 的兼容性。

对于 Go 1 的兼容性,我们根本无法更改函数的类型。 函数不仅被调用。 它们也用于代码中,例如

    var print func(...interface{}) = fmt.Print

人们在制作函数表或使用手动依赖注入进行测试时会编写这样的代码。

我感觉strings.Replacer(https://golang.org/pkg/strings/#Replacer)几乎可以做字符串插值,只是缺少插值标识符(例如${...})和模式处理(例如如果var i int = 2 , "${i+1}" 应该在替换器中映射到 "3")

另一种方法将有一个内置函数,例如 format("I am a %(foo)s %(bar)d") 扩展为 fmt.Sprintf("I am a %s %d", foo,酒吧)。 至少,这是完全向后兼容的,FWIW。

从语言设计的角度来看,将内置函数扩展为对标准库中函数的引用是很奇怪的。 为了为语言的所有实现提供清晰的定义,语言规范必须完全定义fmt.Sprintf的行为。 我认为我们想避免这种情况。

这可能不会让每个人都开心,但我认为以下是最普遍的。 它分为三个部分

  1. fmt.Printm 函数采用格式字符串和map[string]interface{}
  2. 接受 #12854 所以你可以在调用它时删除map[string]interface{}
  3. 允许地图文字中的未键入名称作为"name": name,"qual.name": qual.name,的简写

放在一起,这将允许类似

fmt.Printm("i: {i}; j: {j}", {i, j})
// which is equivalent to
fmt.Printm("i: {i}; j: {j}", map[string]interface{}{
  "i": i,
  "j": j,
})

这仍然在格式字符串和参数之间存在重复,但它在页面上要轻得多,而且它是一种易于自动化的模式:编辑器或工具可以根据字符串和编译器自动填充{i, j}如果它们不在范围内,会通知您。

这不会让你在格式字符串中进行计算,这可能很好,但我已经看到过分多次认为它是一种奖励。

由于它通常适用于地图文字,因此可以在其他情况下使用。 我经常以我正在构建的地图中的键来命名我的变量。

这样做的一个缺点是它不能应用于结构,因为它们可以被取消键控。 这可以通过在 $ {:i, :j}这样的名称之前要求:来纠正,然后你可以这样做

Field2 := f()
return aStruct{
  Field1: 2,
  :Field2,
}

我们需要任何语言支持吗? 就像现在一样,它可以看起来像这样,无论是使用地图类型还是使用流畅、类型更安全的 API:

package main

import (
    "fmt"
    "strings"
)

type V map[string]interface{}

func Printm(format string, args V) {
    for k, v := range args {
        format = strings.ReplaceAll(format, fmt.Sprintf("{%s}", k), fmt.Sprintf("%v", v))
    }
    fmt.Print(format)
}

type Buf struct {
    sb strings.Builder
}

func Fmt(msg string) *Buf {
    res := Buf{}
    res.sb.WriteString(msg)
    return &res
}

func (b *Buf) I(val int) *Buf {
    b.sb.WriteString(fmt.Sprintf("%v", val))
    return b
}

func (b *Buf) F(val float64) *Buf {
    b.sb.WriteString(fmt.Sprintf("%v", val))
    return b
}

func (b *Buf) S(val string) *Buf {
    b.sb.WriteString(fmt.Sprintf("%v", val))
    return b
}

func (b *Buf) Print() {
    fmt.Print(b.sb.String())
}

func main() {
    Printm("Hello {k} {i}\n", V{"k": 22.5, "i": "world"})
    Fmt("Hello ").F(22.5).S(" world").Print()
}

https://play.golang.org/p/v9mg5_Wf-qD

好吧,它仍然效率低下,但看起来制作一个支持它的包根本不需要太多工作。 作为奖励,我包含了一个不同的流体 API,可以说它也可以模拟插值。

@ianlancetaylor的“映射字符串”提议(虽然我个人更喜欢使用 av“...” 语法的“值/变量字符串”)也允许非格式化用例。 例如,#27605(运算符重载函数)在很大程度上存在,因为今天很难为math/big和其他数字库制作可读的 API。 该提案将允许该功能

func MakeInt(expression map[string]interface{}) Int {...}

用作

a := 5
b := big.MakeInt(m"100000")
c := big.MakeInt(m"{a} * ({b}^2)")

重要的是,这个辅助函数可以与当前存在的性能更高、功能更强大的 API 共存。

这种方法允许库对大型表达式执行它想要的任何优化,并且对于其他 DSL 也可能是一种有用的模式,因为它允许自定义表达式解析,同时仍将值表示为 Go 变量。 值得注意的是,Python 的 f 字符串不支持这些用例,因为对封闭值的解释是由语言本身强加的。

@HALtheWise谢谢,这很整洁。

我想从一般开发人员的立场发表评论,以表示对这个提议的一点支持。 我已经专业地使用 golang 编码超过 3 年。 当我搬到 golang(来自 obj-c/swift)时,我很失望没有包括字符串插值。 我过去使用 C 和 C++ 已有十多年了,所以 printf 并没有特别的调整,除了感觉有点倒退——我发现它确实对代码维护和可读性产生了影响对于更复杂的字符串。 我最近做了一点 kotlin(用于 gradle 构建系统),使用字符串插值是一股新鲜空气。

我认为字符串插值可以使字符串组合对于语言新手来说更加平易近人。 由于减少了阅读和编写代码时的认知负担,这也是技术用户体验和维护的胜利。

我很高兴这个提议得到了真正的考虑。 我期待提案得到解决。 =)

如果我理解正确, @ianlancetaylor的提议是:

i := 3
foo := m"twice i is %20{i * 2}s :)"
// the compiler will expand to:
foo := map[string]interface{}{
    "": "twice i is %20{i * 2}s :)",
    "i * 2": 6,
}

之后,打印函数将处理该映射,再次解析整个模板,并利用预解析模板的一些优势

但是如果我们将 m"str" 扩展为一个函数呢?

i := 3
foo := m"twice i is %20{i * 2}s :)"
// the compiler will expands to:
foo := m(
    []string{"twice i is ", " :)"}, // split string
    []string{"%20s"},               // formatter for each value
    []interface{}{6},               // values
)

此函数具有以下签名:

func m(strings []string, formatters []string, values []interface{}) string {}

这个函数会表现得更好,因为为了更好地利用预解析的模板,并且可以像 Rust 对println!函数所做的那样做更多的优化。

我在这里试图描述的与 Javascript 的标记函数非常相似,我们可以讨论编译器是否应该接受用户函数来格式化字符串 ex:

foo.GQL"query { users{ %{expectedFields} } }"

bla.SQL`SELECT *
    FROM ...
    WHERE FOO=%{valueToSanitize}`

@rodcorsi如果我正确阅读了您的建议,则需要将fmt.Printf格式正确地构建到语言中,因为编译器必须了解%20s的开始和结束位置。 这是我试图避免的事情之一。

另请注意,我的建议与fmt.Printf格式无关,也可用于其他类型的插值。

我反对将m"..."视为对函数调用的扩展,因为它掩盖了实际发生的情况,并为函数调用添加了有效的第二种语法。 传递比映射更结构化的表示通常似乎是合理的,以避免在任何地方都需要匹配重新实现解析行为。 也许是一个简单的结构体,其中包含一个常量字符串部分、一个大括号中的字符串切片和一个接口切片?

m"Hello {name}" -> 
struct{...}{
    []string{"Hello ", ""},
    []string{"name"},
    []interface{}{"Gopher"}

第二个和第三个切片的长度必须相同,第一个切片必须更长。 还有其他方法可以表示这一点,也可以在结构上对该约束进行编码。
与直接公开原始字符串的格式相比,它的优点是在使用它的函数中有一个正确且高性能的解析器的要求更宽松。 如果不支持转义字符或嵌套的 m 字符串,这可能没什么大不了的,但我宁愿不需要重新实现和测试该解析器,并且缓存它的结果可能会导致运行时内存泄漏。

如果“格式化选项”是使用这种语法的常见需求,我可以在规范中看到它们的位置,但我个人会使用像m"{name} is {age:%.2f} years old"这样的语法,编译器只是传递所有内容在:之后进入函数。

您好,我想对此发表评论以增加对该提案的支持。 在过去的 5 年里,我一直在使用许多不同的语言(Kotlin、Scala、Java、Javascript、Python、Bash、一些 C 等),现在我正在学习 Go。

我认为字符串插值在任何现代编程语言中都是必须的,就像类型推断一样,我们在 Go 中也有。

对于那些争论你可以用 Sprintf 完成同样事情的人,那么,我不明白为什么我们在 Go 中有类型推断,你可以通过编写类型来完成同样的事情,对吧? 嗯,是的,但这里的重点是字符串插值减少了完成该操作所需的大量冗长,并且更易于阅读(使用 Sprintf,您必须来回跳转到参数列表和字符串才能理解字符串)。

在现实生活中的软件中,这是一个非常受欢迎的功能。

是不是违背了 Go 的极简设计? 不,它不是一个允许你做疯狂的事情或使你的代码复杂化的抽象(如继承)的功能,它只是一种在阅读代码时减少编写并增加清晰度的方法,我相信这并不违背 Go 的宗旨尝试做(我们有类型推断,我们有 := 运算符等)。

为具有静态类型的语言正确格式化

Haskell 具有用于字符串插值的库和语言扩展。 这不是类型的东西。

此页面是否有帮助?
0 / 5 - 0 等级