詳解Golang中字符串的使用
1、字符串編碼
在go中rune是一個(gè)unicode編碼點(diǎn)。
我們都知道UTF-8將字符編碼為1-4個(gè)字節(jié),比如我們常用的漢字,UTF-8編碼為3個(gè)字節(jié)。所以rune也是int32的別名。
type rune = int32
當(dāng)我們打印一個(gè)英文字符hello的時(shí)候,我們可以得到s的長度為5,因?yàn)橛⑽淖帜复?個(gè)字節(jié):
package main
import "fmt"
func main() {
s := "hello"
fmt.Println(len(s)) // 5
}
但是當(dāng)我們打印嗨的時(shí)候,會(huì)打印3個(gè)字節(jié)。因?yàn)槭褂肬TF-8,這個(gè)字符會(huì)被編碼成3個(gè)字節(jié):
package main
import "fmt"
func main() {
s := "嗨"
fmt.Println(len(s)) // 3
}
所以,我們使用len內(nèi)置函數(shù)輸出的并不是字符數(shù),而是字節(jié)數(shù)。
下面看一個(gè)有趣的例子,我們都知道漢字符使用3個(gè)字節(jié)編碼,分別是0xE5, 0x97, 0xA8。我們運(yùn)行下面代碼會(huì)得到漢字嗨:
package main
import "fmt"
func main() {
s := string([]byte{0xE5, 0x97, 0xA8})
fmt.Println(s) // 嗨
}
所以我們需要知道:
- 字符集是一組字符,而編碼描述了如何將字符集轉(zhuǎn)換為二進(jìn)制
- 在 Go 中,字符串引用任意字節(jié)的不可變切片
- Go 源碼使用 UTF-8 編碼。 因此,所有字符串文字都是 UTF-8 字符串。 但是因?yàn)樽址梢园我庾止?jié),如果它是從其他地方(不是源碼)獲得的,則不能保證它是基于 UTF-8 編碼的
- 使用 UTF-8,一個(gè) Unicode 字符可以編碼為 1 到 4 個(gè)字節(jié)
- 在 Go 中對(duì)字符串使用 len 返回字節(jié)數(shù),而不是字符數(shù)
2、字符串遍歷
我們?cè)陂_發(fā)中經(jīng)常會(huì)用到對(duì)字符串進(jìn)行遍歷的場景。 也許我們想對(duì)字符串中的每個(gè) rune 執(zhí)行一個(gè)操作,或者實(shí)現(xiàn)一個(gè)自定義函數(shù)來搜索特定的子字符串。 在這兩種情況下,我們都必須遍歷字符串的不同字符。 但往往會(huì)得到讓我們意想不到的結(jié)果。
我們看下下面的例子,打印一個(gè)字符串中的不同字符和對(duì)應(yīng)的位置:
package main
import "fmt"
func main() {
s := "h嗨llo"
for i := range s {
fmt.Printf("字符位置 %d: %c\n", i, s[i])
}
fmt.Printf("len=%d\n", len(s))
}
輸出結(jié)果:
go run 7.go
字符位置 0: h
字符位置 1: å
字符位置 4: l
字符位置 5: l
字符位置 6: o
len=7
我們想要的效果是通過遍歷字符串,打印出每個(gè)字符的索引。但是我們卻得到了一個(gè)特殊的字符å,其實(shí)我們想要的是嗨。
但是打印的字節(jié)數(shù)是符合我們的預(yù)期的,因?yàn)猷耸且粋€(gè)中文占用了3個(gè)字節(jié),所以len返回的是7。
3、字符串中的字符數(shù)
如果我們想要正確的獲取字符串的字符數(shù),可以使用go中的utf8包:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "h嗨llo"
for i := range s {
fmt.Printf("字符位置 %d: %c\n", i, s[i])
}
fmt.Printf("len=%d\n", len(s))
fmt.Printf(" rune len=%d\n", utf8.RuneCountInString(s)) // 獲取字符數(shù)
}
輸出結(jié)果:
go run 7.go
字符位置 0: h
字符位置 1: å
字符位置 4: l
字符位置 5: l
字符位置 6: o
len=7
rune len=5
在這個(gè)例子中,可以看到,我們確實(shí)遍歷了5次,也就是對(duì)應(yīng)字符串的5個(gè)字符。但是我們獲取到的索引其實(shí)是對(duì)應(yīng)每個(gè)字符的起始位置。像下面這樣

那我們?nèi)绾未蛴〕稣_的結(jié)果呢?我們稍微修改下代碼:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "h嗨llo"
for i, v := range s { // 此處改為獲取v,可以獲取到字符本身
fmt.Printf("字符位置 %d: %c\n", i, v)
}
fmt.Printf("len=%d\n", len(s))
fmt.Printf(" rune len=%d\n", utf8.RuneCountInString(s))
}
輸出結(jié)果:
go run 7.go
字符位置 0: h
字符位置 1: 嗨
字符位置 4: l
字符位置 5: l
字符位置 6: o
len=7
rune len=5
另外一種方法就是把字符串轉(zhuǎn)換成rune切片,這樣也會(huì)正確打印結(jié)果:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "h嗨llo"
b := []rune(s)
for i := range b {
fmt.Printf("字符位置 %d: %c\n", i, b[i])
}
fmt.Printf("len=%d\n", len(s))
fmt.Printf(" rune len=%d\n", utf8.RuneCountInString(s))
}
輸出結(jié)果:
go run 7.go
字符位置 0: h
字符位置 1: 嗨
字符位置 2: l
字符位置 3: l
字符位置 4: o
len=7
rune len=5
下面是rune切片遍歷的過程(中間省略了將字節(jié)轉(zhuǎn)換為rune的過程,需要遍歷字節(jié),復(fù)雜度為O(n))

4、字符串trim
開發(fā)中我們經(jīng)常會(huì)遇到去除字符串頭部或者尾部字符的操作。比如我們現(xiàn)在有個(gè)字符串xohelloxo,現(xiàn)在我們想去除尾部的xo,可能我們會(huì)像下面這樣寫:
package main
import (
"fmt"
"strings"
)
func main() {
s := "xohelloxo"
s = strings.TrimRight(s, "xo")
fmt.Println(s)
}
輸出結(jié)果:
go run 7.go
xohell
可以看到這不是我們期望的結(jié)果。我們可以看下TrimRight的工作原理:
- 從右側(cè)取出第一個(gè)字符o,判斷是否在xo中,在就移除
- 重復(fù)步驟1,知道不符合條件
所以就可以解釋通了。當(dāng)然和它相似的TrimLeft和Trim也是一樣的原理。
如果我們只想刪除最后xo可以使用TrimSuffix函數(shù):
package main
import (
"fmt"
"strings"
)
func main() {
s := "xohelloxo"
s = strings.TrimSuffix(s, "xo")
fmt.Println(s)
}
輸出結(jié)果:
go run 7.go
xohello
當(dāng)然也有對(duì)應(yīng)的從前面刪除的函數(shù)TrimPrefix。
5、字符串連接
開發(fā)中我們經(jīng)常會(huì)用到連接字符串的操作,在go中我們一般有2種方式。
我們先看下+號(hào)連接的方式:
package main
import (
"fmt"
"strings"
)
func implode(values []string, operate string) string {
s := ""
for _, value := range values {
s += operate
s += value
}
s = strings.TrimPrefix(s, operate)
return s
}
func main() {
a := []string{"hello", "world"}
s := implode(a, " ")
fmt.Println(s)
}
輸出結(jié)果:
go run 7.go
hello world
這種方式的缺點(diǎn)就是,由于字符串的不變性,每次+號(hào)賦值的時(shí)候s不會(huì)被更新,而是重新分配內(nèi)存,所以這種方式對(duì)性能有很大影響。
還有一種方式就是使用strings.Builder:
package main
import (
"fmt"
"strings"
)
func implode(values []string, operate string) string {
sb := strings.Builder{}
for _, value := range values {
_, _ = sb.WriteString(operate)
_, _ = sb.WriteString(value)
}
s := strings.TrimPrefix(sb.String(), operate)
return s
}
func main() {
a := []string{"hello", "world"}
s := implode(a, " ")
fmt.Println(s)
}
輸出結(jié)果:
go run 7.go
hello world
首先,我們創(chuàng)建了一個(gè) strings.Builder 結(jié)構(gòu)。 在每次遍歷中,我們通過調(diào)用 WriteString 方法構(gòu)造結(jié)果字符串,該方法將 value 的內(nèi)容附加到其內(nèi)部緩沖區(qū),從而最大限度地減少內(nèi)存復(fù)制。
WriteString 的第二個(gè)參數(shù)返回的是error,但是error的值會(huì)一直為nil。 之所以有第二個(gè)error參數(shù)是因?yàn)槲?strings.Builder 實(shí)現(xiàn)了 io.StringWriter 接口,它包含一個(gè)方法:WriteString(s string) (n int, err error)。
我們看下WriteString的內(nèi)部是什么樣的:
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
我們可以看到b.buf是一個(gè)字節(jié)切片,而里面的實(shí)現(xiàn)是使用了append方法。我們知道如果切片很大,使用append會(huì)讓底層數(shù)組不斷擴(kuò)容,影響代碼執(zhí)行效率。
我們知道解決這個(gè)問題的方法是,如果事先知道切片的大小,我們可以在初始化的時(shí)候就分配好切片的容量。
所以上面的字符串連接還有一種優(yōu)化方案:
package main
import (
"fmt"
"strings"
)
func implode(values []string, operate string) string {
total := 0
for i := 0; i < len(values); i++ {
total += len(values[i])
}
total += len(operate) * len(values)
sb := strings.Builder{}
sb.Grow(total) // 這里會(huì)重新分配b.buf的長度和容量
for _, value := range values {
_, _ = sb.WriteString(operate)
_, _ = sb.WriteString(value)
}
s := strings.TrimPrefix(sb.String(), operate)
return s
}
func main() {
a := []string{"hello", "world"}
s := implode(a, " ")
fmt.Println(s)
}
輸出結(jié)果:
go run 7.go
hello world
6、字節(jié)切片轉(zhuǎn)字符串
需要明確的是,字節(jié)切片轉(zhuǎn)換成字符串,需要復(fù)制一份副本出來??梢酝ㄟ^下面的代碼做驗(yàn)證:
b := []byte{'a', 'b', 'c'}
s := string(b)
b[1] = 'x'
fmt.Println(s)
事實(shí)上,上面將會(huì)輸出abc而不是axc。所以字節(jié)切片到字符串的轉(zhuǎn)換是有開銷的。
但是我們開發(fā)中經(jīng)常用到的包iio.Read之類的,入?yún)⒒蛘叻祷亟?jīng)常是字節(jié)切片類型。而我們調(diào)用這些函數(shù)時(shí)經(jīng)常是以字符串的形式,導(dǎo)致我們不得不做一些字節(jié)切片刀字符串的轉(zhuǎn)換。
所以結(jié)論是,當(dāng)我們需要使用字符串作為入?yún)⒒蛘叻祷貢r(shí),我們首先要考慮的是能用字節(jié)切片的就用字節(jié)切片。
到此這篇關(guān)于詳解Golang中字符串的使用的文章就介紹到這了,更多相關(guān)Golang字符串內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang并發(fā)繞不開的重要組件之Goroutine詳解
Goroutine、Channel、Context、Sync都是Golang并發(fā)編程中的幾個(gè)重要組件,這篇文中主要為大家介紹了Goroutine的相關(guān)知識(shí),需要的可以參考一下2023-06-06
基于go+vue實(shí)現(xiàn)的golang每日新聞數(shù)據(jù)瀏覽與檢索平臺(tái)(推薦)
gonews是基于 go+vue 實(shí)現(xiàn)的golang每日新聞瀏覽與檢索平臺(tái),本文通過實(shí)例代碼給大家講解,介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友參考下吧2018-01-01
golang使用redis實(shí)現(xiàn)全文搜索功能詳解
這篇文章主要為大家詳細(xì)介紹了golang如何使用redis實(shí)現(xiàn)全文搜索功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-02-02
golang結(jié)構(gòu)體與json格式串實(shí)例代碼
本文通過實(shí)例代碼給大家介紹了golang結(jié)構(gòu)體與json格式串的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-10-10
Golang利用Template模板動(dòng)態(tài)生成文本
Go語言中的Go?Template是一種用于生成文本輸出的簡單而強(qiáng)大的模板引擎,它提供了一種靈活的方式來生成各種格式的文本,下面我們就來看看具體如何使用Template實(shí)現(xiàn)動(dòng)態(tài)文本生成吧2023-09-09
golang?gorm學(xué)習(xí)之如何指定數(shù)據(jù)表
在sql中首先要指定是從哪張表中查詢,所以這篇文章小編就來帶大家一起看一下gorm是如何根據(jù)model來自動(dòng)解析表名的,感興趣的小伙伴可以了解下2023-08-08

