[已解决] Golang中如何获取20个字符的字符串长度

[已解决] Golang中如何获取20个字符的字符串长度 我想创建一个结构体,其中包含两个字段,字符串长度为20个字符。

例如

type Name struct { firstName, lastName string }

我一直在寻找任何可以将 firstName 和 lastName 的字符串值修剪为20个字符长度的方法,任何想法都将非常有帮助!

17 回复

太棒了,伙计们,感谢你们的回复,特别是关于基准测试的部分!😊

更多关于[已解决] Golang中如何获取20个字符的字符串长度的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我也学到了新东西,之前不知道有多字节字符,我想这解释了rune的用途。

还有一点需要注意。你不能使用 len 函数,因为它统计的是字节数。你必须使用:

utf8.RuneCountInString(firstName)

我从这里获得了新的灵感,并完成了 exstrings.Reverse 的性能改进,这让我感觉非常好。

它确实能工作,但对于码值大于127的任何字符则无效。它还可能破坏多字节字符,导致字符串末尾出现无效字符。

请查看此链接:https://play.golang.org/p/uVu1egklEuL

func main() {
    fmt.Println("hello world")
}

太棒了!谢谢你,Holloway。确实,使用 getter 和 setter 接口函数是一个非常棒的方法。 这正是我需要的。 表情符号 我将在24小时后标记为已解决,也许其他人会有其他的解决方案。

是否引用原始字符串取决于您的目的,使用不当可能导致内存泄漏。

@thinkeridea,您是否有支持这一观点的案例研究/细节或示例?我对Go中的字节操作感到有些紧张。看起来我们将来可能无法使用字节,而只能使用符文。

字节操作指的是操作二进制/十六进制数据流。据我理解,符文是多字节字符(因此按字节切片切割不等于按字符切割)。

thinkeridea:

我尝试比较了以下两种方法的性能:

哇,这个发现很棒。 👍 以下是来自 Linux amd64 的基准测试结果:

goos: linux
goarch: amd64
pkg: gosandbox
BenchmarkSubStrA-8        857563              1297 ns/op
BenchmarkSubStrB-8      10494722               114 ns/op
BenchmarkSubStrC-8       8502182               142 ns/op
PASS
ok      gosandbox       3.786s

thinkeridea:

我们仍需关注此问题并在必要时处理,但不宜过于频繁,极端情况很少发生。

thinkeridea:

程序中是使用 string 还是 []byte 需要根据数据来源和处理数据的函数来决定,一定要减少类型转换

Translation: whether the program is using string or []byte, it must be decided based on on the source codes and the data processing, with the aim to reduce as many data type conversion as possible.

我明白了。内存泄漏就是这样出现的。

既然如此,只要我不涉及实际的 rune(例如处理 utf8 字符),我仍然可以安全地使用 []byte 操作。否则,我需要将其升级为 []rune

我使用 []byte 处理的一些情况是 I/O 二进制流数据处理(例如 SPI/I2C)。

谢谢!

一种简单的方法是将这些字段作为私有变量,并提供 gettersetter 接口函数。示例:

const (
    maxLength = 20
)

type Name struct {
    first string
    last string
}

func (n *Name) Set(firstName string, lastName string) {
    n.first = firstName
    n.last = lastName

    if len(firstName) > maxLength {
        n.first = firstName[:maxLength]
    }

    if len(lastName) > maxLength {
        n.last = lastName[:maxLength]
    }
}

func (n *Name) GetWithFirst(withFirstName bool) string {
    if withFirstName {
        return n.last + ", " + n.first
    }
    return n.last
}

然后,你可以这样调用该结构体:

func main() {
	l := &Name{}
	l.Set("Dumitru Margareta Corneliu Leopold Blanca Karol Aeon Ignatius Raphael Maria Niketas A. Shilage", "Mihaly")
	s := l.GetWithFirst(true)
	fmt.Printf("My name is: %s\n", s)
}
// 输出:
// My name is: Mihaly, Dumitru Margareta Co

是的。这对我来说也是一次大开眼界的经历!以下是应用齐寅解决方案的最新代码:

// 贡献者:Chew, Kean Ho (Holloway), Johan Dahl, Qi Yin
//
// 主程序用于将名字修剪至20个字符
package main

import (
	"fmt"
	"unicode/utf8"
)

const (
	maxLength = 20
)

type Name struct {
	first string
	last  string
}

func (n *Name) trim(s string, length int) string {
	var size, x int

	for i := 0; i < length && x < len(s); i++ {
		_, size = utf8.DecodeRuneInString(s[x:])
		x += size
	}

	return s[:x]
}

func (n *Name) Set(firstName string, lastName string) {
	n.first = n.trim(firstName, maxLength)
	n.last = n.trim(lastName, maxLength)
}

func (n *Name) GetWithFirst(withFirstName bool) string {
	if withFirstName {
		return n.last + ", " + n.first
	}

	return n.last
}

func main() {
	l := &Name{}
	l.Set("Dumitru Margareta Ἄγγελος Leopold Blanca Karol Aeon Ignatius Raphael Maria Niketas A. Shilage", "Mihaly")
	s := l.GetWithFirst(true)
	fmt.Printf("My name is: %s\n", s)
}

// 输出:
// My name is: Mihaly, Dumitru Margareta Ἄγ

它也可能破坏多字节字符,导致字符串末尾出现无效字符。

感谢指出!我忽略了符文层面的处理,这是我的疏忽。学到了新东西。😅

以下是新改进的代码:Go Playground - The Go Programming Language

// Cotributed by: Chew, Kean Ho (Holloway), Johan Dahl
//
// main program is about trimming names to 20 characters
package main

import (
	"fmt"
)

const (
    maxLength = 20
)

type Name struct {
    first string
    last string
}

func (n *Name) Set(firstName string, lastName string) {
	var rs []rune

	n.first = firstName
	if len(firstName) > maxLength {
		rs = []rune(firstName)
		n.first = string(rs[:maxLength])
	}

	n.last = lastName
	if len(lastName) > maxLength {
		rs = []rune(lastName)
		n.last = string(rs[:maxLength])
	}
}

func (n *Name) GetWithFirst(withFirstName bool) string {
    if withFirstName {
        return n.last + ", " + n.first
    }
    return n.last
}

func main() {
	l := &Name{}
	l.Set("Dumitru Margareta Ἄγγελος Leopold Blanca Karol Aeon Ignatius Raphael Maria Niketas A. Shilage", "Mihaly")
	s := l.GetWithFirst(true)
	fmt.Printf("My name is: %s\n", s)
}
// Output:
// My name is: Mihaly, Dumitru Margareta Ἄγ

这是一个以切片方式截取字符串的简单示例。实际上,并没有发生内存拷贝,只是创建了一个引用。标准库中的函数也经常以这种方式截断字符串。

这种方法简单高效,因为不需要分配新的内存,通常非常有效。

然而,如果原始字符串的生命周期非常短但字符串本身非常大,而获取的子字符串生命周期足够长但字符串本身非常短,那么GC将无法在子字符串的生命周期内释放原始字符串中不再使用的内存,这会导致内存泄漏。

大多数实际程序中存在这样的问题,但通常并不那么难以忍受,只是轻微且短期的问题,在不追求极致性能的情况下可以忽略。

我们仍然应该注意这个问题,并在必要时处理它,但不必过于频繁,因为极端情况很少发生。

这里有一篇关于字符串的中文介绍,可能会有所帮助:string 优化误区及建议

package main

import (
	"fmt"
	"reflect"
	"strings"
	"unsafe"
)

func main() {
	s := strings.Repeat("A", 2000)

	s1 := s[5:10]

	fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s)).Data)  // 824634433536
	fmt.Println((*reflect.StringHeader)(unsafe.Pointer(&s1)).Data) // 824634433541

	// 将 s 字符串类型更改为 []byte 不会产生重复,只是以字节的方式解析存储在 s 变量内存中的字符串数据
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bs := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
		Data: sh.Data,
		Len:  sh.Len,
		Cap:  sh.Len,
	}))

	// 修改一段数据,这段数据被 s1 引用。
	copy(bs[5:10], strings.Repeat("B", 5))

	// 发现 s1 的数据已更改。
	fmt.Println(s1) // BBBBB
}

你不能使用 len,它计算的是字节数。你必须使用…

明白了。len(..)rune 计数的区别非常大。运行示例:Go Playground - The Go Programming Language

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "Ἄγγελος"
	
	x := len(s)
	y := utf8.RuneCountInString(s)

	fmt.Printf("length: x=%v y=%v \n" , x, y)
}
// Output:
// length: x=15 y=7

我也学到了新东西,之前不知道有多字节字符,这解释了 rune 的用途。

我也是。很高兴我也学到了新东西! 😄

更新后的代码如下:

// Cotributed by: Chew, Kean Ho (Holloway), Johan Dahl
//
// main program is about trimming names to 20 characters
package main

import (
	"fmt"
	"unicode/utf8"
)

const (
	maxLength = 20
)

type Name struct {
	first string
	last string
}

func (n *Name) Set(firstName string, lastName string) {
	var rs []rune

	n.first = firstName
	if utf8.RuneCountInString(firstName) > maxLength {
		rs = []rune(firstName)
		n.first = string(rs[:maxLength])
	}

	n.last = lastName
	if utf8.RuneCountInString(lastName) > maxLength {
		rs = []rune(lastName)
		n.last = string(rs[:maxLength])
	}
}

func (n *Name) GetWithFirst(withFirstName bool) string {
    if withFirstName {
        return n.last + ", " + n.first
    }
    return n.last
}

func main() {
	l := &Name{}
	l.Set("Dumitru Margareta Ἄγγελος Leopold Blanca Karol Aeon Ignatius Raphael Maria Niketas A. Shilage", "Mihaly")
	s := l.GetWithFirst(true)
	fmt.Printf("My name is: %s\n", s)
}

// Output:
// My name is: Mihaly, Dumitru Margareta Ἄγ

运行示例:Go Playground - The Go Programming Language

我认为我们应该提供一种更好的字符串截断方法。 我尝试比较了以下两种方法的性能:

import (
	"testing"
	"unicode/utf8"
	"unsafe"
)

func SubStrA(s string, length int) string {
	if utf8.RuneCountInString(s) > length {
		rs := []rune(s)
		return string(rs[:length])
	}

	return s
}

func SubStrB(s string, length int) string {
	var size, n int
	for i := 0; i < length && n < len(s); i++ {
		_, size = utf8.DecodeRuneInString(s[n:])
		n += size
	}

	return s[:n]
}

func SubStrC(s string, length int) string {
	var size, n int
	for i := 0; i < length && n < len(s); i++ {
		_, size = utf8.DecodeRuneInString(s[n:])
		n += size
	}

	b := make([]byte, n)
	copy(b, s[:n])
	return *(*string)(unsafe.Pointer(&b))
}

var s = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"

func BenchmarkSubStrA(b *testing.B) {
	for i := 0; i < b.N; i++ {
		SubStrA(s, 20)
	}
}

func BenchmarkSubStrB(b *testing.B) {
	for i := 0; i < b.N; i++ {
		SubStrB(s, 20)
	}
}

func BenchmarkSubStrC(b *testing.B) {
	for i := 0; i < b.N; i++ {
		SubStrC(s, 20)
	}
}
goos: darwin
goarch: amd64
BenchmarkSubStrA-8        745708              1624 ns/op             336 B/op          2 allocs/op
BenchmarkSubStrB-8       9568920               122 ns/op               0 B/op          0 allocs/op
BenchmarkSubStrC-8       7274718               157 ns/op              48 B/op          1 allocs/op
PASS
ok      command-line-arguments  4.782s

它们的性能差距巨大。是否引用原始字符串取决于你的目的,使用不当可能导致内存泄漏。

SubStrC 尽管分配了子字符串,仍然会有近十倍的性能差距。

在Go中,你可以使用字符串切片或utf8.RuneCountInString结合切片来确保字符串不超过20个字符。以下是两种实现方式:

1. 使用字节切片(适用于ASCII字符)

如果字符串只包含ASCII字符,可以直接使用切片操作:

type Name struct {
    firstName, lastName string
}

func (n *Name) SetFirstName(s string) {
    if len(s) > 20 {
        n.firstName = s[:20]
    } else {
        n.firstName = s
    }
}

func (n *Name) SetLastName(s string) {
    if len(s) > 20 {
        n.lastName = s[:20]
    } else {
        n.lastName = s
    }
}

2. 使用rune切片(支持Unicode字符)

对于包含多字节字符的字符串,应该使用rune切片:

import "unicode/utf8"

type Name struct {
    firstName, lastName string
}

func truncateTo20Chars(s string) string {
    if utf8.RuneCountInString(s) > 20 {
        runes := []rune(s)
        return string(runes[:20])
    }
    return s
}

func (n *Name) SetFirstName(s string) {
    n.firstName = truncateTo20Chars(s)
}

func (n *Name) SetLastName(s string) {
    n.lastName = truncateTo20Chars(s)
}

3. 在结构体初始化时处理

你也可以在创建结构体时直接处理字符串:

func NewName(firstName, lastName string) Name {
    return Name{
        firstName: truncateTo20Chars(firstName),
        lastName:  truncateTo20Chars(lastName),
    }
}

4. 使用自定义类型和验证

创建一个自定义字符串类型并添加验证方法:

type LimitedString string

func NewLimitedString(s string) LimitedString {
    runes := []rune(s)
    if len(runes) > 20 {
        return LimitedString(string(runes[:20]))
    }
    return LimitedString(s)
}

type Name struct {
    firstName, lastName LimitedString
}

// 使用示例
name := Name{
    firstName: NewLimitedString("这是一个很长的名字,需要被截断"),
    lastName:  NewLimitedString("短名"),
}

选择哪种方法取决于你的具体需求。如果只需要处理ASCII字符,第一种方法最简单高效;如果需要处理Unicode字符,第二种方法更安全。

我设计了一个方法 exutf8.RuneIndexInStringexutf8.RuneIndex 来根据字符数获取字节索引,这加速了截取字符串时索引位置的计算,并封装了更易用的字符串截取方法 exutf8.RuneSubStringexutf8.RuneSub,在 exstrings.SubStringexbytes.Sub 中可以找到更易用的别名。

我在 exunicode/exutf8/benchmark/sub_string_test.go 中测试并比较了各种方法的性能。测试结果如下:

$ go test -benchmem -bench="."                                                        
goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRunes-8                    875361              1511 ns/op             336 B/op          2 allocs/op
BenchmarkSubStrRange-8                  11738449                96.7 ns/op             0 B/op          0 allocs/op
BenchmarkSubStrDecodeRuneInString-8     11425912               111 ns/op               0 B/op          0 allocs/op
BenchmarkSubStrRuneIndexInString-8      14508450                82.0 ns/op             0 B/op          0 allocs/op
BenchmarkSubStrRuneSubString-8          14334190                82.3 ns/op             0 B/op          0 allocs/op
PASS
ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     7.447s

虽然 RuneSubString 方法略逊于 RuneIndexInString,但它提供了一个更灵活且易于使用的接口。

这是目前我能达到的最佳结果,希望它能够得到改进。

回到顶部