最新消息:XAMPP默认安装之后是很不安全的,我们只需要点击左方菜单的 "安全"选项,按照向导操作即可完成安全设置。

Go语言学习笔记03:内置基本数据类型之字符串类型

XAMPP新闻 admin 151浏览 0评论

一、字符串编码

在介绍 Go 的字符串时,我想先重点介绍一下 Unicode 编码和 UTF-8 编码的关系。上世纪 60 年代,美国指定了一套字符串编码,对 26 个英语字符和二进制位做了对应关系,也就是我们所熟知的 ASCII 码。

但是随着计算机的发展,这世界不仅仅只有英文字符,汉字、日文、韩文等等各种各样的字符都需要在计算机中找到对应的二进制位进行对应。于是大佬们制定了 Unicode 编码,将世界上所有符号都容纳了进去,每个符号都对应一个独有的编码。

不过 Unicode 只是规定了符号的编码,比如“奶昔”的 Unicode 编码是\u5976\u6614,数字是由十六进制表示的,所以它所对应的二进制位 0b10110010_1110110 和 0b11001100_0010100,如何在内存中存储又是另一个问题了,毕竟 Unicode 编码只规定了字符的编码是什么,没有说计算机内存里该怎么存。像奶 和 昔 这两个字至少需要两个字节进行存储。

此外还有一个问题,如何将 Unicode 编码和 ASCII 区分开来,要知道 ASCII 码一共只有 128 个,一个字节就可以存下。而在内存中存的都是二进制 0 和 1,如何才能分清楚这 8 位是一个字符,这 16 位是一个字符呢?因为这个问题就衍生出了很多种编码规则,UTF-8 就是其中的一种。

UTF-8 是目前互联网上使用最广泛的 Unicode 的实现方式。其最大的特点就是变长的编码格式,可以用 1~4 个字节来表示一个字符,对于英文,在 UTF-8 中就只需要一个字节,而一个汉字则由三个字节组成。具体的规则其实很简单:

一个字节的,二进制第一位为0,后七位为实际的编码对应的二进制:0xxxxxxx
N个字节的,前N个字节为1,第N+1个字节为0,之后的每个字节的前两位统一为10
一个字节: 0xxxxxxx
两个字节: 110xxxxx 10xxxxxx
三个字节: 1110xxxx 10xxxxxx 10xxxxxx
四个字节: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

二、字符串

为了统一编码格式,Go 中定义所有字符串常量的默认编码格式就是 UTF-8,这样做的一个好处就是不用再处理乱码问题。在编译的时候,如果遇到非法的 UTF-8 编码,则会用替换字符 0XFFFD 来替代。

在 Unicode 中,大多数字符可以用一个码点值来表示,当然也存在需要用多个码点字来表示一个字符的情况。在 Go 中码点值用 rune 来表示,在上一篇文章中已经介绍了,rune 其实是 int32 的一个别名。

在 Go 中,字符串有两种字面量形式:双引号和反引号,反引号会保留字符串的所有格式。

a1 := "奶昔"
a2 := `奶昔
在学Go
`
fmt.Printf("%s\n%s \n", a1, a2)

dqz56

与 Java 一样,字符串的内容和长度不可更改,一个可寻址的字符串只能通过将另一个字符串赋值给它来整体修改字符串。

对于标准编译器来说,一个字符串的赋值完成后,此复制中的目标值和源值将共享底层字节。字符串 a 的一个子切片表达式 a[start:end] 也将和 a 共享一部分底层字节。

 

(一)、字符串拼接

在 Go 中字符串的零值是空字符串 “”,目前 Go 提供了五种字符串之间拼接的方式

  1. 使用 + 来进行字符串拼接
  2. 使用 fmt 标准库包中的 Sprintf、Sprint、Sprintln 函数
  3. 使用 strings 标准库包中的 Join 函数
  4. 使用 bytes 标准库包中提供的 Buffer 类型来构建一个字节切片,最后调用 String() 来获得最终的字符串
  5. 在 Go1.11 版本之后,strings 包中增加了 Builder 类型来拼接字符串
result1 := "Hello, " + "World"

strSlice = []string{"Hello", ",", "World"}

result2 := fmt.Sprint(strSlice)

result3 := strings.Join(strSlice, "")

var buffer bytes.Buffer
for _, str := range strSlice {
	buffer.WriteString(str)
}
result4 := buffer.String()

var builder strings.Builder
for _, str := range strSlice {
	builder.WriteString(str)
}
result5 := builder.String()

(二)、字符串类型转换

在 Go 中,字符串可以显示的转换为字节切片([]byte)或者码点切片([]rune),关于切片后续讲到时在重点聊聊,只需要知道切片类似于动态数组。

a := "Hello World"
b := []byte(a[:5])
c := []rune(a[:5])

当一个字符串转换为一个字节切片时,切片中的底层字节序列是字符串中存储的字节序列的一份深拷贝,换句话说,Go 运行时将会开辟一块足够大的内存来容纳被复制过来的所有字节。字节切片显示转换为字符串也是如此。

虽然字符串可以显示转换为字节切片或者码点切片,但是字节切片和码点切片之间无法直接显示转换,如果要进行转换,有三个方法:

  1. 可以利用字符串作为中间过渡,使用方便但是转换效率低
  2. 使用 unicode/utf8 标准包中的函数来实现转换,效率高,但是使用起来不太方便
  3. 使用 bytes 标准库包中的 Runes 函数将一个字节切片转换为码点切片,不过没有提供码点切片显示转换为字节切片的函数

(三)、字符串比较

字符串适用于所有比较操作(==、!=、>、 >=、 <、 <=),在进行字符串比较时,在比较字符串时,其实会对字符串底层的字节逐一进行比较。而 Go 的标准编译器会做出如下优化:

  1. 对于 == 和 != 的比较,会先判断两个字符串的长度,长度不等则一定不相等
  2. 如果两个字符串底层引用着字符串切片的指针相等,则比较结果等同于比较这两个字符串的长度。所以如果两个相等的字符串比较时,如果两个字符串引用的切片的指针相等,则比较的时间复杂度就是 O(1), 否则就是 O(n)
func main() {
	a := "abcde"
	b := "abcde"
	c := &a

	fmt.Printf("a Point: %p, b Point: %p, c Point: %p \n", &a, &b, c) 

	// a 和 b的底层指针不同,所以比较的时间复杂度是O(n), 
	// a 和 c 因为共用同一个字符串切片指针,所以时间复杂度是O(1)
	fmt.Printf("a == b: %v, a==c: %v, b==c: %v\n", a == b, a == *c, b == *c) 
}

(四)、字符串遍历

Go 中 for 循环有两种写法,传统的 for 循环和 for-range 循环,对于字符串而言,这两种循环遍历的内容是不同的。

for-range 循环遍历字符串时,实际上遍历的是码点切片([]rune),在遍历时非法的字节会被替换为 0xFFFD;使用传统的 for 循环遍历时,遍历的是字节切片([]byte)。

func main() {
	a := "abcdef奶昔有点闲!"
	for index, s := range a {
		fmt.Printf("%2v: 0x%x %v \n", index, s, string(s))
	}
	fmt.Println("==============")

	for i := 0; i < len(a); i++ {
		fmt.Printf("%2v: 0x%x \n", i, a[i])
	}
}

dqz056

(五)、语法糖:将字符串当作字节切片使用

当使用内置函数 copy 和 append 来复制和向字节切片添加元素时,函数的第二个实参可以直接使用字符串。对于 append 需要在字符串后添加省略号 …

hello := []byte("Hello ")
world := "World"

helloWorld := append(hello, world...)

helloWorld2 := make([]byte, len(hello)+len(world))
copy(helloWorld2, hello)
copy(helloWorld2, world)

三、每日一题

剑指 Offer 05. 替换空格请实现一个函数,把字符串 s 中的每个空格替换成”%20″。

思路一:使用 bytes.Buffer 函数,使用 for 循环遍历字符串,非空格字节字节添加,空格改用字符串%20 代替

import "bytes"

func replaceSpace(s string) string {
	var buffer bytes.Buffer
	for _, sc := range []byte(s) {
		if sc == ' ' {
			buffer.WriteString("%20")
		} else {
			buffer.WriteByte(sc)
		}
	}
	return buffer.String()
}

时间复杂度: O(n)

空间复杂度: O(n)

 

思路二:不借助额外的空间,原地修改字符串的字节切片,当然因为原先的空格要转换为%20,所以需要对字节切片进行一次扩容。额外增加 2 * 空格数量的长度设置双指针 i,j, j 指向新切片的末端,i 指向老切片的末端位置,从后往前移动,每遇到空格,i 不动,j 多移动两格,同时将 0、2、%依次填充到切片中。不是空格,则将下标 i 处的字节复制到下标 j 处

func ReplaceSpace2(s string) string {
	b := []byte(s)
	length := len(b)
	space_count := 0
	for _, bc := range b {
		if bc == ' ' {
			space_count++
		}
	}
	c := make([]byte, space_count*2)
	b = append(b, c...)
	i := length - 1
	j := len(b) - 1
	for i >= 0 {
		if b[i] != ' ' {
			b[j] = b[i]
		} else {
			b[j] = '0'
			j--
			b[j] = '2'
			j--
			b[j] = '%'
		}
		i--
		j--
	}
	return string(b)
}

时间复杂度:O(n)

空间复杂度:O(n)

转载请注明:XAMPP中文组官网 » Go语言学习笔记03:内置基本数据类型之字符串类型

您必须 登录 才能发表评论!