ChatGPT解决这个技术问题 Extra ChatGPT

在 Go 中测试空字符串的最佳方法是什么?

哪种方法最适合(最典型的)测试非空字符串(在 Go 中)?

if len(mystring) > 0 { }

或者:

if mystring != "" { }

或者是其他东西?


A
ANisus

这两种风格都在 Go 的标准库中使用。

if len(s) > 0 { ... }

可以在 strconv 包中找到:http://golang.org/src/pkg/strconv/atoi.go

if s != "" { ... }

可以在 encoding/json 包中找到:http://golang.org/src/pkg/encoding/json/encode.go

两者都是惯用的并且足够清楚。这更多的是个人品味和清晰度的问题。

Russ Cox 在 golang-nuts thread 中写道:

使代码清晰的那个。如果我要查看元素 x,我通常会写 len(s) > x,即使 x == 0,但如果我关心“是这个特定的字符串”,我倾向于写 s ==“”。可以合理地假设成熟的编译器会将 len(s) == 0 和 s == "" 编译为相同的高效代码。 ... 使代码清晰。

正如 Timmmm's answer 中所指出的,Go 编译器在这两种情况下都会生成相同的代码。


我不同意这个答案。简单的 if mystring != "" { } 是当今最好、首选和惯用的方式。标准库包含其他内容的原因是因为它是在 2010 年之前编写的,当时 len(mystring) == 0 优化有意义。
@honzajde 刚刚尝试验证您的声明,但在标准库中发现使用 len 不到 1 年的提交来检查空/非空字符串。就像 Brad Fitzpatrick 的 commit。恐怕这仍然是品味和清晰度的问题;)
@honzajde 不是拖钓。提交中有 3 个 len 关键字。我指的是 h2_bundle.go 中的 len(v) > 0(第 2702 行)。我相信它不会自动显示,因为它是从 golang.org/x/net/http2 生成的。
如果它是差异中的 noi,那么它不是新的。为什么不直接发链接?无论如何。对我来说足够的侦探工作......我没有看到它。
@honzajde您实际上必须扩展该文件的差异,因为它太大了。
z
zzzz

这似乎是过早的微优化。编译器可以自由地为这两种情况或至少为这两种情况生成相同的代码

if len(s) != 0 { ... }

if s != "" { ... }

因为语义显然是相等的。


同意,但是,它实际上取决于字符串的实现......如果字符串像pascal那样实现,那么len(s)在o(1)中执行,如果像C那么它是o(n)。或者其他什么,因为 len() 必须执行完成。
您是否查看过代码生成以查看编译器是否预料到这一点,或者您是否只是建议编译器可以实现这一点?
E
Edwinner

假设应该删除空格和所有前导和尾随空格:

import "strings"
if len(strings.TrimSpace(s)) == 0 { ... }

因为:
len("") // is 0
len(" ") // one empty space is 1
len(" ") // two empty spaces is 2


为什么你有这个假设?这家伙清楚地讲述了空字符串。假设您只需要字符串中的 ascii 字符,然后添加一个删除所有非 ascii 字符的函数,您可以按照相同的方式进行判断。
因为 len("") , len(" ") 和 len(" ") 在 go 中不是一回事。我假设他想确保他之前初始化为其中之一的变量确实“在技术上”仍然是空的。
这实际上正是我从这篇文章中需要的。我需要用户输入至少有 1 个非空白字符,并且这个单行字清晰简洁。我需要做的就是使 if 条件 < 1 +1
W
Wilhelm Murdoch

检查长度是一个很好的答案,但您也可以考虑一个“空”字符串,它也只是空格。不是“技术上”空的,但如果您愿意检查:

package main

import (
  "fmt"
  "strings"
)

func main() {
  stringOne := "merpflakes"
  stringTwo := "   "
  stringThree := ""

  if len(strings.TrimSpace(stringOne)) == 0 {
    fmt.Println("String is empty!")
  }

  if len(strings.TrimSpace(stringTwo)) == 0 {
    fmt.Println("String two is empty!")
  }

  if len(stringTwo) == 0 {
    fmt.Println("String two is still empty!")
  }

  if len(strings.TrimSpace(stringThree)) == 0 {
    fmt.Println("String three is empty!")
  }
}

TrimSpace 将从原始字符串分配和复制一个新字符串,因此这种方法会大规模引入低效率。
@Dai 查看源代码,只有在给定 s 是字符串类型,s[0:i] 返回一个新副本的情况下才是正确的。字符串在 Go 中是不可变的,所以它需要在这里创建一个副本吗?
@MichaelPaesold Right - 如果字符串不需要修剪,strings.TrimSpace( s ) 将不会导致新的字符串分配和字符复制,但如果字符串确实需要修剪,则将调用额外的副本(没有空白字符)。
“技术上是空的”是个问题。
gocritic linter 建议使用 strings.TrimSpace(str) == "" 而不是长度检查。
T
Timmmm

到目前为止,Go 编译器在这两种情况下都会生成相同的代码,所以这是个人喜好问题。 GCCGo 确实会生成不同的代码,但几乎没有人使用它,所以我不会担心。

https://godbolt.org/z/fib1x1


J
Janis Viksne

根据官方指南,从性能的角度来看,它们看起来是等效的 (ANisus answer),由于语法优势,s != "" 会更好。如果变量不是字符串,s != "" 将在编译时失败,而 len(s) == 0 将通过其他几种数据类型。


有一段时间,我计算了 CPU 周期并查看了 C 编译器生成的汇编程序,并深入理解了 C 和 Pascal 字符串的结构......即使有世界上所有的优化len(),也只需要一点额外的工作.然而,我们曾经在 C 中做的一件事是将左侧强制转换为 const 或将静态字符串放在运算符的左侧以防止 s=="" 变成 s="" ,这在 C 语法中是可以接受的......而且可能也是golang。 (见扩展如果)
B
BaCaRoZzo

我认为 == "" 更快且更具可读性。

package main 

import(
    "fmt"
)
func main() {
    n := 1
    s:=""
    if len(s)==0{
        n=2
    }
    fmt.Println("%d", n)
}

dlv debug playground.go cmp 与 len(s) 和 =="" 我得到这个 s == "" 情况

    playground.go:6         0x1008d9d20     810b40f9        MOVD 16(R28), R1       
    playground.go:6         0x1008d9d24     e28300d1        SUB $32, RSP, R2       
    playground.go:6         0x1008d9d28     5f0001eb        CMP R1, R2             
    playground.go:6         0x1008d9d2c     09070054        BLS 56(PC)             
    playground.go:6         0x1008d9d30*    fe0f16f8        MOVD.W R30, -160(RSP)  

    playground.go:6         0x1008d9d34     fd831ff8        MOVD R29, -8(RSP)      
    playground.go:6         0x1008d9d38     fd2300d1        SUB $8, RSP, R29       
    playground.go:7         0x1008d9d3c     e00340b2        ORR $1, ZR, R0         

    playground.go:7         0x1008d9d40     e01f00f9        MOVD R0, 56(RSP)       
    playground.go:8         0x1008d9d44     ff7f05a9        STP (ZR, ZR), 80(RSP)  

    playground.go:9         0x1008d9d48     01000014        JMP 1(PC)                        
    playground.go:10        0x1008d9d4c     e0037fb2        ORR $2, ZR, R0         

len(s)==0 情况

    playground.go:6         0x100761d20     810b40f9        MOVD 16(R28), R1       
    playground.go:6         0x100761d24     e2c300d1        SUB $48, RSP, R2       
    playground.go:6         0x100761d28     5f0001eb        CMP R1, R2             
    playground.go:6         0x100761d2c     29070054        BLS 57(PC)             
    playground.go:6         0x100761d30*    fe0f15f8        MOVD.W R30, -176(RSP)  

    playground.go:6         0x100761d34     fd831ff8        MOVD R29, -8(RSP)      
    playground.go:6         0x100761d38     fd2300d1        SUB $8, RSP, R29       
    playground.go:7         0x100761d3c     e00340b2        ORR $1, ZR, R0         

    playground.go:7         0x100761d40     e02300f9        MOVD R0, 64(RSP)       
    playground.go:8         0x100761d44     ff7f06a9        STP (ZR, ZR), 96(RSP)  
    playground.go:9         0x100761d48     ff2700f9        MOVD ZR, 72(RSP)       

    playground.go:9         0x100761d4c     01000014        JMP 1(PC)              
    playground.go:10        0x100761d50     e0037fb2        ORR $2, ZR, R0         
    playground.go:10        0x100761d54     e02300f9        MOVD R0, 64(RSP)       
    playground.go:10        0x100761d58     01000014        JMP 1(PC)      
    playground.go:6         0x104855d2c     09070054        BLS 56(PC)        

块引用


有趣...但是,可读性取决于您是否在代码中思考:“空字符串”与“零长度字符串”。 (我不确定我如何看待它,所以下次我必须抓住自己)。也就是说,golang 将字符串视为符文切片,我不确定是否将切片称为空长度或零长度。我想一致性会更好。我曾经是 ASM 时钟周期计数器,但通过编译器和运行时优化,我不确定计算周期、跳转、缓存失效是否有意义。谢谢,这带回了回忆。
I
Ioannis Sermetziadis

使用如下函数会更简洁,更不容易出错:

func empty(s string) bool {
    return len(strings.TrimSpace(s)) == 0
}

M
Markus Linnala

只是为了向 comment 添加更多内容

主要是关于如何进行性能测试。

我使用以下代码进行了测试:

import (
    "testing"
)

var ss = []string{"Hello", "", "bar", " ", "baz", "ewrqlosakdjhf12934c r39yfashk fjkashkfashds fsdakjh-", "", "123"}

func BenchmarkStringCheckEq(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss {
                    if s == "" {
                            c++
                    }
            }
    } 
    t := 2 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}
func BenchmarkStringCheckLen(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss { 
                    if len(s) == 0 {
                            c++
                    }
            }
    } 
    t := 2 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}
func BenchmarkStringCheckLenGt(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss {
                    if len(s) > 0 {
                            c++
                    }
            }
    } 
    t := 6 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}
func BenchmarkStringCheckNe(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss {
                    if s != "" {
                            c++
                    }
            }
    } 
    t := 6 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}

结果是:

% for a in $(seq 50);do go test -run=^$ -bench=. --benchtime=1s ./...|grep Bench;done | tee -a log
% sort -k 3n log | head -10

BenchmarkStringCheckEq-4        150149937            8.06 ns/op
BenchmarkStringCheckLenGt-4     147926752            8.06 ns/op
BenchmarkStringCheckLenGt-4     148045771            8.06 ns/op
BenchmarkStringCheckNe-4        145506912            8.06 ns/op
BenchmarkStringCheckLen-4       145942450            8.07 ns/op
BenchmarkStringCheckEq-4        146990384            8.08 ns/op
BenchmarkStringCheckLenGt-4     149351529            8.08 ns/op
BenchmarkStringCheckNe-4        148212032            8.08 ns/op
BenchmarkStringCheckEq-4        145122193            8.09 ns/op
BenchmarkStringCheckEq-4        146277885            8.09 ns/op

实际上,变体通常不会达到最快时间,并且变体最高速度之间只有很小的差异(大约 0.01ns/op)。

如果我查看完整的日志,尝试之间的差异大于基准函数之间的差异。

此外,BenchmarkStringCheckEq 和 BenchmarkStringCheckNe 或 BenchmarkStringCheckLen 和 BenchmarkStringCheckLenGt 之间似乎没有任何可测量的差异,即使后者的变体应该增加 6 次而不是 2 次。

您可以尝试通过添加带有修改后的测试或内部循环的测试来获得对相同性能的信心。这更快:

func BenchmarkStringCheckNone4(b *testing.B) {
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, _ = range ss {
                    c++
            }
    }
    t := len(ss) * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}

这不是更快:

func BenchmarkStringCheckEq3(b *testing.B) {
    ss2 := make([]string, len(ss))
    prefix := "a"
    for i, _ := range ss {
            ss2[i] = prefix + ss[i]
    }
    c := 0
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
            for _, s := range ss2 {
                    if s == prefix {
                            c++
                    }
            }
    }
    t := 2 * b.N
    if c != t {
            b.Fatalf("did not catch empty strings: %d != %d", c, t)
    }
}

两种变体通常比主要测试之间的差异更快或更慢。

使用具有相关分布的字符串生成器生成测试字符串(ss)也很好。并且也有可变长度。

所以我对在 go 中测试空字符串的主要方法之间的性能差异没有任何信心。

而且我可以肯定地说,根本不测试空字符串比测试空字符串要快。而且测试空字符串比测试 1 个字符字符串(前缀变体)更快。


B
Brian Leishman

这比修剪整个字符串更高效,因为您只需要检查至少一个非空格字符是否存在

// Strempty checks whether string contains only whitespace or not
func Strempty(s string) bool {
    if len(s) == 0 {
        return true
    }

    r := []rune(s)
    l := len(r)

    for l > 0 {
        l--
        if !unicode.IsSpace(r[l]) {
            return false
        }
    }

    return true
}

@Richard 可能是这样,但是当谷歌搜索“golang 检查字符串是否为空白”或类似的东西时,这是唯一出现的问题,所以对于那些人来说,这是给他们的,这不是前所未有的事情堆栈交换
K
Ketan Parmar

我认为最好的方法是与空白字符串进行比较

BenchmarkStringCheck1 正在检查空白字符串

BenchmarkStringCheck2 正在检查 len 零

我检查空和非空字符串检查。您可以看到使用空白字符串进行检查更快。

BenchmarkStringCheck1-4     2000000000           0.29 ns/op        0 B/op          0 allocs/op
BenchmarkStringCheck1-4     2000000000           0.30 ns/op        0 B/op          0 allocs/op


BenchmarkStringCheck2-4     2000000000           0.30 ns/op        0 B/op          0 allocs/op
BenchmarkStringCheck2-4     2000000000           0.31 ns/op        0 B/op          0 allocs/op

代码

func BenchmarkStringCheck1(b *testing.B) {
    s := "Hello"
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        if s == "" {

        }
    }
}

func BenchmarkStringCheck2(b *testing.B) {
    s := "Hello"
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        if len(s) == 0 {

        }
    }
}

我认为这证明不了。由于您的计算机在测试时会做其他事情,并且差异很小,可以说一个比另一个更快。这可能暗示两个函数都被编译为同一个调用。