golang string类型特点总结

string简介

字符串(string)是 Go 语言提供的一种基础数据类型,在我们编程过程中,字符串可以说是我们使用的最多的一个数据结构了,凡是涉及到文本处理的地方,我们都会用到字符串。

在 go 语言中,字符串实际上是一个只读字节切片

string底层结构

// src/runtime/string.go
type stringStruct struct {
  str unsafe.Pointer
  len int
}
  • str :指向字符串的首地址
  • len :表示字符串的长度(len存储实际的字节数,而非字符数。所以对于非单字节编码的字符)

定义一个字符串

data:= "Hello"

实际结构如下:

字符串声明

 golang以字面量来声明字符串有两种方式,双引号和反引号:

str1 := "Hello World"
str2 := `Hello
Golang`

 使用双引号声明的字符串和其他语言中的字符串没有太多的区别,但是这种使用双引号的字符串只能用于单行字符串的初始化,当字符串里使用到一些特殊字符,比如双引号,换行符等等需要用进行转义。但是,反引号声明的字符串没有这些限制,字符内容即为字符串里的原始内容,所以一般用反引号来声明的比较复杂的字符串,比如json串。

基本操作

package main

import "fmt"

func main() {
	// 使用字符串字面量初始化
	var a = "a,星"
	fmt.Println(a)  //a,星

	// 可以使用下标访问,但不可修改
	fmt.Printf("a[0] is %d\n", a[0]) //a[0] is 97
	fmt.Printf("a[0:2] is %s\n", a[0:2]) //a[0:2] is a,
	//a[0] = 'a' 编译报错,Cannot assign to a[0]
	//
	// 字符串拼接
	var b = a + "狗"
	fmt.Printf("b is %s\n", b) //b is a,星狗

	// 使用内置 len() 函数获取其长度
	fmt.Printf("a's length is: %d\n", len(a)) //a's length is: 5

	// 使用 for;len 遍历
	for i := 0; i < len(a); i++ {
		fmt.Println(i, a[i])
	}
	//0 97
	//1 44
	//2 230
	//3 152
	//4 159

	// 使用 for;range 遍历
	for i, v := range a {
		fmt.Println(i, v)
	}

	//0 97
	//1 44
	//2 26143
}

为什么只读

  • 安全性:字符串的不可变性有助于防止数据竞争和意外修改。在多线程或并发环境中,如果字符串是可变的,那么一个goroutine可能在另一个goroutine不知情的情况下修改了字符串的内容,从而导致难以调试的错误。

  • 简化内存管理:由于字符串是不可变的,一旦字符串被创建,其内容就不会改变。这意味着Go运行时可以安全地共享字符串实例,而不需要担心一个实例的内容会被另一个操作修改。这种共享减少了内存分配和复制的需要,提高了程序的效率。

  • 一致性:保持与其他现代编程语言(如 Java、Python、C#)的设计一致性,使开发者在不同语言之间切换时更容易理解和使用字符串。

对string使用len()

len("hello") = 5,而len("你好") = 6

在 Go 语言中,len 函数返回字符串的字节长度,而不是字符长度。这是因为 Go 语言中的字符串是以 UTF-8 编码存储的。

  • 对于字符串 "hello",每个字符都用一个字节表示,因为它们是 ASCII 字符。 因此,len("hello") 返回 5。

  • 对于字符串 "你好",每个汉字用三个字节表示(UTF-8 编码下)。所以 "你好" 中有两个汉字,每个汉字占三个字节,总共是 6 个字节。因此,len("你好") 返回 6。

这是因为 UTF-8 是一种可变长度的字符编码,对于不同的字符,使用的字节数是不同的。具体来说:

  • ASCII 字符(U+0000 至 U+007F)用 1 个字节表示。
  • 其他的字符会使用 2 到 4 个字节表示,具体取决于字符的 Unicode 代码点。

因此,在使用 len 函数时,需要注意它返回的是字节数,而不是字符数。如果需要获取字符串中的字符数,可以将字符串转换为 rune 切片:

package main

import (
    "fmt"
)

func main() {
    fmt.Println(len("hello"))        // 输出 5
    fmt.Println(len("你好"))         // 输出 6
    fmt.Println(len([]rune("你好"))) // 输出 2
}

在上面的代码中,[]rune("你好") 将字符串转换为 rune 切片,每个 rune 表示一个 Unicode 字符,因此可以正确地计算出字符的数量。

string和[]byte之间的转化

前面说了,string是只读的,不可以被改变,但是可以将字符串转换为字节切片,然后通过下标修改字节切片,再转为字符串。

package main

import "fmt"

func main() {

    var ss string
    ss = "Hello"
    strByte := []byte(ss)
    strByte[1] = 65
    fmt.Println(string(strByte))    //hAllo
}

虽然这种方法看似可行,修改了字符串Hello,但其实最终得到的只是ss字符串的一个拷贝,源字符串并没有改变。要注意的是用这种方法去修改中文结果会是一串乱码,因为中文并不是占一个字节。

[]byte转化为string的原理

string与[]byte的转化其实会发生一次内存拷贝,并申请一块新的数组(切片的底层是数组)内存空间。

[]byte转化为string是否一定会发生内存拷贝

很多场景中会用到[]byte转化为string,但是并不是每一次转化都会发生内存拷贝,转化为的字符串被用于临时场景就不会发生内存拷贝。

  • 字符串比较:string(ss)=="hello"
  • 字符串拼接:"hello" + string(ss) + "world"
  • 用作查找:key,val := map[string(ss)]

这几种情况下,[]byte转化成的字符串并不会被后面的程序用到,只是在当下场景被临时用到,所有不会发生内存拷贝,而是直接返回一个string,这个string的指针指向切片的内存

字符串拼接

go语言中字符串是不可改变的,所以在对字符串进行拼接的时候会有内存的拷贝,存在性能损耗。常见的字符串拼接方式:

方法说明
++拼接2个字符串时,会生成一个新的字符串,开辟一段新的内存空间,新空间的大小是原来两个字符串的大小之和,所以没拼接一次买就要开辟一段空间,性能很差
SprintfSprintf 会从临时对象池中获取一个对象,然后格式化操作,最后转化为string,释放对象,实现很复杂,性能也很差
 
strings.Bulider底层存储使用[] byte,转化为字符串时可复用,每次分配内存的时候,支持预分配内存并且自动扩容,所以总体来说,开辟内存的次数就少,性能最好
 
bytes.Buffer底层存储使用[] byte,转化为字符串时不可复用,底层实现和strings.Builder差不多,性能比strings.Builder略差一点,区别是bytes.Buffer转化为字符串时重新申请了一块空间,存放生成的字符串变量,而strings.Builder直接将底层的[]byte转换成了字符串类型返回了回来,性能仅次于strings.Builder
 
append直接使用[]byte扩容机制,可复用,支持预分配内存和自动扩容,性能只比+和Sprintf好,但是如果能提前分配好内存的话,性能将会仅次于strings.Bulider
 
string.Joinstrings.join的性能约等于strings.builder,在已知字符串slice的时候可以使用,未知时不建议使用,构造切片也是会有性能损耗的
 

空string不为nil

最后一点注意var s string定义了一个字符串的空值,字符串的空值是空字符串,即""。字符串不可能为nil

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值