### 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)將看起來像是繫統路徑的前綴刪除，同時將看似文件類型的後綴名部分刪除：

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

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

```Go
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庫函數：

```Go
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”。這個版本隻適用於整數類型；支持浮點數類型的支持留作練習。

```Go
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之間可以相互轉換：

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

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

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

```Go
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包中也對應的六個函數：

```Go
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變量併不需要處理化，因爲零值也是有效的：

```Go
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：** 編寫一個函數，判斷兩個字符串是否是是相互打亂的，也就是説它們有着相同的字符，但是對應不同的順序。




