5

Go字符串比较,终于有人讲清楚了

 2 years ago
source link: https://tonybai.com/2022/04/18/inside-go-string-comparison/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go字符串比较,终于有人讲清楚了

本文永久链接 – https://tonybai.com/2022/04/18/inside-go-string-comparison

西娅(Thea)是一个刚刚入门Go语言的妹子程序员,今天她遇到了一个让她“surprise”的问题。下面就是那段让妹子西娅困惑的Go代码:

func main() {
    s1 := "12345"
    s2 := "2"
    fmt.Println(`"12345" > "2":`, s1 > s2) // false

    s3 := "零"
    s4 := "一"
    s5 := "二"

    fmt.Println(`"一" > "零":`, s4 > s3) // false
    fmt.Println(`"二" > "零":`, s5 > s3) // false
    fmt.Println(`"二" > "一":`, s5 > s4) // true
}

在这段关于Go字符串比较的代码中:

  • 为什么表达式”12345″ > “2″的求值结果是false呢?
  • 为什么”一” > “零”和”二” > “零”两个表达式的求值结果都是false呢?
  • 而”二” > “一”的求值结果却又为true呢?

四个结果都让西娅百思不得其解!于是西娅在网络上寻找能为其解惑的Go技术资料。

她网上看到一本名为《Go语言精进之路》的“小黄书”,据说这本书中有有关Go字符串原理与字符串比较的详细讲解。

西娅不经意间瞥见,旁边的同事Tony桌上摆着一本黄色的、厚重的书,这不正是她想看的吗!于是西娅向Tony发出了借书一阅的请求。Tony面对“美女攻势”向来是“每战必败”的,于是西娅顺利地拿到了两卷本的《Go语言精进之路》。借午休时间,西娅花了1.5个小时认真学习了书中有关Go字符串的三个章节:第15节的“了解string实现原理和高效使用”、 第52节的“掌握字符集的原理和字符编码方案间的转换”和第56节的“掌握bytes包和strings包的基本操作”。看完后大呼Wonderful!书中的讲解完全解答了西娅的问题。

此时西娅想起了在《Go语言第一课专栏》的结课语《和你一起迎接Go的黄金十年》中作者关于学习Go语言方法的建议:输出大法!通过输出将学到的知识真正内化为自己的知识,于是西娅将自己对书中内容的理解记录了下来。恰好此时旁边的Tony刚刚从午睡中苏醒过来,西娅决定再为一把人师。Tony就这样被稀里糊涂地拽了过来充当学生:)。

以下是西娅的讲解。


1. Go语言中的字符串类型

字符串类型是现代编程语言中最常使用的数据类型之一。在Go语言的先祖之一C语言当中,字符串类型并没有被显式定义,而是以字符串字面值
常量或以’\0′结尾的字符类型(char)数组来呈现的。

Go语言修复了C语言的这一“缺陷”,原生内置了string类型,统一了对“字符串”的抽象。在Go语言中,无论是字符串常量、字符串变量或是代码中出现的字符串字面量,它们的类型都被统一设置为string

Go的string类型设计充分吸取了C语言字符串设计的经验教训,并结合了其他主流语言在字符串类型设计上的最佳实践,最终为Gopher呈现的string类型具有如下功能特点:

  • string类型的数据是不可变的

即一旦声明了一个string类型的标识符,无论是常量还是变量,该标识符所指代的数据在整个程序的生命周期内便无法被更改。

Go string类型支持零值可用的理念。Go字符串无需像C语言中那样考虑结尾’\0′字符,因此其零值为”",长度为0。

  • 获取长度的时间复杂度是O(1)级别

  • 支持各种比较关系操作符:==、!= 、>=、<=、> 和<

鉴于Go string是不可变的,因此如果两个字符串的长度不相同,那么无需比较具体字符串数据,也可以断定两个字符串是不同的;如果长度相
同,则要进一步判断数据指针是否指向同一块底层存储数据。如果相同,则两个字符串是等价的,如果不同,则还需进一步去比对实际的数据内容。至于怎么比较,我接下来会讲。

  • 对非ASCII字符提供原生支持

这一特点就涉及到Go字符串中的字符是什么字符、用什么字符编码的问题了。下面我们就来看看。

2. Go字符串采用的字符集编码

Go语言默认使用Unicode字符集,并采用UTF-8编码方案,Go还提供了rune原生类型来表示Unicode字符。Unicode(万国码/统一码)在1994年发布,它是以收纳人类所有字符为目的的统一字符集。Unicode字符集就是将世界上存在的绝大多数常用字符进行统一排队和编号。比如下面是一个Unicode字符集表的片段:

序号 字符

U+0000 … …

… … … …

U+0031 1

U+0032 2

… … … …

U+4E2D 中

… … … …

U+4EBA 人

… … … …

U+56FD 国

… … … …

U+10FFFF … …

我们看到每个Unicode字符(比如表格里的”1″、”中”等)都有自己的唯一序号,这个序号就叫做字符的码点(code point),Go中的rune类型可用于表示码点。

好了,问题来了!Unicode字符集表格有了,Go是如何在内存中存储这些字符的呢?目前业界有多种存储方案,比如:UTF-32(即4个字节表示每个Unicode字符码点)、UTF-16(使用2个字节或4个字节表示每个Unicode字符码点)以及UTF-8。

UTF-8使用变长度字节对Unicode字符(的码点)进行编码。编码采用的字节数量与Unicode字符在码点表中的序号有关:表示序号(码点)小的字符使用的字节数量就少,表示序号(码点)大的字符使用的字节数量就多

UTF-8编码使用的字节数量从1个到4个不等。前128个与ASCII字符重合的码点(U+0000~U+007F)使用1个字节表示;带变音符号的拉丁文、希腊文、西里尔字母、阿拉伯文等使用2个字节来表示;而东亚文字(包括汉字)使用3个字节表示;其他极少使用的语言的字符则使用4个字节表示。

这样的编码方案是兼容ASCII字符内存表示的,这意味着采用UTF-8方案在内存中表示Unicode字符时,已有的ASCII字符可以被直接当成Unicode字符进行存储和传输,无需做任何改变。相对于UTF-16和UTF-32方案,UTF-8方案的空间利用率也是最高的。并且,utf8解码和编码时,也无需考虑字节序问题。

于是,Go语言使用了Utf8编码方案在内存中存储Unicode字符。

以字符“中”为例,它的码点(序号)为U+4E2D,它在Utf8编码则为“0xE4 0xB8 0xAD”,即在内存中Go实际用三个字节来表示“中”这个Unicode字符。

3. Go字符串比较

上面铺垫了这么些内容,就是为了为字符串比较开道。关于Go字符串比较,Go语言规范中只说了一句话:String values are comparable and ordered, lexically byte-wise。什么意思呢?这句话表达了三个意思:

  • 定性:字符串可比较
  • 定量:字符串是有序的
  • 方法:逐字节

下面我对开篇的例子做逐一说明,首先看下面代码:

s1 := "12345"
s2 := "2"
fmt.Println(`"12345" > "2":`, s1 > s2)

s1和s2两个字符串中的字符都是ASCII字符范畴的,每个字符在内存中的编码都是一个字节。按照Go string比较的原理,我们对s1和s2进行逐字节比较。首先比较s1的第一个字符”1″和s2的第一个字符”2″。字符”2″在内存中的字节为0×32,而字符”1″在内存中的字节为0×31,显然0×32大于0×31,到这里已经比出大小了,程序不会继续对后续的字符进行比对了。这也是为什么s1 > s2这个表达式为false的原因。

如果s2 = “12346″呢?那么按照Go string比较的原理,程序在比较s1和s2的前4个字符时都相等,于是只能由第5个字符来判定两个字符串的大小了,s2的第五个字符”6″显然大于s1的第五个字符”5″,于是当s2=”12346″时,s2是大于s1的。

我们再看看含有汉字的字符串的例子:

s3 := "零"
s4 := "一"
s5 := "二"

fmt.Println(`"一" > "零":`, s4 > s3) // false
fmt.Println(`"二" > "零":`, s5 > s3) // false
fmt.Println(`"二" > "一":`, s5 > s4) // true

为了方便后续说明,我们先把”零”、”一”和”二”这三个汉字的Utf8编码计算出来:

  • “零”的UTF8编码为:0xE9 0x9B 0xB6
  • “一”的UTF8编码为:0xE4 0xB8 0×80
  • “二”的UTF8编码为:0xE4 0xBA 0x8C

我们看到,三个汉字的Utf8编码都是三个字节。

好了接下来,我们先比较s4(“一”)和s3(“零”)。根据Go字符串比较原理,程序对s3和s4做逐字节比较,”零”这个字符的第一个字节为0xE9,而”一”这个字符的第一个字节为0xE4,我们知道0xE9 > 0xE4,于是比较停止,判定:s3 > s4。

同理,s3 > s5。

在比较s4(“一”)和s5(“二”)时,由于它们的第一个字节都是0xE4,于是第二个字节决定了它们的大小,0xBA > 0xB8,所以s5 > s4。

4. Go strings包中的Compare函数

Go标准库在strings包中提供了Compare函数用于对两个字符串做大小比较。但按照Go团队的comment,这个函数存在的意义更多是是为了与bytes包尽量保持API的一致,其自身也是使用原生排序比较操作符实现的:

// $GOROOT/src/strings/compare.go
func Compare(a, b string) int {
    if a == b {
        return 0
    }
    if a < b {
        return -1
    }
    return +1
}

实际应用中,我们很少使用strings.Compare更多的是直接使用排序比较操作符对字符串类型变量进行比较,这样更直观,性能大多数场景也会更高,毕竟少一次函数调用。


“好了以上就是我要讲给你听的,听懂了么”。西娅兴高采烈地对此时已经处于清醒状态的Tony说。

“讲的真好。比我书里讲的还透彻”。Tony一边鼓掌一边微笑着说。“程序员妹子西娅Thea终于把Go字符串比较讲清楚了”。

西娅惊讶!“你的什么书”?

Tony指了指办公桌上的小黄书说:“这书就是我写的啊^_^”。

西娅脸上现出一丝红晕… …。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2022, bigwhite. 版权所有.

Related posts:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK