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


260

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

if len(mystring) > 0 { }

要么:

if mystring != "" { }

或者是其他东西?

Answers:


388

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线程中写道:

使代码清晰的代码。
如果我要查看元素x
,即使x == 0,我通常也写len(s)> x,但是如果我关心
“是这个特定的字符串”,我倾向于写s ==“”。

合理地假设成熟的编译器会将
len == 0和s ==“” 编译成相同的高效代码。
...

使代码清晰。

正如Timmmm的回答所指出,Go编译器在两种情况下的确生成相同的代码。


1
我不同意这个答案。简直if mystring != "" { }是当今最好,首选和惯用的方式。标准库之所以包含其他原因,是因为它是在2010年之前进行len(mystring) == 0优化时编写的。
honzajde

12
@honzajde刚刚尝试验证您的语句,但是在不到1年的标准库中发现了len用于检查空/非空字符串的提交。像这样的承诺由布拉德·菲茨帕特里克。恐怕仍然是口味和清晰度的问题;)
ANisus

6
@honzajde不拖钓。提交中有3个len关键字。我指的len(v) > 0h2_bundle.go(第2702行)。我相信它不会自动显示,因为它是从golang.org/x/net/http2生成的。
阿尼斯(Anisus)'17年

2
如果在差异中是noi,那不是新的。为什么不发布直接链接?无论如何。对我来说足够的侦探工作...我看不到。
honzajde

6
@honzajde不用担心。我假设其他人会知道如何单击h2_bundle.go文件的“ Load diff”。
阿尼斯(Anisus)'17年

30

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

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

if s != "" { ... }

因为语义显然是相等的。


1
但是,这确实取决于字符串的实现...如果像pascal那样实现字符串,则在o(1)中执行len(s),如果像C这样执行,则为o(n)。或其他原因,因为len()必须执行到完成。
理查德

您是否查看过代码生成以查看编译器是否预期到这一点,或者仅建议编译器可以实现此目的?
MichaelLabbé18年

19

检查长度是一个很好的答案,但是您也可以考虑一个仅包含空格的“空”字符串。不是“技术上”为空,而是如果您希望检查以下内容:

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中是不可变的,是否需要在此处创建副本?
Michael Paesold '18

@MichaelPaesold对- strings.TrimSpace( s )如果不需要修剪字符串,将不会导致新的字符串分配和字符复制,但是如果需要修剪字符串,则将调用多余的副本(不包含空格字符)。

1
问题是“技术上是空的”。
理查德

gocritic棉短绒建议使用strings.TrimSpace(str) == "",而不是长度检查。
y3sh

12

假设应该删除空白空间以及所有前导和尾随空白:

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

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


2
你为什么有这个假设?那家伙清楚地讲出了空字符串。假设您只想在字符串中使用ascii字符,然后添加一个删除所有非ascii字符的函数,则您可以用相同的方式告诉您。
萨尔瓦多·达利

1
因为len(“”),len(“”)和len(“”)并不是一回事。我以为他要确保已将其初始化为较早的那些变量的确实是“技术上”为空的变量。
Edwinner '17

实际上这正是我从这篇文章中所需要的。我需要用户输入至少有1个非空白字符,并且这种单行代码清晰明了。我需要做的就是使if条件< 1+1
Shadoninja '17

7

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

https://godbolt.org/z/fib1x1



0

只是添加更多评论

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

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

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)也是很好的。并且长度也可变。

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

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


0

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


曾经有一段时间我计算CPU周期并回顾C编译器产生的汇编程序,并深刻理解C和Pascal字符串的结构...即使世界上的所有优化都只len()需要一点点额外的工作。但是,我们过去在C语言中所做的一件事是将左侧const的字符强制转换为a 或将静态字符串放置在运算符的左侧,以防止s =“ =”“变为s =”“,这在C语法中是可以接受的。 ..也可能是golang。(如果有,请参见扩展)
理查德

-1

这将比修剪整个字符串更有效,因为您只需要检查至少一个非空格字符即可

// 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
}

3
@Richard可能是,但是当谷歌搜索“ golang检查字符串是否为空”或类似的东西时,这是唯一出现的问题,因此对于那些人来说,这是给他们的,这不是史无前例的事情堆栈交换
Brian Leishman

-1

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

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 {

        }
    }
}

5
我认为这个证明没什么。由于您的计算机在测试时会做其他事情,因此差异很小就可以说一个更快。这可能暗示这两个函数已编译到同一调用。
SR
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.