Go 高效截取字符串的一些思考
最近我在GoForum中发现了Stringsizeof20character的问题,“hollowaykeanho”给出了相关的答案,而我从中发现了截取字符串的方案并非最理想的方法,因此做了一系列实验并获得高效截取字符串的方法,这篇文章将逐步讲解我实践的过程。
字节切片截取
这正是“hollowaykeanho”给出的第一个方案,我想也是很多人想到的第一个方案,利用go的内置切片语法截取字符串:
s:="abcdef" fmt.Println(s[1:4])
我们很快就了解到这是按字节截取,在处理ASCII单字节字符串截取,没有什么比这更完美的方案了,中文往往占多个字节,在utf8编码中是3个字节,如下程序我们将获得乱码数据:
s:="Go语言" fmt.Println(s[1:4])
杀手锏-类型转换[]rune
“hollowaykeanho”给出的第二个方案就是将字符串转换为[]rune,然后按切片语法截取,再把结果转成字符串。
s:="Go语言" rs:=[]rune(s) fmt.Println(strings(rs[1:4]))
首先我们得到了正确的结果,这是最大的进步。不过我对类型转换一直比较谨慎,我担心它的性能问题,因此我尝试在搜索引擎和各大论坛查找答案,但是我得到最多的还是这个方案,似乎这已经是唯一的解。
我尝试写个性能测试评测它的性能:
packagebenchmark import( "testing" ) varbenchmarkSubString="Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" varbenchmarkSubStringLength=20 funcSubStrRunes(sstring,lengthint)string{ ifutf8.RuneCountInString(s)>length{ rs:=[]rune(s) returnstring(rs[:length]) } returns } funcBenchmarkSubStrRunes(b*testing.B){ fori:=0;i我得到了让我有些吃惊的结果:
goos:darwin goarch:amd64 pkg:github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRunes-88722531363ns/op336B/op2allocs/op PASS okgithub.com/thinkeridea/go-extend/exunicode/exutf8/benchmark2.120s对69个的字符串截取前20个字符需要大概1.3微秒,这极大的超出了我的心里预期,我发现因为类型转换带来了内存分配,这产生了一个新的字符串,并且类型转换需要大量的计算。
救命稻草-utf8.DecodeRuneInString
我想改善类型转换带来的额外运算和内存分配,我仔细的梳理了一遍strings包,发现并没有相关的工具,这时我想到了utf8包,它提供了多字节计算相关的工具,实话说我对它并不熟悉,或者说没有主动(直接)使用过它,我查看了它所有的文档发现utf8.DecodeRuneInString函数可以转换单个字符,并给出字符占用字节的数量,我尝试了如此下的实验:
packagebenchmark import( "testing" "unicode/utf8" ) varbenchmarkSubString="Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" varbenchmarkSubStringLength=20 funcSubStrDecodeRuneInString(sstring,lengthint)string{ varsize,nint fori:=0;i运行它之后我得到了令我惊喜的结果:
goos:darwin goarch:amd64 pkg:github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrDecodeRuneInString-810774401105ns/op0B/op0allocs/op PASS okgithub.com/thinkeridea/go-extend/exunicode/exutf8/benchmark1.250s较[]rune类型转换效率提升了13倍,消除了内存分配,它的确令人激动和兴奋,我迫不及待的回复了“hollowaykeanho”告诉他我发现了一个更好的方法,并提供了相关的性能测试。
我有些小激动,兴奋的浏览着论坛里各种有趣的问题,在查看一个问题的帮助时(忘记是哪个问题了-_-||),我惊奇的发现了另一个思路。
良药不一定苦-range字符串迭代
许多人似乎遗忘了range是按字符迭代的,并非字节。使用range迭代字符串时返回字符起始索引和对应的字符,我立刻尝试利用这个特性编写了如下用例:
packagebenchmark import( "testing" ) varbenchmarkSubString="Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" varbenchmarkSubStringLength=20 funcSubStrRange(sstring,lengthint)string{ varn,iint fori=ranges{ ifn==length{ break } n++ } returns[:i] } funcBenchmarkSubStrRange(b*testing.B){ fori:=0;i我尝试运行它,这似乎有着无穷的魔力,结果并没有令我失望。
goos:darwin goarch:amd64 pkg:github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRange-81235499191.3ns/op0B/op0allocs/op PASS okgithub.com/thinkeridea/go-extend/exunicode/exutf8/benchmark1.233s它仅仅提升了13%,但它足够的简单和易于理解,这似乎就是我苦苦寻找的那味良药。
如果你以为这就结束了,不、这对我来只是探索的开始。
终极时刻-自己造轮子
喝了range那碗甜的腻人的良药,我似乎冷静下来了,我需要造一个轮子,它需要更易用,更高效。
于是乎我仔细观察了两个优化方案,它们似乎都是为了查找截取指定长度字符的索引位置,如果我可以提供一个这样的方法,是否就可以提供用户一个简单的截取实现s[:strIndex(20)],这个想法萌芽之后我就无法再度摆脱,我苦苦思索两天来如何来提供易于使用的接口。
之后我创造了exutf8.RuneIndexInString和exutf8.RuneIndex方法,分别用来计算字符串和字节切片中指定字符数量结束的索引位置。
我用exutf8.RuneIndexInString实现了一个字符串截取测试:
packagebenchmark import( "testing" "unicode/utf8" "github.com/thinkeridea/go-extend/exunicode/exutf8" ) varbenchmarkSubString="Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" varbenchmarkSubStringLength=20 funcSubStrRuneIndexInString(sstring,lengthint)string{ n,_:=exutf8.RuneIndexInString(s,length) returns[:n] } funcBenchmarkSubStrRuneIndexInString(b*testing.B){ fori:=0;i尝试运行它,我对结果感到十分欣慰:
goos:darwin goarch:amd64 pkg:github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRuneIndexInString-81354684982.4ns/op0B/op0allocs/op PASS okgithub.com/thinkeridea/go-extend/exunicode/exutf8/benchmark1.213s性能较range提升了10%,让我很欣慰可以再次获得新的提升,这证明它是有效的。
它足够的高效,但是却不够易用,我截取字符串需要两行代码,如果我想截取10~20之间的字符就需要4行代码,这并不是用户易于使用的接口,我参考了其它语言的sub_string方法,我想我应该也设计一个这个样的接口给用户。
exutf8.RuneSubString和exutf8.RuneSub是我认真思索后编写的方法:
funcRuneSubString(sstring,start,lengthint)string
它有三个参数:
- s:输入的字符串
- start:开始截取的位置,如果start是非负数,返回的字符串将从string的start位置开始,从0开始计算。例如,在字符串“abcdef”中,在位置0的字符是“a”,位置2的字符串是“c”等等。如果start是负数,返回的字符串将从string结尾处向前数第start个字符开始。如果string的长度小于start,将返回空字符串。
- length:截取的长度,如果提供了正数的length,返回的字符串将从start处开始最多包括length个字符(取决于string的长度)。如果提供了负数的length,那么string末尾处的length个字符将会被省略(若start是负数则从字符串尾部算起)。如果start不在这段文本中,那么将返回空字符串。如果提供了值为0的length,返回的子字符串将从start位置开始直到字符串结尾。
我为他们提供了别名,根据使用习惯大家更倾向去strings包寻找这类问题的解决方法,我创建了exstrings.SubString和exbytes.Sub作为更易检索到的别名方法。
最后我需要再做一个性能测试,确保它的性能:
packagebenchmark import( "testing" "github.com/thinkeridea/go-extend/exunicode/exutf8" ) varbenchmarkSubString="Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。" varbenchmarkSubStringLength=20 funcSubStrRuneSubString(sstring,lengthint)string{ returnexutf8.RuneSubString(s,0,length) } funcBenchmarkSubStrRuneSubString(b*testing.B){ fori:=0;i运行它,不会让我失望:
goos:darwin goarch:amd64 pkg:github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark BenchmarkSubStrRuneSubString-81330908283.9ns/op0B/op0allocs/op PASS okgithub.com/thinkeridea/go-extend/exunicode/exutf8/benchmark1.215s虽然相较exutf8.RuneIndexInString有所下降,但它提供了易于交互和使用的接口,我认为这应该是最实用的方案,如果你追求极致仍然可以使用exutf8.RuneIndexInString,它依然是最快的方案。
总结
当看到有疑问的代码,即使它十分的简单,依然值得深究,并不停的探索它,这并不枯燥和乏味,反而会有极多收获。
从起初[]rune类型转换到最后自己造轮子,不仅得到了16倍的性能提升,我还学习了utf8包、加深了range遍历字符串的特性以及为go-extend仓库收录了多个实用高效的解决方案,让更多go-extend的用户得到成果。
go-extend是一个收录实用、高效方法的仓库,读者们如果好的函数和通用高效的解决方案,期待你们不吝啬给我发送Pullrequest,你也可以使用这个仓库加快功能实现及提升性能。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。