如何在 golang 中上下对齐打印字符串

本文最后更新于:2023年2月2日 下午

在一些场景中,我们需要像表格一样整齐地打印一些信息,比如一个人的姓名,家庭地址和联系方式,我希望打印的格式像下面这样:

1
2
3
Name    : Bob
Address : New York Avenue
Phone : 12345674567

这个问题听上去非常简单,简单到似乎不该成为一个问题,我们很容易地给出下面这段代码:

1
2
3
4
5
6
7
8
information := map[string]string{
"Name": "Bob",
"Address": "New York Avenue",
"Phone": "12345674567",
}
for k, v := range information {
fmt.Printf("%-10s: %s\n", k, v)
}

效果如下:

1
2
3
Name      : Bob
Address : New York Avenue
Phone : 12345674567

好,下面我们希望打印的信息中可能参杂一些中文字符,使用相同的代码去打印:

1
2
3
4
Name      : Bob
Address : New York Avenue
Phone : 12345674567
国籍 : 无国籍

可以看到不这么对齐了,这实在让人有点困惑。

而在我们继续往下之前,对 golang 比较熟悉的人应该都知道 character 和 byte 的区别,golang 之父 Rob Pike 也在这篇文章[1]中比较详尽地解释了差别。

对于一个中文字符,使用len()实际上计算的是其 byte 长度

1
2
fmt.Printf("the length of a is %d\n", len("a"))		// the length of a is 1
fmt.Printf("the length of 我 is %d\n", len("我")) // the length of 我 is 3

而如果想要统计字符即 character 的数量,golang 提供了utf8.RuneCountInString

1
2
3
text := "你好,世界"
fmt.Printf("the length of %s is %d\n", text, utf8.RuneCountInString(text))
// the length of 你好,世界 is 5

有没有可能fmt在计算中文这样的字符的长度时有问题呢?我们可以看一下fmt的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// padString appends s to f.buf, padded on left (!f.minus) or right (f.minus).
func (f *fmt) padString(s string) {
if !f.widPresent || f.wid == 0 {
f.buf.writeString(s)
return
}
width := f.wid - utf8.RuneCountInString(s)
if !f.minus {
// left padding
f.writePadding(width)
f.buf.writeString(s)
} else {
// right padding
f.buf.writeString(s)
f.writePadding(width)
}
}

fmt在统计长度时使用的同样是utf8.RuneCountInString,那么问题只可能出现在打印的效果上,在这里就需要区分两个概念,一个是character length,另一个则是displayed width,同样是一个长度的字符,中文的i占的宽度并不相同,详细地可以参考一下 UAX 的 report[2]

而为了能够整齐地打印就需要有办法统计字符串的宽度,在 tablewritergo-pretty 这些打印 table 的开源库中在计算 padding 时都用到了 go-runewidth 这个库,我们试着使用它改写一下我们的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
information := map[string]string{
"Name": "Bob",
"Address": "New York Avenue",
"Phone": "12345674567",
"国籍": "无国籍",
}
for k, v := range information {
kWid := runewidth.StringWidth(k)
if kWid <= 10 {
k += strings.Repeat(" ", 10-kWid)
}
fmt.Printf("%s: %s\n", k, v)
}

It works like a charm!

当然,也正如 stackoverflow 一篇回答[3]评论中提到的那样

runes do not have a “pixel width”, the font does. Therefore the answer will depend on the tool/package you’re using to render the font.

有的时候显示长度会取决于字体的设计,不过 go-runewidth 在大部分情况都可以工作得很好。

参考


如何在 golang 中上下对齐打印字符串
https://flaglord.com/2023/02/02/如何在-golang-中上下对齐打印字符串/
作者
flaglord
发布于
2023年2月2日
许可协议