一起读Go源码(第11期) bytes:bytes
概述
朋友们好啊,这一期我们来看bytes提供的主流功能及其源码实现。Go官方给的描述是:bytes包实现了对字节切片进行操作的相关函数,她与strings包中的功能相似。因此bytes包也实现了如字节切片比较、包含、查找、前缀后缀匹配、分割与拼接、修剪、大小写转换、替换、统计等功能,下面我们来看看具体实例吧。
字节切片比较 Equal与Compare
Equal和Compare都用于比较字节切片是否一致,但它们的返回值不一样,Equal返回bool类型,而Compare会根据字典序比较大小返回int类型;下面是Equal和Compare的使用示例:
a, b, c := []byte("abc"), []byte("abc"), []byte("abcd")log.Println(bytes.Equal(a, b), bytes.Equal(a, c), bytes.Equal(nil, []byte{}))log.Println(bytes.Compare(a, b), bytes.Equal(a, c))// 2026/01/29 20:22:41 true false true// 2026/01/29 20:22:41 0 false
我们再看看这两个函数的源码:
// Equal reports whether a and b// are the same length and contain the same bytes.// A nil argument is equivalent to an empty slice.func Equal(a, b []byte) bool { // Neither cmd/compile nor gccgo allocates for these string conversions. return string(a) == string(b)}
首先是Equal函数,先把[]byte零拷贝成string,在通过string的 == 比较,通过判断长度和字节是否一致返回结果,这时候有彦祖要问了什么是零拷贝?首先Go有两种拷贝,一种是底层数据拷贝,会复制切片指向的字节数组,[]byte数据是多大内存,拷贝就要新占多一份内存;另一种是结构体,复制切片的元数据结构体(指针,长度,容量三个数),拷贝性能开销忽略不计。基于Go中规定字符串内容不可以被修改,使string和[]byte共享内存不会有安全问题,这里[]byte零拷贝转成string的零拷贝是Go编译器专门做过优化——当编译器在代码中检测到string(a)会接编译阶段直接生成特殊的机器码:
-
1. 在[]byte结构体中读取ptr指针和len长度 -
2. 在栈上新分配一个string结构体(16字节大小) -
3. 将读取到的ptr指针和len长度赋值到string结构体上 -
4. 完成[]byte转string,过程完全没有复制底层字节数组。
一通操作下来极大节省了内存开销,提高了判断的性能。
// Compare returns an integer comparing two byte slices lexicographically.// The result will be 0 if a == b, -1 if a < b, and +1 if a > b.// A nil argument is equivalent to an empty slice.func Compare(a, b []byte) int { return bytealg.Compare(a, b)}
那么Compare函数会按字典序比较两个字节切片,返回一个整数;规则是:若a等于b则返回0,若a小于b则返回-1,若a>b则返回1;Compare函数内部调用了bytealg.Compare方法,bytealg是Go内部私有包,不属于对外暴露的标准库。
字节切片包含、索引 Contains与Index
Contains报告原切片范围内是否存在子切片,返回bool类型的结果;衍生周边还有ContainsAny,ContainsRune,ContainsFunc等方法;而Index函数会字符串s中第一个出现字符串sep的索引位置,若sep不存在则返回-1,Index返回的是Int类型结果;衍生还有IndexByte,IndexByteProtable,LastIndex,LastIndexByte,IndexRune,IndexAny等函数。
str := []byte("abcdefg")log.Println(bytes.Contains(str, []byte("abc")))log.Println(bytes.Index(str, []byte("f")))// 2026/01/29 20:22:41 true// 2026/01/29 20:22:41 5
先来看看Contains的源码:
// Contains reports whether subslice is within b.func Contains(b, subslice []byte) bool { return Index(b, subslice) != -1}
芜湖里面直接用Index判断,返回Index中若sep不存在则返回-1的bool结果,那我们看看Index函数的源码:
-
1. 首先使用Switch将类型分类, -
• n等于0直接返回0 -
• n等于1放到IndexByte处理 -
• n等于len(s),只需一次全量比较就知道结果,交给Equal处理 -
• n>len(s),肯定是错的,返回-1 -
• n<=bytealg.MaxLen, bytealg.MaxLen是Go中内部字节操作包bytealg中定义的常量阈值(int类型),是作为子串和子切片长短的分界线;而bytealg.MaxBruteForce则是原切片/原字符串的长度阈值(int类型)。若子切片和原切片都是短的交给bytealg.Index(s, sep)处理(反正都很小,暴力匹配)。 -
2. 然后剩下的就是短子切片长原切片,长子切片长原切片这两种场景的处理。 -
• 短子切片长原切片的情况 -
• 长子切片长原切片的情况
// Index returns the index of the first instance of sep in s, or -1 if sep is not present in s.func Index(s, sep []byte) int { n := len(sep) switch { case n == 0: return 0 case n == 1: return IndexByte(s, sep[0]) case n == len(s): if Equal(sep, s) { return 0 } return -1 case n > len(s): return -1 case n <= bytealg.MaxLen: // Use brute force when s and sep both are small if len(s) <= bytealg.MaxBruteForce { return bytealg.Index(s, sep) } c0 := sep[0] c1 := sep[1] i := 0 t := len(s) - n + 1 fails := 0 for i < t { if s[i] != c0 { // IndexByte is faster than bytealg.Index, so use it as long as // we're not getting lots of false positives. o := IndexByte(s[i+1:t], c0) if o < 0 { return -1 } i += o + 1 } if s[i+1] == c1 && Equal(s[i:i+n], sep) { return i } fails++ i++ // Switch to bytealg.Index when IndexByte produces too many false positives. if fails > bytealg.Cutover(i) { r := bytealg.Index(s[i:], sep) if r >= 0 { return r + i } return -1 } } return -1 } c0 := sep[0] c1 := sep[1] i := 0 fails := 0 t := len(s) - n + 1 for i < t { if s[i] != c0 { o := IndexByte(s[i+1:t], c0) if o < 0 { break } i += o + 1 } if s[i+1] == c1 && Equal(s[i:i+n], sep) { return i } i++ fails++ if fails >= 4+i>>4 && i < t { // Give up on IndexByte, it isn't skipping ahead // far enough to be better than Rabin-Karp. // Experiments (using IndexPeriodic) suggest // the cutover is about 16 byte skips. // TODO: if large prefixes of sep are matching // we should cutover at even larger average skips, // because Equal becomes that much more expensive. // This code does not take that effect into account. j := bytealg.IndexRabinKarp(s[i:], sep) if j < 0 { return -1 } return i + j } } return -1}
字节切片前缀、后缀匹配 HasPrefix
HasPrefix函数会报告字节切片s是否以前缀开头,HasSuffix函数会报告字节切片s是否以后缀suffix结尾,简单示例如下:
log.Println(bytes.HasPrefix(str, []byte("ab")), bytes.HasSuffix(str, []byte("fg")))// 2026/01/29 20:22:41 true true
这两个函数都返回bool类型,在HasPrefix函数中判断同时满足原字节s长度比前缀prefix长,且s原字节[0:len(prefix)]与前缀prefix完全相同;HasSuffix则相反,判断同时满足原字节s长度比后缀suffix长,且s原字节s[len(s)-len(suffix):]与后缀suffix完全相同。
// HasPrefix reports whether the byte slice s begins with prefix.func HasPrefix(s, prefix []byte) bool { return len(s) >= len(prefix) && Equal(s[0:len(prefix)], prefix)}// HasSuffix reports whether the byte slice s ends with suffix.func HasSuffix(s, suffix []byte) bool { return len(s) >= len(suffix) && Equal(s[len(s)-len(suffix):], suffix)}
字节切片分割与拼接 Split Join
先来看看字节切片分割Split,将切片s按照分隔符sep分割成所有子切片,并返回这些分隔符之间的子切片的切片。若sep为空,则Split会在每个UFT-8序列之后进行分割,它等同于将SplitN的count参数设置为-1。若要在分隔符的首次出现进行拆分,就看[Cut]。
str2 := []byte("a,b,c,defg")sp := []byte(",")log.Println(bytes.Split(str2, sp))// 2026/01/29 20:22:41 [[97] [98] [99] [100 101 102 103]]s3 := [][]byte{}s3 = append(s3, []byte("1234"))s3 = append(s3, []byte("5678"))log.Println(string(bytes.Join(s3, []byte("@"))))// 2026/01/29 20:22:41 1234@5678
// Split slices s into all subslices separated by sep and returns a slice of// the subslices between those separators.// If sep is empty, Split splits after each UTF-8 sequence.// It is equivalent to SplitN with a count of -1.//// To split around the first instance of a separator, see [Cut].func Split(s, sep []byte) [][]byte { return genSplit(s, sep, 0, -1) }
Join方法将字符串s的各个元素连接起来,从而生成一个新的字节切片,分隔符sep会置于生成的切片中的各个元素之间。从Join函数进行了以下操作:
-
1. 边界判断:判断s如果长度为0直接返回空切片;如果s长度是1则返回该切片第一个元素的一个全新副本。 -
2. 计算最终字节的长度,避免频繁扩容;并用 bytealg.MakeNoZero分配未初始化的内存。 -
3. 拼接数据,返回结果
// Join concatenates the elements of s to create a new byte slice. The separator// sep is placed between elements in the resulting slice.func Join(s [][]byte, sep []byte) []byte { if len(s) == 0 { return []byte{} } if len(s) == 1 { // Just return a copy. return append([]byte(nil), s[0]...) } var n int if len(sep) > 0 { if len(sep) >= maxInt/(len(s)-1) { panic("bytes: Join output length overflow") } n += len(sep) * (len(s) - 1) } for _, v := range s { if len(v) > maxInt-n { panic("bytes: Join output length overflow") } n += len(v) } b := bytealg.MakeNoZero(n)[:n:n] bp := copy(b, s[0]) for _, v := range s[1:] { bp += copy(b[bp:], sep) bp += copy(b[bp:], v) } return b}
字节切片修剪 Trim
Trim函数会通过从字符串s中切片所有位于cutset中的UFT-8编码的其实和结尾字符来返回一个子字符串。相关类似的还有TrimLeft,TrimRight。
s5 := []byte("111@qq.com")log.Println(string(bytes.Trim(s5, "111")), string(bytes.TrimLeft(s5, "11")), string(bytes.TrimRight(s5, ".com")))// 2026/01/29 20:22:41 @qq.com @qq.com 111@qq
Trim函数做了以下步骤:
-
1. 边界判断,若s长度为0直接返回nil,若 cutset为空,直接返回原切片s。 -
2. 如果 cutset长度为1,且cueset(cutset是修剪字节切片,只要字符出现在cutset中,就会被s的首尾删除;中间的内容不受影响)内容是ASCII字符(对应值小于128,老朋友了) -
3. 调用 trimRightByte从s末尾开始,删除等于cutset[0]的字节 -
4. 调用 trimLeftByte从s开头开始,删除等于cutset[0]的字节 -
5. 最后调用 trimLeftUnicode(trimRightUnicode(s, cutset), cutset),会将字节切片s按uft-8规则解析成rune类型,再判断rune是否在cutset中,是的话就删除。
// Trim returns a subslice of s by slicing off all leading and// trailing UTF-8-encoded code points contained in cutset.func Trim(s []byte, cutset string) []byte { if len(s) == 0 { // This is what we've historically done. return nil } if cutset == "" { return s } if len(cutset) == 1 && cutset[0] < utf8.RuneSelf { return trimLeftByte(trimRightByte(s, cutset[0]), cutset[0]) } if as, ok := makeASCIISet(cutset); ok { return trimLeftASCII(trimRightASCII(s, &as), &as) } return trimLeftUnicode(trimRightUnicode(s, cutset), cutset)}
字节切片大小写转换
ToUpper方法会返回一个与输入字节切片s相同的副本,但其中所有的Unicode字母都会被转换成大写形式。ToLower方法会返回一个与输入字节切片s相同的副本,但其中所有的Unicode字母都会被转换成小写形式。
s6 := []byte("abcdefGHI")log.Println(string(bytes.ToUpper(s6)), string(bytes.ToLower(s6)))// 2026/01/29 20:22:41 ABCDEFGHI abcdefghi
我们就来看看ToUpper的操作步骤:
-
1. 有isASCII和hasLower两个变量,分别用来标记是否为纯ASCII切片和是否包含ASCII小写字母。 -
2. 遍历字节,如果发现有非isASCII字符,例如中文,立即跳出循环。 -
3. 然后是判断,若是纯大写ASCII字节,就无需转换直接返回。 -
4. 若包含小写ASCII且hasLower是true,高效内存分配 bytealg.MakeNoZero(len(s)),遍历s,通过c -=’a’ – ‘A’ 即-32。 -
5. 若包含Unicode,调用Map(unicode.ToUpper,s)函数。
// ToUpper returns a copy of the byte slice s with all Unicode letters mapped to// their upper case.func ToUpper(s []byte) []byte { isASCII, hasLower := true, false for i := 0; i < len(s); i++ { c := s[i] if c >= utf8.RuneSelf { isASCII = false break } hasLower = hasLower || ('a' <= c && c <= 'z') } if isASCII { // optimize for ASCII-only byte slices. if !hasLower { // Just return a copy. return append([]byte(""), s...) } b := bytealg.MakeNoZero(len(s))[:len(s):len(s)] for i := 0; i < len(s); i++ { c := s[i] if 'a' <= c && c <= 'z' { c -= 'a' - 'A' } b[i] = c } return b } return Map(unicode.ToUpper, s)}
字节切片 Replace
Replace函数会返回一个与原始切片s相同的副本,其中前n个不重叠的旧元素会被替换为新元素。如果旧元素为空,则匹配切片的开头以及每个UTF-8序列之后的部分,最多可进行k+1次替换(对于包含k个字符的切片而言),如果n为负值,则替换没有次数限制。
s7 := []byte("aaaaabbbccc")old, n := []byte("a"), []byte("A")log.Println(string(bytes.Replace(s7, old, n, 2)))// 2026/01/29 20:22:41 AAaaabbbccc
-
1. 首先是匹配次数预计,使用 Count方法进行统计。若n用户替换次数0,跳过统计,保持m=0; -
2. 若不存在old则直接返回拷贝结果。 -
3. 进行内存分配 make([]byte, len(s)+n*(len(new)-len(old)))后进入循环进行替换
// Replace returns a copy of the slice s with the first n// non-overlapping instances of old replaced by new.// If old is empty, it matches at the beginning of the slice// and after each UTF-8 sequence, yielding up to k+1 replacements// for a k-rune slice.// If n < 0, there is no limit on the number of replacements.func Replace(s, old, new []byte, n int) []byte { m := 0 if n != 0 { // Compute number of replacements. m = Count(s, old) } if m == 0 { // Just return a copy. return append([]byte(nil), s...) } if n < 0 || m < n { n = m } // Apply replacements to buffer. t := make([]byte, len(s)+n*(len(new)-len(old))) w := 0 start := 0 for i := 0; i < n; i++ { j := start if len(old) == 0 { if i > 0 { _, wid := utf8.DecodeRune(s[start:]) j += wid } } else { j += Index(s[start:], old) } w += copy(t[w:], s[start:j]) w += copy(t[w:], new) start = j + len(old) } w += copy(t[w:], s[start:]) return t[0:w]}
字节切片次数统计 Count
Count函数会统计字符串s中不重叠出现的sep的次数,如果sep是一个空切片,则该函数返回1加上字符串s中的uft-8编码字符点的数量。
s8 := []byte("aaaabbbbddd")log.Println(bytes.Count(s8, []byte("a")))// 2026/01/29 20:22:41 4
对于Count函数而言:
-
1. 首先是边界判断,若sep为空切片,返回s中的uft-8码点数量+1(可以理解为字符之间的分割位置,就好像空格);若sep不费控就统计其非重叠出现的次数。 -
2. 在循环中若找不到则返回i=-1,返回累计次数n;若找得到则n+1,然后继续循环。
// Count counts the number of non-overlapping instances of sep in s.// If sep is an empty slice, Count returns 1 + the number of UTF-8-encoded code points in s.func Count(s, sep []byte) int { // special case if len(sep) == 0 { return utf8.RuneCount(s) + 1 } if len(sep) == 1 { return bytealg.Count(s, sep[0]) } n := 0 for { i := Index(s, sep) if i == -1 { return n } n++ s = s[i+len(sep):] }}
写在最后
本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!下一篇我们看看fmt。
夜雨聆风
