Go語言之字符串八

字符串在 Go 語言中以原生數據類型出現,使用字符串就像使用其他原生數據類型(int、bool、float32、float64 等)一樣。
字符串的值為雙引號中的內容,可以在 Go 語言的源碼中直接添加非 ASCII 碼字符,代碼如下:

str := "hello world"
ch := "中文"

字符串轉義符

Go 語言的字符串常見轉義符包含回車、換行、單雙引號、制表符等,如下表所示。
轉移符 含 義
\r 回車符(返回行首)
\n 換行符(直接跳到下一行的同列位置)
\t 制表符
' 單引號
" 雙引號
\ 反斜杠
在 Go 語言源碼中使用轉義符代碼如下:

package main
import (
    "fmt"
)
func main() {
    fmt.Println("str := \"c:\\Go\\bin\\go.exe\"")
}

代碼運行結果:

str := "c:\Go\bin\go.exe"

這段代碼中將雙引號和反斜杠“\”進行轉義。

字符串實現基于 UTF-8 編碼

Go 語言里的字符串的內部實現使用 UTF-8 編碼。通過 rune 類型,可以方便地對每個 UTF-8 字符進行訪問。當然,Go 語言也支持按傳統的 ASCII 碼方式進行逐字符訪問。

定義多行字符串

在源碼中,將字符串的值以雙引號書寫的方式是字符串的常見表達方式,被稱為字符串字面量(string literal)。這種雙引號字面量不能跨行。如果需要在源碼中嵌入一個多行字符串時,就必須使用`字符,代碼如下:

const str = ` 第一行
第二行
第三行
\r\n
`
fmt.Println(str)

代碼運行結果:

第一行
第二行
第三行
\r\n

`叫反引號,就是鍵盤上 1 鍵左邊的鍵,兩個反引號間的字符串將被原樣賦值到 str 變量中。
在這種方式下,反引號間換行將被作為字符串中的換行,但是所有的轉義字符均無效,文本將會原樣輸出。

const codeTemplate = `// Generated by github.com/davyxu/cellnet/
protoc-gen-msg
// DO NOT EDIT!{{range .Protos}}
// Source: {{.Name}}{{end}}

package {{.PackageName}}

{{if gt .TotalMessages 0}}
import (
    "github.com/davyxu/cellnet"
    "reflect"
    _ "github.com/davyxu/cellnet/codec/pb"
)
{{end}}

func init() {
    {{range .Protos}}
    // {{.Name}}{{range .Messages}}
    cellnet.RegisterMessageMeta("pb","{{.FullName}}", reflect.TypeOf((*{{.Name}})(nil)).Elem(), {{.MsgID}})    {{end}}
    {{end}}
}
`

這段代碼只定義了一個常量 codeTemplate,類型為字符串,使用`定義。字符串的內容為一段代碼生成中使用到的 Go 源碼格式。
`間的所有代碼均不會被編譯器識別,而只是作為字符串的一部分。

字符串的長度

Go 語言的內建函數 len(),可以用來獲取切片、字符串、通道(channel)等的長度。下面的代碼可以用 len() 來獲取字符串的長度。

tip1 := "genji is a ninja"
fmt.Println(len(tip1))
tip2 := "忍者" //一個中文是3個字符
fmt.Println(len(tip2))

程序輸出如下:
16
6
len() 函數的返回值的類型為 int,表示字符串的 ASCII 字符個數或字節長度。

  • 輸出中第一行的 16 表示 tip1 的字符個數為 16。
  • 輸出中第二行的 6 表示 tip2 的字符格式,也就是“忍者”的字符個數是 6,然而根據習慣,“忍者”的字符個數應該是 2。

這里的差異是由于 Go 語言的字符串都以 UTF-8 格式保存,每個中文占用 3 個字節,因此使用 len() 獲得兩個中文文字對應的 6 個字節。
如果希望按習慣上的字符個數來計算,就需要使用 Go 語言中 UTF-8 包提供的 RuneCountInString() 函數,統計 Uncode 字符數量。
下面的代碼展示如何計算UTF-8的字符個數。

fmt.Println(utf8.RuneCountInString("忍者"))
fmt.Println(utf8.RuneCountInString("龍忍出鞘,fight!"))

程序輸出如下:
2
11
一般游戲中在登錄時都需要輸入名字,而名字一般有長度限制。考慮到國人習慣使用中文做名字,就需要檢測字符串 UTF-8 格式的長度。
總結
ASCII 字符串長度使用 len() 函數。
Unicode 字符串長度使用 utf8.RuneCountInString() 函數。

字符串的fmt.Sprintf(格式化輸出)

格式化在邏輯中非常常用。使用格式化函數,要注意寫法:
fmt.Sprintf(格式化樣式, 參數列表…)

  • 格式化樣式:字符串形式,格式化動詞以%開頭。
  • 參數列表:多個參數以逗號分隔,個數必須與格式化樣式中的個數一一對應,否則運行時會報錯。
    Go 語言中,格式化的命名延續C語言風格:
var progress = 2
var target = 8

// 兩參數格式化
title := fmt.Sprintf("已采集%d個藥草, 還需要%d個完成任務", progress, target)

fmt.Println(title)

pi := 3.14159
// 按數值本身的格式輸出
variant := fmt.Sprintf("%v %v %v", "月球基地", pi, true)

fmt.Println(variant)

// 匿名結構體聲明, 并賦予初值
profile := &struct {
    Name string
    HP   int
}{
    Name: "rat",
    HP:   150,
}

fmt.Printf("使用'%%+v' %+v\n", profile)

fmt.Printf("使用'%%#v' %#v\n", profile)

fmt.Printf("使用'%%T' %T\n", profile)

代碼輸出如下:

已采集2個藥草, 還需要8個完成任務
"月球基地" 3.14159 true
使用'%+v' &{Name:rat HP:150}
使用'%#v' &struct { Name string; HP int }{Name:"rat", HP:150}
使用'%T' *struct { Name string; HP int }C語言中, 使用%d代表整型參數

下表中標出了常用的一些格式化樣式中的動詞及功能。
表:字符串格式化時常用動詞及功能

動  詞    功  能
%d int變量
%x, %o, %b 分別為16進制,8進制,2進制形式的int
%f, %g, %e 浮點數: 3.141593 3.141592653589793 3.141593e+00
%t 布爾變量:true 或 false
%c rune (Unicode碼點),Go語言里特有的Unicode字符類型
%s string
%q 帶雙引號的字符串 "abc" 或 帶單引號的 rune 'c'
%v 會將任意變量以易讀的形式打印出來
%T 打印變量的類型
%% 字符型百分比標志(%符號本身,沒有其他操作)

遍歷字符串——獲取每一個字符串元素

遍歷字符串有下面兩種寫法。
遍歷每一個ASCII字符
遍歷 ASCII 字符使用 for 的數值循環進行遍歷,直接取每個字符串的下標獲取 ASCII 字符,如下面的例子所示。

theme := "狙擊 start"
for i := 0; i < len(theme); i++ {
    fmt.Printf("ascii: %c  %d\n", theme[i], theme[i])
}

程序輸出如下:

ascii: ?  231
ascii:     139
ascii:     153
ascii: ?  229
ascii:     135
ascii: ?  187
ascii:    32
ascii: s  115
ascii: t  116
ascii: a  97
ascii: r  114
ascii: t  116

這種模式下取到的漢字“慘不忍睹”。由于沒有使用 Unicode,漢字被顯示為亂碼。
按Unicode字符遍歷字符串
同樣的內容:

theme := "狙擊 start"
for _, s := range theme {
    fmt.Printf("Unicode: %c  %d\n", s, s)
}

程序輸出如下:
Unicode: 狙 29401
Unicode: 擊 20987
Unicode: 32
Unicode: s 115
Unicode: t 116
Unicode: a 97
Unicode: r 114
Unicode: t 116
可以看到,這次漢字可以正常輸出了。
總結

  • ASCII 字符串遍歷直接使用下標。
  • Unicode 字符串遍歷用 for range。

字符串截取(獲取字符串的某一段字符)

獲取字符串的某一段字符是開發中常見的操作,我們一般將字符串中的某一段字符稱做子串(substring)。

下面例子中使用 strings.Index() 函數在字符串中搜索另外一個子串,代碼如下:

tracer := "死神來了, 死神bye bye"
comma := strings.Index(tracer, ", ")
pos := strings.Index(tracer[comma:], "死神")
fmt.Println(comma, pos, tracer[comma+pos:])

程序輸出如下:
12 3 死神bye bye
代碼說明如下:

  1. 第 2 行嘗試在 tracer 的字符串中搜索中文的逗號,返回的位置存在 comma 變量中,類型是 int,表示從 tracer 字符串開始的 ASCII 碼位置。

strings.Index() 函數并沒有像其他語言一樣,提供一個從某偏移開始搜索的功能。不過我們可以對字符串進行切片操作來實現這個邏輯。

  1. 第4行中,tracer[comma:] 從 tracer 的 comma 位置開始到 tracer 字符串的結尾構造一個子字符串,返回給 string.Index() 進行再索引。得到的 pos 是相對于 tracer[comma:] 的結果。

comma 逗號的位置是 12,而 pos 是相對位置,值為 3。我們為了獲得第二個“死神”的位置,也就是逗號后面的字符串,就必須讓 comma 加上 pos 的相對偏移,計算出 15 的偏移,然后再通過切片 tracer[comma+pos:] 計算出最終的子串,獲得最終的結果:“死神bye bye”。

總結

字符串索引比較常用的有如下幾種方法:

  • strings.Index:正向搜索子字符串。
  • strings.LastIndex:反向搜索子字符串。
  • 搜索的起始位置可以通過切片偏移制作。

修改字符串

Go 語言的字符串無法直接修改每一個字符元素,只能通過重新構造新的字符串并賦值給原來的字符串變量實現。請參考下面的代碼:

angel := "Heros never die"
angleBytes := []byte(angel)
for i := 5; i <= 10; i++ {
    angleBytes[i] = ' '
}
fmt.Println(string(angleBytes))

程序輸出如下:
Heros die
代碼說明如下:

  • 在第 2 行中,將字符串轉為字符串數組。
  • 第 3~6 行利用循環,將 never 單詞替換為空格。
    最后打印結果。
    感覺我們通過代碼達成了修改字符串的過程,但真實的情況是:Go 語言中的字符串和其他高級語言(Java、C#)一樣,默認是不可變的(immutable)。

字符串不可變有很多好處,如天生線程安全,大家使用的都是只讀對象,無須加鎖;再者,方便內存共享,而不必使用寫時復制(Copy On Write)等技術;字符串 hash 值也只需要制作一份。

所以說,代碼中實際修改的是 []byte,[]byte 在 Go 語言中是可變的,本身就是一個切片。

在完成了對 []byte 操作后,在第 9 行,使用 string() 將 []byte 轉為字符串時,重新創造了一個新的字符串。
總結

  • Go 語言的字符串是不可變的。
  • 修改字符串時,可以將字符串轉換為 []byte 進行修改。
  • []byte 和 string 可以通過強制類型轉換互轉。

字符串拼接(連接)

連接字符串這么簡單,還需要學嗎?確實,Go 語言和大多數其他語言一樣,使用+對字符串進行連接操作,非常直觀。

但問題來了,好的事物并非完美,簡單的東西未必高效。除了加號連接字符串,Go 語言中也有類似于 StringBuilder 的機制來進行高效的字符串連接,例如:

hammer := "吃我一錘"
sickle := "死吧"
// 聲明字節緩沖
var stringBuilder bytes.Buffer
// 把字符串寫入緩沖
stringBuilder.WriteString(hammer)
stringBuilder.WriteString(sickle)
// 將緩沖以字符串形式輸出
fmt.Println(stringBuilder.String())

bytes.Buffer 是可以緩沖并可以往里面寫入各種字節數組的。字符串也是一種字節數組,使用 WriteString() 方法進行寫入。
將需要連接的字符串,通過調用 WriteString() 方法,寫入 stringBuilder 中,然后再通過 stringBuilder.String() 方法將緩沖轉換為字符串。

Base64編碼——電子郵件的基礎編碼格式

Base64 編碼是常見的對 8 比特字節碼的編碼方式之一。Base64 可以使用 64 個可打印字符來表示二進制數據,電子郵件就是使用這種編碼。

Go 語言的標準庫自帶了 Base64 編碼算法,通過幾行代碼就可以對數據進行編碼,示例代碼如下。

package main
import (
    "encoding/base64"
    "fmt"
)
func main() {
    // 需要處理的字符串
    message := "Away from keyboard. https://golang.org/"
    // 編碼消息
    encodedMessage := base64.StdEncoding.EncodeToString([]byte (message))
    // 輸出編碼完成的消息
    fmt.Println(encodedMessage)
    // 解碼消息
    data, err := base64.StdEncoding.DecodeString(encodedMessage)
    // 出錯處理
    if err != nil {
        fmt.Println(err)
    } else {
        // 打印解碼完成的數據
        fmt.Println(string(data))
    }
}

代碼說明如下:
第 11 行為需要編碼的消息,消息可以是字符串,也可以是二進制數據。
第 14 行,base64 包有多種編碼方法,這里使用 base64.StdEnoding 的標準編碼方法進行編碼。傳入的字符串需要轉換為字節數組才能供這個函數使用。
第 17 行,編碼完成后一定會輸出字符串類型,打印輸出。
第 20 行,解碼時可能會發生錯誤,使用 err 變量接收錯誤。
第 24 行,出錯時,打印錯誤。
第 27 行,正確時,將返回的字節數組([]byte)轉換為字符串。

本文學習來源于C語言中文網>Go語言教程

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容