go 语言学习 了解go语言字符串类型

go语言支持原生字符串类型和c语言比较优点

go支持原生字符串类型

go支持原生字符串类型, go string 类型的数据是不可变的,提高了字符串的并发安全性和存储利用率,我们不能修改字符串的某一部分值,可以进行二次赋值操作

var s string = "hello"
s[0] = 'k'   // 错误:字符串的内容是不可改变的
s = "gopher" // ok

类型的数据是不可变的这种特性,也去除了字符串的并发安全问题,
针对同一个字符串值,无论它在程序的几个位置被使用,Go 编译器只需要为它分配一块存储就好了,大大提高了存储利用率。

没有结尾’\0’,而且获取字符串长度的时间复杂度是常数级别

在 C 语言中,获取一个字符串的长度可以调用标准库的 strlen 函数,这个函数的实现原理是遍历字符串中的每个字符并做计数,直到遇到字符串的结尾’\0’停止。显然这是一个线性时间复杂度的算法,执行时间与字符串中字符个数成正比,计算字符串长度是一个(o)n级别复杂度了

go语言去c掉了语言特性中结尾\0 的机制,计算字符串长度是一个简单的常数级别

对非 ASCII 字符提供原生支持,消除了源码在不同环境下显示乱码的可能

C 语言没有内置对非 ASCII 字符(如中文字符)的支持。

Go 语言源文件默认采用的是 Unicode 字符集,Unicode 字符集是目前市面上最流行的字符集,它囊括了几乎所有主流非 ASCII 字符(包括中文字符)。Go 字符串中的每个字符都是一个 Unicode 字符,并且这些 Unicode 字符是以 UTF-8 编码格式存储在内存当中的

Go 字符串的组成

字节层面上看

Go 语言中的字符串值也是一个可空的字节序列,字节序列中的字节个数称为该字符串的长度。一个个的字节只是孤立数据,不表意。

	var a = "中国人"
	fmt.Printf("the length of s = %d\n", len(a)) //字节长度 the length of s = 9
	for i := 0; i < len(a); i++ {
		fmt.Printf("0x%x ", a[i]) //这个字符串有9个字节 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
	}

字符层面上看

字符串是由一个可空的字符序列构成

	var a = "中国人"
	fmt.Printf("the length of s = %d\n", utf8.RuneCountInString(a)) //字符长度 the length of s = 3

	for _, c := range a {
		fmt.Printf("0x%x ", c) //返回字符集表中的码点,每一个字符对应的 unicode 码点   0x4e2d 0x56fd 0x4eba
	}
Unicode 码点

Unicode 码点,就是指将 Unicode 字符集中的所有字符“排成一队”,字符在这个“队伍”中的位次,就是它在 Unicode 字符集中的码点。也就说,一个码点唯一对应一个字符,“码点”的概念和我们马上要讲的 rune 类型有很大关系。

rune 类型与字符字面值

Go 使用 rune 这个类型来表示一个 Unicode 码点。rune 本质上是 int32 类型的别名类型,它与 int32 类型是完全等价的

由于一个 Unicode 码点唯一对应一个 Unicode 字符。所以我们可以说,一个 rune 实例就是一个 Unicode 字符,一个 Go 字符串也可以被视为 rune 实例的集合。我们可以通过字符字面值来初始化一个 rune 变量。

rune 类型字面值 单引号
s := 'a' 
string 类型字面值 双引号
s := "a" 

UTF-8 编码方案

UTF-32编码方案,固定长度字节 将所有 Unicode字符的码点都按照固定 4 字节编码。

UTF-8编码方案:变长度字节,根据 Unicode字符的码点序号不同,所编码的字节数不同。1~4 个字节

UTF-8 使用更优于 UTF-32

UTF-8 编码使用的字节数量从 1 个到 4 个不等,UTF-32 只能使用4个字节,utf-8 能 更好的利用内存的使用,以及 utf-8 支持所有 ASCII 码以及语言字节,UTF-32 和1字节的ASCII 有兼容问题,UTF-8 编码方案 成为了主流的编码方案

深入了解Go 字符串类型的内部表示

Go 语言在运行时层面通过一个二元组结构(Data, Len)来表示一个 string 类型变量,其中 Data 是一个指向存储字符串数据内容区域的指针值,Len 是字符串的长度。因此,本质上,一个 string 变量仅仅是一个“描述符”,并不真正包含字符串数据。因此,我们即便直接将 string 类型变量作为函数参数,其传递的开销也是恒定的,不会随着字符串大小的变化而变化。

Go 字符串类型的常见操作

下标操作

在字符串的实现中,真正存储数据的是底层的数组。字符串的下标操作本质上等价于底层数组的下标操作


var s = "中国人"
fmt.Printf("0x%x\n", s[0]) // 0xe4:字符“中” utf-8编码的第一个字节

字符迭代

Go 有两种迭代形式:常规 for 迭代与 for range 迭代。你要注意,通过这两种形式的迭代对字符串进行操作得到的结果是不同的。

常规 for 迭代 ,每一个迭代的单元代表的是字节
	var a = "中国人"
	fmt.Printf("the length of s = %d\n", len(a)) //字节长度 the length of s = 9
	for i := 0; i < len(a); i++ {
		fmt.Printf("0x%x ", a[i]) //这个字符串有9个字节 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
	}
for range 迭代 每一个迭代的单元代表的是字节
	var a = "中国人"
	fmt.Printf("the length of s = %d\n", utf8.RuneCountInString(a)) //字符长度 the length of s = 3

	for _, c := range a {
		fmt.Printf("0x%x ", c) //返回字符集表中的码点,每一个字符对应的 unicode 码点   0x4e2d 0x56fd 0x4eba
	}

字符串连接

+/+= 方式
	var s = "hell"
	s = s + "word"

	var a = "hell"
	a += "word"
	println(s, a) //hellword hellword

+/+= 方法实现字符串拼接,Go 还提供了 strings.Builder、strings.Join、fmt.Sprintf 等函数来进行字符串连接操作

strings.Builder 方式
var b strings.Builder
	for i := 3; i >= 1; i-- {
		fmt.Fprintf(&b, "%d...", i)
	}
	b.WriteString("ignition")
	fmt.Println(b.String())//3...2...1...ignition
strings.Join 方式
	s := []string{"foo", "bar", "baz"}
	fmt.Println(strings.Join(s, ", "))
	println(s)//foo, bar, baz
fmt.Sprintf 方式
	var a = "中国人"
	fmt.Printf("the length of s = %d\n", utf8.RuneCountInString(a)) //字符长度 the length of s = 3

字符串比较

Go 字符串类型支持各种比较关系操作符,包括 = =、!= 、>=、<=、> 和 <。在字符串的比较上,Go 采用字典序的比较策略,分别从每个字符串的起始处,开始逐个字节地对两个字符串类型变量进行比较。


func main() {
        // ==
        s1 := "世界和平"
        s2 := "世界" + "和平"
        fmt.Println(s1 == s2) // true

        // !=
        s1 = "Go"
        s2 = "C"
        fmt.Println(s1 != s2) // true

        // < and <=
        s1 = "12345"
        s2 = "23456"
        fmt.Println(s1 < s2)  // true
        fmt.Println(s1 <= s2) // true

        // > and >=
        s1 = "12345"
        s2 = "123"
        fmt.Println(s1 > s2)  // true
        fmt.Println(s1 >= s2) // true
}

字符串和其他类型的转换

Go 支持字符串与字节切片、字符串与 rune 切片的双向转换,并且这种转换无需调用任何函数,只需使用显式类型转换就可以了

	var s string = "中国人"
	//string => []rune
	rs := []rune(s)
	fmt.Printf("%x\n", rs) //[4e2d 56fd 4eba]

	// string -> []byte
	bs := []byte(s)
	fmt.Printf("%x\n", bs) //e4b8ade59bbde4baba

	// []rune -> string
	s1 := string(rs)
	fmt.Printf(s1) // 中国人

	// []byte -> string
	s2 := string(bs)
	fmt.Println(s2) // 中国人

这样的转型看似简单,但无论是 string 转切片,还是切片转 string,这类转型背后也是有着一定开销的。这些开销的根源就在于 string 是不可变的,运行时要为转换后的类型分配新内存。

好记性不如烂笔头 本文学自 极客时间 Tony Bai · Go 语言第一课

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值