声明:该系列文章是基于对@benbjohnson的《Go Walkthrough》、go官方文档、《Go语言标准库》的学习汇总而成
bytes和strings简单对比
[]byte表示了一组可修改、可扩展、连续性的byte列表。而string表示不可修改、固定长度、连续的byte列表。这意味着你不能更新字符串,只能创建,而这可能带来很大的负担
从使用者角度来看,字符串更易于使用,能作为map的键。而bytes能够更好地处理字节流,也能更好地减少内存分配以及重用
字符串和bytes与字节流的交互
创建内存reader
bytes.NewReader和strings.NewReader都可以包裹[]byte或者string返回一个实现了所有read相关接口的reader
func NewReader(b []byte) *Reader
func NewReader(s string) *Reader
经常我们可以看到下述的写法,将[]byte或者string写入到buffer,然后作为一个reader,这样涉及到内存分配会很慢
var buf bytes.Buffer
buf.WriteString("foo")
http.Post("http://example.com/", "text/plain", &buf)
事实上,我们完全可以直接使用NewReader
r := strings.NewReader("foobar")
http.Post("http://example.com", "text/plain", r)
创建内存Writer
我们可以通过声明bytes.Buffer来创建一个新的writer,它实现了除io.Seeker和io.Closer以外的所有写接口,而且同样实现了WriteString辅助函数
一个常用的场景是在单元测试中捕捉日志输出
var buf bytes.Buffer
myService.Logger = log.New(&buf, "", log.LstdFlags)
myService.Run()
if !strings.Contains(buf.String(), "service failed") {
t.Fatal("expected log message")
}
但是实际生产中可能更多的使用bufio包来做缓存读写
包的组织结构
乍一看下,bytes和strings都是规模较大的包。但实质上它们只是一系列简单的辅助函数,总的来说,可以分为五种
- 比较函数
- 查找函数
- 前缀/后缀函数
- 替换函数
- 切分/组合函数
比较函数
相等性
我们可以使用Equal函数来判断二者是否相等,该函数只出现在bytes包因为string可以直接用==比较。
func Equal(a, b []byte) bool
有些时候我们会希望获知A、B如果不考虑大小写是否相等,一个常见的错误写法如下图
if strings.ToUpper(a) == strings.ToUpper(b) {
return true
}
上述的做法会造成两次重新内存分配(为新的字符串),一个正确的做法应该是使用EqualFold函数
func EqualFold(s, t []byte) bool
func EqualFold(s, t string) bool
Fold一词来自于 Unicode case-folding,因此该函数不仅能处理a-z的大小写问题,还能处理其他语言如φ to ϕ的转换
比较
我们可以使用Compare来比较[]byte或者string,如果返回-1表示a<b,0表示相等,1表示a>b
func Compare(a, b []byte) int
func Compare(a, b string) int
其中strings.Compare的出现仅仅是为了对称,事实上,该函数的comment里都指出几乎没有人应该使用strings.Compare,字符串比较可以直接使用<,>
通常来说我们比较两个[]byte是为了排序,然后sort.Interface要求实现Less函数,因此我们可以自行实现一个转换
type ByteSlices [][]byte
func (p ByteSlices) Less(i, j int) bool {
return bytes.Compare(p[i], p[j]) == -1
}
查找函数
统计出现次数
我们可以用Contains函数来审查是否特定[]byte或者string是否存在
func Contains(b, subslice []byte) bool
func Contains(s, substr string) bool
func ContainsAny(s, chars string) bool
func ContainsAny(b []byte, chars string) bool
func ContainsRune(s string, r rune) bool
func ContainsRune(b []byte, r rune) bool
其中ContainsAny会在chars中有任意一个码点在s中出现就成立
如果我们需要统计出现的次数,我们可以使用Count,Count会计算的是无重叠的次数fmt.Println(strings.Count("fivevev", "vev"))//1
func Count(s, sep []byte) int
func Count(s, sep string) int
Count的另外一个用途适用于统计字符串所有的runes(实际字符数),通过传入"",Count会返回实际字符数+1
strings.Count("I ❤ ☃", "") // 6,实际字符数5+1
len("I ❤ ☃") // 9
查找具体位置
我们可以用下面的查找函数来获取特定子串的具体位置
Index(s, sep []byte) int
IndexAny(s []byte, chars string) int
IndexByte(s []byte, c byte) int
IndexFunc(s []byte, f func(r rune) bool) int
IndexRune(s []byte, r rune) int
这五个函数strings和bytes都有(strings就是以string为第一个参数),Index用于查找一个多字节子串,IndexByte查找一个特定字节,IndexRune查找一个特定码点(将[]byte经utf-8转码后),IndexAny和IndexRune相似但是可以查找多码点,IndexFunc允许你传入一个过滤器查找每一个码点直到发现一个match
同时还有从尾部开始查找第一个符合条件的子串的对应函数
LastIndex(s, sep []byte) int
LastIndexAny(s []byte, chars string) int
LastIndexByte(s []byte, c byte) int
LastIndexFunc(s []byte, f func(r rune) bool) int
通常会比较少使用到,因为很多时候你会发现你需要的是一个parser
前缀、后缀和剪枝
显然,处理前缀、后缀是查找的特殊例子,但是足够特殊使得我们需要提供对应函数
检查是否有某前缀、后缀
我们可以使用HasPrefix和HasSuffix来检查是否有前缀和后缀
func HasPrefix(s, prefix []byte) bool
func HasPrefix(s, prefix string) bool
func HasSuffix(s, suffix []byte) bool
func HasSuffix(s, suffix string) bool
可能有些时候功能看起来太简单以至于不需要使用,但是一个常见的错误就是关于忘记检查长度
if str[0] == '@' {
return true
}
如果str为空,上述代码会触发panic,而HasPrefix解决了这个特殊情况
if strings.HasPrefix(str, "@") {
return true
}
剪枝
剪枝意味着从[]byte或者字符串头和尾去掉一些字符,最通用的函数是Trim
func Trim(s []byte, cutset string) []byte
func Trim(s string, cutset string) string
该函数会从s的头部和尾部去掉所有匹配cutset的码点,我们也可以用TrimLeft和TrimRight单独处理头部或者尾部
但通常来说,我们只是想去掉空格,我们可以使用TrimSpace
func TrimSpace(s []byte) []byte
func TrimSpace(s string) string
可能很多人觉得我为什么要用TrimSpace,我可以直接使用Trim(s,"\n\t"),答案是TrimSpace可以去掉unicode定义的所有空格,这不仅仅包括空格、换行、tab还有如thin space和hair space
不过实质上,TrimSpace只是一个TrimFunc的简单包装而已
func TrimSpace(s string) string {
return TrimFunc(s, unicode.IsSpace)
}
我们可以仿造TrimSpace来设计一个专门只用来清理尾部空格的函数
TrimRightFunc(s, unicode.IsSpace)
最后,如果我们是想去掉实际前缀(而不是字符集),我们可以使用TrimPrefix和TrimSuffix
func TrimPrefix(s, prefix []byte) []byte
func TrimPrefix(s, prefix string) string
func TrimSuffix(s, suffix []byte) []byte
func TrimSuffix(s, suffix string) string
这函数通常与HasPrefix和HasSuffix结合
// Replace tilde prefix with home directory.
if strings.HasPrefix(path, "~/") {
path = filepath.Join(u.HomeDir, strings.TrimPrefix(path, "~/"))
}
替换函数
简单替换
在绝大多数简单的替换场景下,我们可以使用Replace函数来实现替换
func Replace(s, old, new []byte, n int) []byte
func Replace(s, old, new string, n int) string
该函数会替换s中的old为new,如果n为非负数,那么就最多可以替换n次
该函数可以用于比方说你有一个placeholder如$Now,然后希望转换成当前时间
now := time.Now().Format(time.Kitchen)
println(strings.Replace(data, "$NOW", now, -1)
但是如果你有多个映射关系,你就可以使用strings.Replacer,该结构体需要结合strings.NewReplacer和一对对新、旧字符串
r := strings.NewReplacer("$NOW", now, "$USER", "mary")
println(r.Replace("Hello $USER, it is $NOW"))
// Output: Hello mary, it is 3:04PM
大小写替换
你可能会认为大小写很简单,但实质上由于go支持unicode,因此大小写转换就不是单纯的a-A了,我们有三种转换:upper,lower和title
对于绝大多数语言而言,大写和小写是简单的,只需要调用ToUpper和ToLower函数
func ToUpper(s []byte) []byte
func ToUpper(s string) string
func ToLower(s []byte) []byte
func ToLower(s string) string
但是有一些语言有着特别的大小写转换规则,如将i转换成 İ,对于这些特殊的例子,可以使用特别版本的Upper
strings.ToUpperSpecial(unicode.TurkishCase, "i")
接下来,我们有title case以及ToTitle函数
func ToTitle(s []byte) []byte
func ToTitle(s string) string
但是值得注意的是,ToTitle不是我们想象中的将每个单词首字母大写,事实上title case在unicode中是一种特别的casing。绝大多数时候,title case和大写一致,但是有少部分码点不同,如lj,其大写upper case是LJ ,其title case是Lj
如果你希望将每次单词首字母大写,那么你需要调用的是Title函数
func Title(s []byte) []byte
func Title(s string) string
匹配runes
Map函数允许你传入一个函数对每一个rune进行检查并替换
func Map(mapping func(r rune) rune, s []byte) []byte
func Map(mapping func(r rune) rune, s string) string
该函数很少使用
切分拼接函数
很多时候我们有待切分的有明确分隔符的字符串,如csv中的,、unix路径中的:
子字符串切分
对于简单的子字符串,我们有Split函数
func Split(s, sep []byte) [][]byte
func SplitAfter(s, sep []byte) [][]byte
func SplitAfterN(s, sep []byte, n int) [][]byte
func SplitN(s, sep []byte, n int) [][]byte
func Split(s, sep string) []string
func SplitAfter(s, sep string) []string
func SplitAfterN(s, sep string, n int) []string
func SplitN(s, sep string, n int) []string
上述这些函数会将s根据sep切分成若干个部分,期中After结尾表示将分隔符附在子字符串后面,N指定切分允许的次数
strings.Split("a:b:c", ":") // ["a", "b", "c"]
strings.SplitAfter("a:b:c", ":") // ["a:", "b:", "c"]
strings.SplitN("a:b:c", ":", 2) // ["a", "b:c"]
分割是一个很常见的需求,但是往往出现在一个诸如csv文件、路径切分等场景中,一般来说,更适合使用encoding/csv或者path
组切分
有些时候分隔符不是一个字符串,而是一串(相同)字符串。一个最佳例子就是切分有着不定长空格的橘子。如果有连续空格单纯调用Split只会得到空白字符串,这时候可以使用Fields函数
func Fields(s []byte) [][]byte
该函数将不定长空格视作一个分隔符,除此之外我们还可以使用FieldFunc函数来将特定rune视作分隔符
func FieldsFunc(s []byte, f func(rune) bool) [][]byte
拼接
我们可以使用Join函数来拼接字符串
func Join(s [][]byte, sep []byte) []byte
func Join(a []string, sep string) string
一个常见的错误在于很多人试图自行实现拼接,如下图函数所示
var output string
for i, s := range a {
output += s
if i < len(a) - 1 {
output += ","
}
}
return output
这样实现的问题在于中间有太多不必要的内存分配,因为字符串是不可变的,所以循环的每一次都在创建一个新的字符串,而strings.Join则用一个byte切片作为buffer然后将其转换为字符串,从而最小化堆内存分配花销
其他混杂函数
Repeat函数
Repeat函数允许我们创建一个被重复的字符串,比较少用到
println(strings.Repeat("-", 80))
Runes函数
该函数将一个字符串经UTF-8翻译后返回一个全新的[]rune,这函数很少使用,因为是用for range可以实现类似的功能同时不需要内存分配