分类 技术志 下的文章

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://tonybai.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

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

Go是如何缓解供应链攻击的[译]

本文永久链接 – https://tonybai.com/2022/04/02/how-go-mitigates-supply-chain-attacks

这些年来,关于软件供应链的安全问题频发,软件供应链已然成为IT安全领域的一个热点,在前不久的《聊聊Go语言的软件供应链安全》一文中我曾提到过Go在SBOM(软件物料清单)方面给开发人员带来的方便。这两天Go官博又发表了一篇由Go项目安全负责人Filippo Valsorda撰写的文章《How Go Mitigates Supply Chain Attacks》,系统总结了Go语言应对软件供应链方面攻击的“防护秘笈”。笔者觉得文章中提到的这些点是每个Gopher都应该知道的必备知识,于是这里将文章做简单翻译,供大家参考。


现代软件工程基于相互协作,并以重用开源软件为基础。但这也使软件项目成为了供应链攻击的目标,攻击方式就是破坏软件项目的依赖(dependencies)。

尽管知道这些,为了完成项目,我们需要依赖,我们会采取一些流程或技术措施在项目与依赖之间建立一种信任关系。好在,Go的工具链与设计可以帮助我们降低各个阶段的风险。

所有构建都是被“锁定(locked)”的

外部世界的变化,比如项目的某个依赖发布了一个新版本,是不会影响到Go的构建的。

与其他大多数软件包管理器(package manager)所使用的配置文件不同,Go module没有将存储约束列表的文件和锁定特定版本的lock文件分开管理,对任何Go构建作出贡献的每个依赖项的版本完全由main module的go.mod文件决定。

Go 1.16版本开始,Go命令默认按照这种确定性执行,如果go.mod不完整,构建命令(go build, go test, go install, go run, …)将失败。唯一可以改变go.mod文件(当然构建也会随之改变)的命令是go get和go mod tidy。这两个命令通常不会自动运行或在CI中运行,所以对依赖树的改变必须是主观故意的,我们可以在操作前对这种改变做代码评审。

这对安全非常重要,因为当CI系统或新机器运行时,签入(checked-in)的源码是最终的和完整的,代码将说明什么会被构建,第三方没有办法影响它。

此外,当用go get添加新依赖时,由于最小版本选择的存在,它的传递依赖(transitive dependencies)的指定版本,不是最新版本,也会被添加到go.mod文件中。同样的情况也发生在调用go install example.com/cmd/devtoolx@latest 的情况下,在某些生态系统中,同样的构建发生时会绕过“已锁定”的版本(译注:去获取依赖的最新版本)。但在Go中,example.com/cmd/devtoolx的最新版本将被获取,但其所有的依赖项的版本将取决于其go.mod文件中的设置。

如果一个module被破坏,新的恶意版本被发布,没有人会受到影响,直到他们明确地更新该依赖关系,这种方式为gopher提供了审查变化的机会,并为生态系统提供时间来检测该事件会引发的影响。

版本内容永不改变

确保第三方不能影响构建的另一个关键属性是,module版本的内容是不可改变的。如果一个破坏某依赖项的攻击者可以重新上传该依赖项的一个现有的版本,那么他就可以自动破坏所有依赖该依赖项的项目。

这就是go.sum文件的作用。它包含了对构建有贡献的每个依赖项的加密哈希值的列表。同样,一个不完整的go.sum会导致一个错误,并且只有go get和go mod tidy会修改它,所以对它的任何修改都会伴随着一个主观故意的依赖性的改变。其他的构建将被保证有一套完整的校验和。

这是大多数lock文件的一个共同特征。但Go通过校验和数据库(简称sumdb)领先了一步,sumdb是一个全局性的、仅可附加的(append)、加密验证的go.sum条目列表。当go get需要在go.sum文件中添加一个条目时,它从sumdb中获取该条目,并对sumdb的完整性进行加密证明。这不仅确保了某一module的每一次构建都使用相同的依赖,而且确保了每一个module都使用相同的依赖内容。

sumdb使那些试图用修改过的(例如放置后门的)源码来攻击特定依赖项变为不可能,甚至谷歌自己运维的Go基础设施也做不到。

它将保证你使用的代码与其他使用例如example.com/modulex v1.9.2的人所使用的代码完全相同,并且已经过审查。

最后,我最喜欢sumdb的特点:它不需要module作者的任何密钥管理,而且它与Go module的非中心化特性无缝连接。

VCS是真相之源

大多数项目是通过一些版本控制系统(VCS)开发的。在其他生态系统中,这些项目还需要被再次上传到中心软件包库(译注:比如js生态中的npm)。这意味着有两个账户可能被入侵,一个是VCS主机,另一个是中心软件包库。对后者的攻击使用得更少,也更容易被忽视。这也意味着在上传到中心仓库的版本中更容易隐藏恶意代码,尤其是当源码作为上传的一部分被例行修改时,比如说将其最小化(译注:比如js代码的压缩)。

在Go中,不存在中心包库账户这样的东西。包的导入路径包含了go mod download所需要的信息,以便go命令直接从VCS中获取其module,vcs上的标签定义了module的版本。

我们确实有Go Module Mirror,但那只是一个代理。module作者不需要注册账户,也不需要向代理上传版本。代理使用与go工具链相同的逻辑(事实上,代理运行go module download)来获取和缓存一个版本。由于校验数据库保证一个给定的module版本只能有一个源码树,每个使用代理的人都会看到从代理获取的结果与绕过代理直接从VCS获取的结果是相同的。(如果该版本在VCS中不再可用,或者其内容发生了变化,直接获取将导致错误,而从代理获取可能仍然有效,提高了可用性并保护生态系统免受“左键”问题的影响)。

在客户端运行VCS工具会暴露出一个相当大的攻击面。这也是Go module mirror的另一个作用:代理上的Go工具在一个强大的沙盒内运行,并被配置为支持所有的VCS工具,而默认的是只支持两个主要的VCS系统(git和Mercurial)。任何使用代理的人仍然可以获取使用非默认的VCS系统发布的代码,但攻击者在大多数安装中无法接触到这些代码。

仅构建代码,但并不会执行它

Go工具链的一个明确的安全设计目标是,无论是获取还是构建代码,都不会让代码执行,无论代码是否是不被信任的和恶意的。这与其他大多数生态系统不同,许多生态系统在获取软件包时对运行代码提供了first-class的支持。这些”post-install”的钩子在过去被用作一种最方便的攻击方式:通过受到攻击的依赖攻击开发者的机器,并通过module作者进行蠕虫攻击

公平地说,如果你要获取一些代码,往往会在不久之后执行,要么作为开发者机器上的测试的一部分,要么作为生产中的二进制文件的一部分,所以缺乏post-install钩子只会减缓攻击者。(在构建过程中没有安全边界:任何有助于构建的软件包都可以定义一个init函数)。然而,它可以成为一个有意义的风险缓解措施,因为你可能正在执行一个二进制文件或测试一个包,而这个包只使用module依赖的一个子集。例如,如果你在macOS上构建并执行example.com/cmd/devtoolx,那么针对Windows的依赖或example.com/cmd/othertool的依赖就不可能危害到你的机器。

在Go中,没有为特定构建提供代码的module对构建没有安全影响(译注:得益于Go 1.17引入的module依赖图修剪)。

“一点复制比一点依赖性好”

在Go生态系统中,最后一个可能也是最重要的软件供应链风险缓解措施,可能也是最没有技术含量的一个:Go有一种拒绝大型依赖树的文化,宁可复制一点也不愿意增加新的依赖关系。这可以追溯到Go的一个谚语:“一点复制比一点依赖性好”。”零依赖”的标签总是被高质量的可重复使用的Go module所自豪地佩戴。如果你发现自己需要一个这样的库,你很可能会发现它不会导致你依赖其他作者和所有者的几十个module。

丰富的标准库和附加module(golang.org/x/…的module)也使之成为可能,这些module提供了常用的高级构建模块,如HTTP栈、TLS库、JSON编码等。

所有这些意味着只需少量的依赖关系就可以建立丰富、复杂的应用程序。无论工具有多好,它都不能消除重复使用代码的风险,所以最有力的缓解措施永远是一个小的依赖树


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

img{512x368}
img{512x368}

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

我爱发短信:企业级短信平台定制开发专家 https://tonybai.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

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats