gopl-zh.github.com/ch3/ch3-05-4.md

6.3 KiB
Raw Blame History

3.5.4. 字符串和Byte切片

標準庫中有四個包對字符串處理尤爲重要bytes、strings、strconv和unicode包。strings包提供了許多如字符串的査詢、替換、比較、截斷、拆分和合併等功能。

bytes包也提供了很多類似功能的函數但是針對和字符串有着相同結構的[]byte類型。因爲字符串是隻讀的因此逐步構建字符串會導致很多分配和複製。在這種情況下使用bytes.Buffer類型將會更有效稍後我們將展示。

strconv包提供了布爾型、整型數、浮點數和對應字符串的相互轉換還提供了雙引號轉義相關的轉換。

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等類似功能它們用於給字符分類。每個函數有一個單一的rune類型的參數然後返迴一個布爾值。而像ToUpper和ToLower之類的轉換函數將用於rune字符的大小寫轉換。所有的這些函數都是遵循Unicode標準定義的字母、數字等分類規范。strings包也有類似的函數它們是ToUpper和ToLower將原始字符串的每個字符都做相應的轉換然後返迴新的字符串。

下面例子的basename函數靈感於Unix shell的同名工具。在我們實現的版本中basename(s)將看起來像是繫統路徑的前綴刪除,同時將看似文件類型的後綴名部分刪除:

fmt.Println(basename("a/b/c.go")) // "c"
fmt.Println(basename("c.d.go"))   // "c.d"
fmt.Println(basename("abc"))      // "abc"

第一個版本併沒有使用任何庫,全部手工硬編碼實現:

gopl.io/ch3/basename1
// basename removes directory components and a .suffix.
// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c
func basename(s string) string {
	// Discard last '/' and everything before.
	for i := len(s) - 1; i >= 0; i-- {
		if s[i] == '/' {
			s = s[i+1:]
			break
		}
	}
	// Preserve everything before last '.'.
	for i := len(s) - 1; i >= 0; i-- {
		if s[i] == '.' {
			s = s[:i]
			break
		}
	}
	return s
}

簡化個版本使用了strings.LastIndex庫函數

gopl.io/ch3/basename2

func basename(s string) string {
	slash := strings.LastIndex(s, "/") // -1 if "/" not found
	s = s[slash+1:]
	if dot := strings.LastIndex(s, "."); dot >= 0 {
		s = s[:dot]
	}
	return s
}

path和path/filepath包提供了關於文件路徑名更一般的函數操作。使用斜槓分隔路徑可以在任何操作繫統上工作。斜槓本身不應該用於文件名但是在其他一些領域可能會用於文件名例如URL路徑組件。相比之下path/filepath包則使用操作繫統本身的路徑規則例如POSIX繫統使用/foo/bar而Microsoft Windows使用c:\foo\bar等。

讓我們繼續另一個字符串的例子。函數的功能是將一個表示整值的字符串每隔三個字符插入一個逗號分隔符例如“12345”處理後成爲“12,345”。這個版本隻適用於整數類型支持浮點數類型的支持留作練習。

gopl.io/ch3/comma

// comma inserts commas in a non-negative decimal integer string.
func comma(s string) string {
	n := len(s)
	if n <= 3 {
		return s
	}
	return comma(s[:n-3]) + "," + s[n-3:]
}

輸入comma函數的參數是一個字符串。如果輸入字符串的長度小於或等於3的話則不需要插入逗分隔符。否則comma函數將在最後三個字符前位置將字符串切割爲兩個兩個子串併插入逗號分隔符然後通過遞歸調用自身來出前面的子串。

一個字符串是包含的隻讀字節數組一旦創建是不可變的。相比之下一個字節slice的元素則可以自由地脩改。

字符串和字節slice之間可以相互轉換

s := "abc"
b := []byte(s)
s2 := string(b)

從概念上講,一個[]byte(s)轉換是分配了一個新的字節數組用於保存字符串數據的拷貝然後引用這個底層的字節數組。編譯器的優化可以避免在一些場景下分配和複製字符串數據但總的來説需要確保在變量b被脩改的情況下原始的s字符串也不會改變。將一個字節slice轉到字符串的string(b)操作則是構造一個字符串拷貝以確保s2字符串是隻讀的。

爲了避免轉換中不必要的內存分配bytes包和strings同時提供了許多實用函數。下面是strings包中的六個函數

func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string

bytes包中也對應的六個函數

func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte

它們之間唯一的區别是字符串類型參數被替換成了字節slice類型的參數。

bytes包還提供了Buffer類型用於字節slice的緩存。一個Buffer開始是空的但是隨着string、byte或[]byte等類型數據的寫入可以動態增長一個bytes.Buffer變量併不需要處理化因爲零值也是有效的

gopl.io/ch3/printints

// intsToString is like fmt.Sprintf(values) but adds commas.
func intsToString(values []int) string {
	var buf bytes.Buffer
	buf.WriteByte('[')
	for i, v := range values {
		if i > 0 {
			buf.WriteString(", ")
		}
		fmt.Fprintf(&buf, "%d", v)
	}
	buf.WriteByte(']')
	return buf.String()
}

func main() {
	fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"
}

當向bytes.Buffer添加任意字符的UTF8編碼時最好使用bytes.Buffer的WriteRune方法但是WriteByte方法對於寫入類似'['和']'等ASCII字符則會更加有效。

bytes.Buffer類型有着很多實用的功能我們在第七章討論接口時將會涉及到我們將看看如何將它用作一個I/O的輸入和輸出對象例如當做Fprintf的io.Writer輸出對象或者當作io.Reader類型的輸入源對象。

練習 3.10 編寫一個非遞歸版本的comma函數使用bytes.Buffer代替字符串鏈接操作。

練習 3.11 完善comma函數以支持浮點數處理和一個可選的正負號的處理。

練習 3.12 編寫一個函數,判斷兩個字符串是否是是相互打亂的,也就是説它們有着相同的字符,但是對應不同的順序。