go基础10 -字符串的高效构造与转换

这篇具有很好参考价值的文章主要介绍了go基础10 -字符串的高效构造与转换。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前面提到过,Go原生支持通过+/+=操作符来连接多个字符串以构造一个更长的字符串,并且通过+/+=操作符的字符串连接构造是最自然、开发体验最好的一种。

但Go还提供了其他一些构造字符串的方法,比如:
● 使用fmt.Sprintf;
● 使用strings.Join;
● 使用strings.Builder;
● 使用bytes.Buffer。
在这些方法中哪种方法最为高效呢?我们使用基准测试的数据作为参考:

var sl []string = []string{
"Rob Pike ",
"Robert Griesemer ",
"Ken Thompson ",
}
func concatStringByOperator(sl []string) string {
var s string
for _, v := range sl {
s += v
}
return s
}
func concatStringBySprintf(sl []string) string {
var s string
for _, v := range sl {
s = fmt.Sprintf("%s%s", s, v)
}
return s
}
func concatStringByJoin(sl []string) string {
return strings.Join(sl, "")
}
func concatStringByStringsBuilder(sl []string) string {
var b strings.Builder
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func concatStringByStringsBuilderWithInitSize(sl []string) string {
var b strings.Builder
b.Grow(64)
 for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func concatStringByBytesBuffer(sl []string) string {
var b bytes.Buffer
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func concatStringByBytesBufferWithInitSize(sl []string) string {
buf := make([]byte, 0, 64)
b := bytes.NewBuffer(buf)
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func BenchmarkConcatStringByOperator(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByOperator(sl)
}
}
func BenchmarkConcatStringBySprintf(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringBySprintf(sl)
}
}
func BenchmarkConcatStringByJoin(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByJoin(sl)
}
}
func BenchmarkConcatStringByStringsBuilder(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByStringsBuilder(sl)
}
}
func BenchmarkConcatStringByStringsBuilderWithInitSize(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByStringsBuilderWithInitSize(sl)
}
}
func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByBytesBuffer(sl)
}
}
func BenchmarkConcatStringByBytesBufferWithInitSize(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByBytesBufferWithInitSize(sl)
}
}

运行该基准测试:

$go test -bench=. -benchmem ./string_concat_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkConcatStringByOperator-8 11744653 89.1 ns/op 80 B/op 2 allocs/op
BenchmarkConcatStringBySprintf-8 2792876 420 ns/op 176 B/op 8 allocs/op
BenchmarkConcatStringByJoin-8 22923051 49.1 ns/op 48 B/op 1 allocs/op
BenchmarkConcatStringByStringsBuilder-8 11347185 96.6 ns/op 112 B/op 3 allocs/op
BenchmarkConcatStringByStringsBuilderWithInitSize-8 26315769 42.3 ns/op 64 B/op 1 allocs/op
BenchmarkConcatStringByBytesBuffer-8 14265033 82.6 ns/op 112 B/op 2 allocs/op
BenchmarkConcatStringByBytesBufferWithInitSize-8 24777525 48.1 ns/op 48 B/op 1 allocs/op
PASS
ok command-line-arguments 8.816s

从基准测试的输出结果的第三列,即每操作耗时的数值来看:

● 做了预初始化的strings.Builder连接构建字符串效率最高;

● 带有预初始化的bytes.Buffer和strings.Join这两种方法效率十分接近,分列二三位;

● 未做预初始化的strings.Builder、bytes.Buffer和操作符连接在第三档次;

● fmt.Sprintf性能最差,排在末尾。

由此可以得出一些结论:

● 在能预估出最终字符串长度的情况下,使用预初始化的strings.Builder连接构建字符串效率最高

● strings.Join连接构建字符串的平均性能最稳定,如果输入的多个字符串是以[]string承载的,那么strings.Join也是不错的选择;

● 使用操作符连接的方式最直观、最自然,在编译器知晓欲连接的字符串个数的情况下,使用此种方式可以得到编译器的优化处理;

● fmt.Sprintf虽然效率不高,但也不是一无是处,如果是由多种不同类型变量来构建特定格式的字符串,那么这种方式还是最适合的。

转换

在前面的例子中,我们看到了string到[]rune以及string到[]byte的转换,这两个转
换也是可逆的,也就是说string和[]rune、[]byte可以双向转换。下面就是从[]rune或
[]byte反向转换为string的例子:

func main() {
	rs := []rune{
		0x4E2D,
		0x56FD,
		0x6B22,
		0x8FCE,
		0x60A8,
	}
	s := string(rs)
	fmt.Println(s)
	sl := []byte{
	0xE4, 0xB8, 0xAD,
	0xE5, 0x9B, 0xBD,
	0xE6, 0xAC, 0xA2,
	0xE8, 0xBF, 0x8E,
	0xE6, 0x82, 0xA8,
	}
	s = string(sl)
	fmt.Println(s)
}
$go run string_slice_to_string.go
中国欢迎您
中国欢迎您

无论是string转slice还是slice转string,转换都是要付出代价的,这些代价的根源
在于string是不可变的,运行时要为转换后的类型分配新内存。我们以byte slice与
string相互转换为例,看看转换过程的内存分配情况:

func byteSliceToString() {
sl := []byte{
0xE4, 0xB8, 0xAD,
0xE5, 0x9B, 0xBD,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
0xEF, 0xBC, 0x8C,
0xE5, 0x8C, 0x97,
0xE4, 0xBA, 0xAC,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
 0xE6, 0x82, 0xA8,
}
_ = string(sl)
}
func stringToByteSlice() {
s := "中国欢迎您,北京欢迎您"
_ = []byte(s)
}
func main() {
fmt.Println(testing.AllocsPerRun(1, byteSliceToString))
fmt.Println(testing.AllocsPerRun(1, stringToByteSlice))
}

运行这个例子:

$go run string_mallocs_in_convert.go
1
1

我们看到,针对“中国欢迎您,北京欢迎您”这个长度的字符串,在string与byte
slice互转的过程中都要有一次内存分配操作。

在Go运行时层面,字符串与rune slice、byte slice相互转换对应的函数如下:

slicebytetostring: []byte -> string
slicerunetostring: []rune -> string
stringtoslicebyte: string -> []byte
stringtoslicerune: string -> []rune

以byte slice为例,看看slicebytetostring和stringtoslicebyte的实现:

const tmpStringBufSize = 32
type tmpBuf [tmpStringBufSize]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}


func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
	l := len(b)
	 if l == 0 {
		return ""
	}
	// 此处省略一些代码
	if l == 1 {
	stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
	stringStructOf(&str).len = 1
	return
	}
	var p unsafe.Pointer
	if buf != nil && len(b) <= len(buf) {
	p = unsafe.Pointer(buf)
	} else {
	p = mallocgc(uintptr(len(b)), nil, false)
	}
	stringStructOf(&str).str = p
	stringStructOf(&str).len = len(b)
	memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
	return
}

想要更高效地进行转换,唯一的方法就是减少甚至避免额外的内存分配操作。我们看
到运行时实现转换的函数中已经加入了一些避免每种情况都要分配新内存操作的优化(如
tmpBuf的复用)。
slice类型是不可比较的,而string类型是可比较的,因此在日常Go编码中,我们会经
常遇到将slice临时转换为string的情况。Go编译器为这样的场景提供了优化。在运行时中
有一个名为slicebytetostringtmp的函数就是协助实现这一优化的:

func slicebytetostringtmp(b []byte) string {
	if raceenabled && len(b) > 0 {
		racereadrangepc(unsafe.Pointer(&b[0]),
		uintptr(len(b)),
		getcallerpc(),
	funcPC(slicebytetostringtmp))
	}
	if msanenabled && len(b) > 0 {
		msanread(unsafe.Pointer(&b[0]), uintptr(len(b)))
	}
	return *(*string)(unsafe.Pointer(&b))
}

该函数的“秘诀”就在于不为string新开辟一块内存,而是直接使用slice的底层存
储。当然使用这个函数的前提是:在原slice被修改后,这个string不能再被使用了。因此
这样的优化是针对以下几个特定场景的。

1)string(b)用在map类型的key中

b := []byte{'k', 'e', 'y'}
m := make(map[string]string)
m[string(b)] = "value"
m[[3]string{string(b), "key1", "key2"}] = "value1"2string(b)用在字符串连接语句中
b := []byte{'t', 'o', 'n', 'y'}
s := "hello " + string(b) + "!"3string(b)用在字符串比较中
s := "tom"
b := []byte{'t', 'o', 'n', 'y'}
if s < string(b) {
...
}

Go编译器对用在for-range循环中的string到[]byte的转换也有优化处理,它不会为
[]byte进行额外的内存分配,而是直接使用string的底层数据。

看下面的例子:

func convert() {
s := "中国欢迎您,北京欢迎您"
sl := []byte(s)
for _, v := range sl {
_ = v
}
}
func convertWithOptimize() {
s := "中国欢迎您,北京欢迎您"
for _, v := range []byte(s) {
_ = v
}
}
func main() {
fmt.Println(testing.AllocsPerRun(1, convert))
fmt.Println(testing.AllocsPerRun(1, convertWithOptimize))
}

运行这个例子程序:

$go run string_for_range_covert_optimize.go
1
0

从结果我们看到,convertWithOptimize函数将string到[]byte的转换放在for-range
循环中,Go编译器对其进行了优化,节省了一次内存分配操作。
文章来源地址https://www.toymoban.com/news/detail-703952.html

到了这里,关于go基础10 -字符串的高效构造与转换的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • go基础09-Go语言的字符串类型

    字符串类型是现代编程语言中最常使用的数据类型之一。在Go语言的先祖之一C语言当中,字符串类型并没有被显式定义,而是以字符串字面值常量或以’\\0’结尾的字符类型(char)数组来呈现的: 这给C程序员在使用字符串时带来一些问题,诸如: ● 类型安全性差; ● 字符

    2024年02月09日
    浏览(47)
  • 【Go基础】编译、变量、常量、基本数据类型、字符串

    面试题文档下链接点击这里免积分下载 go语言入门到精通点击这里免积分下载 使用 go build 1.在项目目录下执行 2.在其他路径下编译 go build ,需要再后面加上项目的路径(项目路径从GOPATH/src后开始写起,编译之后的可执行文件就保存再当前目录) 3.打包的时候自定义名字:

    2024年02月09日
    浏览(46)
  • 【Java 基础篇】Java StringBuffer详解:更高效的字符串处理

    在Java编程中,字符串是一个常见的数据类型,用于存储文本信息。然而,与字符串相关的操作可能会导致性能问题,因为字符串是不可变的,每次对字符串进行操作都会创建一个新的字符串对象。为了解决这个问题,Java提供了 StringBuffer 类,它允许我们有效地处理可变字符串

    2024年02月10日
    浏览(28)
  • MFC 编辑框输入16进制字符串转换为16进制数或者10进制数据计算

    1. 编辑框添加变量,并选择变量类型为CString。     CString m_strReg;        DDX_Text(pDX, IDC_EDIT_REG, m_strReg); 2. 使用“strtoul”或“_tcstoul”函数将Cstring 类型转换为16进制/10进制数进行计算。     CString tmp;     UpdateData(TRUE);     UpdateData(FALSE);          OutputDebugString(m_strReg);  

    2024年02月16日
    浏览(42)
  • 【C语言基础考研向】10 字符数组初始化及传递和scanf 读取字符串

    字符数组的定义方法与前面介绍的一维数组类似.例如, 字符数组的初始化可以采用以下方式. (1)对每个字符单独赋值进行初始化.例如, (2)对整个数组进行初始化.例如, 但工作中一般不用以上两种初始化方式,因为字符数组一般用来存取字符串.通常采用的初始化方式是

    2024年01月25日
    浏览(45)
  • 力扣2182.构造限制重复的字符串

     思路:先记录每个字符的出现次数,构建一个新字符串,从尾取字符,每取一个该字符个数-1,若该字符已经取到有repeatLimit个,则递归取次大的字符,并对应字符个数-1,若没有次大字符了,则直接返回 代码:  

    2024年02月01日
    浏览(36)
  • 【每日一题】构造限制重复的字符串

    【贪心】【字符串】【2024-01-13】 2182. 构造限制重复的字符串 思路 解题思想比较简单,利用贪心思想,每次选择当前剩余字符串中字典序最大的字符加到答案字符串末尾,如果答案字符串末尾的字符已经连续出现了 repeatLimit 次,则将字典序次大的字符加到答案字符串,随后

    2024年01月22日
    浏览(31)
  • 2645. 构造有效字符串的最少插入数

    给你一个字符串  word  ,你可以向其中任何位置插入 \\\"a\\\"、\\\"b\\\" 或 \\\"c\\\" 任意次,返回使  word   有效  需要插入的最少字母数。 如果字符串可以由 \\\"abc\\\" 串联多次得到,则认为该字符串  有效  。 示例 1: 示例 2: 示例 3: 提示: 1 = word.length = 50 word  仅由字母 \\\"a\\\"、\\\"b\\\" 和 \\\"c\\\" 组成

    2024年01月20日
    浏览(31)
  • 【Kotlin】基础速览(1):操作符 | 内建类型 | 类型转换 | 字符串模板 | 可变 var 和不可变 val

      📜 本章目录: 0x00 操作符(operators) 0x01 内建类型(Build-in) 0x02 类型转换:显式类型转换 0x03 在较长数字中使用下划线 0x04 字符串(String) 0x05 字符串模板(String Templates) 0x06 字符串连接(变量插值) 0x06 指定变量类型 0x07 可变 var 和不可变 val 0x00 操作符(operators)

    2024年02月11日
    浏览(34)
  • 【算法题】2745. 构造最长的新字符串

    给你三个整数 x ,y 和 z 。 这三个整数表示你有 x 个 “AA” 字符串,y 个 “BB” 字符串,和 z 个 “AB” 字符串。你需要选择这些字符串中的部分字符串(可以全部选择也可以一个都不选择),将它们按顺序连接得到一个新的字符串。新字符串不能包含子字符串 “AAA” 或者

    2024年02月12日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包