4.7 strings 和 strconv 包

做为一种基本数据结构,每种语言都有一些对于字符串的预定义处理函数。Go中使用strings包来完成对字符串的主要操作

4.7.1 前缀和后缀

HasPrefix 判断字符串s 是否以prefix 开头:

strings.HasPrefix(s, prefix string) bool

HasSuffix 判断字符串 s 是否以 suffix 结尾:

strings.HasSuffix(s, suffix string) bool

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	
	str := "This is a test"
	fmt.Println(strings.HasPrefix(str, "Th"))
	fmt.Println(strings.HasSuffix(str, "st"))
}

输出:

true
true

4.7.2 字符串包含关系

Contains 判断字符串s是否包含substr:

strings.Contains(s, substr string) bool

例:

package main

import (
	"fmt"
	"strings"
)

func main() {

	str := "This is a test"
	sub := "i"
	fmt.Println(strings.Contains(str, sub))
}

输出:

true

4.7.3判断子字符串或字符在父字符串中出现的位置(索引)

Index 返回字符串str 在字符串s 中的索引(str的第一个字符的索引),-1 表示字符串s不包含字符串str:

strings.Index(s, str string) int

LastIndex 返回字符串 str 在字符串 s 中最后出现位置的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str:

strings.LastIndex(s, str string) int

例:

package main

import (
	"fmt"
	"strings"
)

func main() {

	str := "This is a test"
	fmt.Println(strings.Index(str, "a"))
	fmt.Println(strings.LastIndex(str, "i"))
}

输出:

8
5

如果需要查询非 ASCII 编码的字符在父字符串中的位置,建议使用以下函数来对字符进行定位:

strings.IndexRune(s string, r rune) int

注: 原文为 "If ch is a non-ASCII character use strings.IndexRune(s string, ch int) int."
该方法在最新版本的 Go 中定义为 func IndexRune(s string, r rune) int
实际使用中的第二个参数 rune 可以是 rune 或 int, 例如 strings.IndexRune("chicken", 99) 或 strings.IndexRune("chicken", rune('k'))

4.7.4 字符串替换

Replace 用于将字符串 str 中的前 n 个old 字符串替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new:

strings.Replace(str, old, new string, n int) string

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "This is a test"
	fmt.Println(strings.Replace(str, "i", "I", 1)) //替换第一个i为I
	fmt.Println(strings.Replace(str, "i", "I", -1))
	fmt.Println(strings.Replace(str, "s", "S", 2))

}

输出:

ThIs is a test
ThIs Is a test
ThiS iS a test

4.7.5 统计字符串出现次数

Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数:

strings.Count(s, str string) int

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "abc abc"
	str2 := "ggggggggggg"
	fmt.Println(strings.Count(str, "abc"))
	fmt.Println(strings.Count(str2, "gg"))
}

输出:

2
5

4.7.6 重复字符串

Repeat 用于重复 count 次字符串 s 并返回一个新的字符串:

strings.Repeat(s, count int) string

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "abc"
	res := strings.Repeat(str, 2)
	fmt.Println(res)
}

输出:

abcabc

4.7.7 修改字符串大小写

ToLower 将字符串中的 Unicode 字符全部转换为相应的小写字符:

strings.ToLower(s) string

ToUpper 将字符串中的 Unicode 字符全部转换为相应的大写字符:

strings.ToUpper(s) string

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "abc"
	res := strings.ToUpper(str)
	fmt.Println(res)
}

输出:

ABC

4.7.8 修剪字符串

你可以使用 strings.TrimSpace(s) 来剔除字符串开头和结尾的空白符号;如果你想要剔除指定字符,则可以使用 strings.Trim(s, "cut") 来将开头和结尾的 cut 去除掉。该函数的第二个参数可以包含任何字符,如果你只想剔除开头或者结尾的字符串,则可以使用 TrimLeft 或者 TrimRight 来实现。

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "   abc   "
	fmt.Println(strings.TrimSpace(str))
	str2 := "cut abc cut"
	fmt.Println(strings.Trim(str2, "cut"))
	fmt.Println(strings.TrimLeft(str2, "cut"))
	fmt.Println(strings.TrimRight(str2, "cut"))
}

输出:

abc
 abc 
 abc cut
cut abc 

4.7.9 分割字符串

strings.Fields(s) 将会利用 1 个或多个空白符号来作为动态长度的分隔符将字符串分割成若干小块,并返回一个 slice,如果字符串只包含空白符号,则返回一个长度为 0 的 slice。

strings.Split(s, sep) 用于自定义分割符号来对指定字符串进行分割,同样返回 slice。

因为这 2 个函数都会返回 slice,所以习惯使用 for-range 循环来对其进行处理(第 7.3 节)。

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := "a b     c"
	str2 := "    "
	fmt.Println(strings.Fields(str))
	fmt.Println(strings.Split(str, "     "))
	fmt.Println(strings.Fields(str2))
}

4.7.10 拼接 slice 到字符串

Join 用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串:

strings.Join(sl []string, sep string) string

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	strs := []string{"a", "b", "c", "de"}
	fmt.Println(strings.Join(strs, ""))
	fmt.Println(strings.Join(strs, " "))
	fmt.Println(strings.Join(strs, "¥"))
}

输出:

abcde
a b c de
a¥b¥c¥de

4.7.11 从字符串中读取内容

函数 strings.NewReader(str) 用于生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针,从其它类型读取内容的函数还有:

  • Read() 从 []byte 中读取内容。
  • ReadByte() 和 ReadRune() 从字符串中读取下一个 byte 或者 rune。

例:

package main

import (
	"fmt"
	"strings"
)

func main() {
	r := strings.NewReader("abcdefghijklmn")
	//fmt.Println(r.Len()) //14 输出是未读长度等于字符串长度
	//buf := make([]byte, 5)
	//readLen, err := r.Read(buf)
	//if err != nil {
	//	fmt.Println(err)
	//}
	//fmt.Println("读取到的长度: ", readLen)
	//fmt.Println(string(buf))
	//fmt.Println(r.Len()) //9 读取了5个, 剩余额就是14 -5
	//fmt.Println(r.Size()) //14 字符串的长度

	bufAt := make([]byte, 256)
	readAtLen, err := r.ReadAt(bufAt, 5)
	fmt.Println(readAtLen) //9  剩余未读长度为9
	if err != nil {
		fmt.Println(err)
	}
	//测试下是否影响len() 和 read() 方法
	buf := make([]byte, 5)
	fmt.Println(r.Read(buf)) //5 nil , Read
	fmt.Println(r.Len()) // 9


}

4.7.12 字符串与其它类型的转换

与字符串相关的类型转换都是通过 strconv 包实现的。

该包包含了一些变量用于获取程序运行的操作系统平台下 int 类型所占的位数,如:strconv.IntSize。

任何类型 T 转换为字符串总是成功的。

针对从数字类型转换到字符串,Go 提供了以下函数:

  • strconv.Itoa(i int) string 返回数字 i 所表示的字符串类型的十进制数。
  • strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string 将 64 位浮点型的数字转换为字符串,其中 fmt 表示格式(其值可以是 'b'、'e'、'f' 或 'g'),prec 表示精度,bitSize 则使用 32 表示 float32,用 64 表示 float64。

将字符串转换为其它类型 tp 并不总是可能的,可能会在运行时抛出错误 parsing "…": invalid argument。

针对从字符串类型转换为数字类型,Go 提供了以下函数:

  • strconv.Atoi(s string) (i int, err error) 将字符串转换为 int 型。
  • strconv.ParseFloat(s string, bitSize int) (f float64, err error) 将字符串转换为 float64 型。

利用多返回值的特性,这些函数会返回 2 个值,第 1 个是转换后的结果(如果转换成功),第 2 个是可能出现的错误,因此,我们一般使用以下形式来进行从字符串到其它类型的转换:

val, err = strconv.Atoi(s)

在下面这个示例中,我们忽略可能出现的转换错误:

例:

package main

import (
	"fmt"
	"strconv"
)

func main() {

	var orig string = "666"
	var an int
	var news string
	fmt.Printf("This size of init is %d \n", strconv.IntSize) //64 位系统是64 , 32位系统是32

	an, _ = strconv.Atoi(orig)
	an += 5
	fmt.Println(an)
	news = strconv.Itoa(an)
	fmt.Println(news)
}

4.8 时间和日期

time 包为我们提供了一个数据类型 time.Time(作为值使用)以及显示和测量时间和日期的功能函数。

当前时间可以使用 time.Now() 获取,或者使用 t.Day()、t.Minute() 等等来获取时间的一部分;你甚至可以自定义时间格式化字符串,例如: fmt.Printf("%02d.%02d.%4d\n", t.Day(), t.Month(), t.Year()) 将会输出 21.07.2011。

Duration 类型表示两个连续时刻所相差的纳秒数,类型为 int64。Location 类型映射某个时区的时间,UTC 表示通用协调世界时间。

包中的一个预定义函数 func (t Time) Format(layout string) string 可以根据一个格式化字符串来将一个时间 t 转换为相应格式的字符串,你可以使用一些预定义的格式,如:time.ANSIC 或 time.RFC822。

一般的格式化设计是通过对于一个标准时间的格式化描述来展现的,这听起来很奇怪,但看下面这个例子你就会一目了然:

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	fmt.Println(t) //2022-04-01 15:38:44.815669 +0800 CST m=+0.000062793
	fmt.Println(t.Format("02 Jan 2006 15:04")) //01 Apr 2022 15:38
	fmt.Printf("%02d %02d %4d", t.Day(), t.Month(), t.Year()) //01 04 2022
}

4.9 指针

不像 Java 和 .NET,Go 语言为程序员提供了控制数据结构的指针的能力;但是,你不能进行指针运算。通过给予程序员基本内存布局,Go 语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这些对构建运行良好的系统是非常重要的:指针对于性能的影响是不言而喻的,而如果你想要做的是系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。

由于各种原因,指针对于使用面向对象编程的现代程序员来说可能显得有些陌生,不过我们将会在这一小节对此进行解释,并在未来的章节中展开深入讨论。

程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如:0x6b0820 或 0xf84001d7f0。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

下面的代码片段(示例 4.9 pointer.go)可能输出 An integer: 5, its location in memory: 0x6b0820(这个值随着你每次运行程序而变化)。

var i1 = 5
fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)

这个地址可以存储在一个叫做指针的特殊数据类型中,在本例中这是一个指向 int 的指针,即 i1:此处使用 *int 表示。如果我们想调用指针 intP,我们可以这样声明它:

这个地址可以存储在一个叫做指针的特殊数据类型中,在本例中这是一个指向 int 的指针,即 i1:此处使用 *int 表示。如果我们想调用指针 intP,我们可以这样声明它:

var intP *int

然后使用 intP = &i1 是合法的,此时 intP 指向 i1。

(指针的格式化标识符为 %p)

intP 存储了 i1 的内存地址;它指向了 i1 的位置,它引用了变量 i1。

一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上 * 号(前缀)来获取指针所指向的内容,这里的 * 号是一个类型更改器。使用一个指针引用一个值被称为间接引用。

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

一个指针变量通常缩写为 ptr。

注意事项

在书写表达式类似 var p type 时,切记在 * 号和指针名称间留有一个空格,因为 - var ptype 是语法正确的,但是在更复杂的表达式中,它容易被误认为是一个乘法表达式!

符号 * 可以放在一个指针前,如 *intP,那么它将得到这个指针指向地址上所存储的值;这被称为反引用(或者内容或者间接引用)操作符;另一种说法是指针转移。

对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)。

现在,我们应当能理解 pointer.go 的全部内容及其输出:

示例 4.21 pointer.go:

package main
import "fmt"
func main() {
	var i1 = 5
	fmt.Printf("An integer: %d, its location in memory: %p\n", i1, &i1)
	var intP *int
	intP = &i1
	fmt.Printf("The value at memory location %p is %d\n", intP, *intP)
}

输出:

An integer: 5, its location in memory: 0x24f0820
The value at memory location 0x24f0820 is 5

我们可以用下图来表示内存使用的情况:

程序 string_pointer.go 为我们展示了指针对string的例子。

它展示了分配一个新的值给 *p 并且更改这个变量自己的值(这里是一个字符串)。

示例 4.22 string_pointer.go

package main
import "fmt"
func main() {
	s := "good bye"
	var p *string = &s
	*p = "ciao"
	fmt.Printf("Here is the pointer p: %p\n", p) // prints address
	fmt.Printf("Here is the string *p: %s\n", *p) // prints string
	fmt.Printf("Here is the string s: %s\n", s) // prints same string
}

输出:

Here is the pointer p: 0x2540820
Here is the string *p: ciao
Here is the string s: ciao

通过对 *p 赋另一个值来更改“对象”,这样 s 也会随之更改。

内存示意图如下:

注意事项

你不能获取字面量或常量的地址,例如:

const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10

所以说,Go 语言和 C、C++ 以及 D 语言这些低级(系统)语言一样,都有指针的概念。但是对于经常导致 C 语言内存泄漏继而程序崩溃的指针运算(所谓的指针算法,如:pointer+2,移动指针指向字符串的字节数或数组的某个位置)是不被允许的。Go 语言中的指针保证了内存安全,更像是 Java、C# 和 VB.NET 中的引用。

因此 p++ 在 Go 语言的代码中是不合法的。

指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。

另一方面(虽然不太可能),由于一个指针导致的间接引用(一个进程执行了另一个地址),指针的过度频繁使用也会导致性能下降。

指针也可以指向另一个指针,并且可以进行任意深度的嵌套,导致你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。

如我们所见,在大多数情况下 Go 语言可以使程序员轻松创建指针,并且隐藏间接引用,如:自动反向引用。

对一个空指针的反向引用是不合法的,并且会使程序崩溃:

示例 4.23 testcrash.go:

package main
func main() {
	var p *int = nil
	*p = 0
}
// in Windows: stops only with: <exit code="-1073741819" msg="process crashed"/>
// runtime error: invalid memory address or nil pointer dereference