Qual é a melhor maneira de testar uma string vazia no Go?

261

Qual método é o melhor (mais idomatic) para testar seqüências de caracteres não vazias (no Go)?

if len(mystring) > 0 { }

Ou:

if mystring != "" { }

Ou alguma outra coisa?

Richard
fonte

Respostas:

390

Ambos os estilos são usados ​​nas bibliotecas padrão do Go.

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

pode ser encontrado no strconvpacote: http://golang.org/src/pkg/strconv/atoi.go

if s != "" { ... }

pode ser encontrado no encoding/jsonpacote: http://golang.org/src/pkg/encoding/json/encode.go

Ambos são idiomáticos e são claros o suficiente. É mais uma questão de gosto pessoal e clareza.

Russ Cox escreve em um tópico de golang-nuts :

Aquele que torna o código claro.
Se estou prestes a olhar para o elemento x, normalmente escrevo
len (s)> x, mesmo para x == 0, mas se me preocupo com
"é essa sequência específica", tendem a escrever s == "".

É razoável supor que um compilador maduro compile
len (s) == 0 es == "" no mesmo código eficiente.
...

Faça o código claro.

Como apontado na resposta de Timmmm , o compilador Go gera código idêntico nos dois casos.

ANisus
fonte
1
Eu não concordo com esta resposta. Simplesmente if mystring != "" { }é a melhor, preferida e idiomática maneira HOJE. A razão pela qual a biblioteca padrão contém o contrário é porque ela foi escrita antes de 2010 quando a len(mystring) == 0otimização fazia sentido.
Honzajde 13/05
12
@honzajde Apenas tentei validar sua declaração, mas encontrei commits na biblioteca padrão com menos de 1 ano de idade usando lenpara verificar cadeias vazias / não vazias. Como este cometido por Brad Fitzpatrick. Receio que ainda é uma questão de gosto e clareza;)
ANisus
6
@honzajde Não estou trollando. Existem 3 palavras-chave len no commit. Eu estava me referindo len(v) > 0no h2_bundle.go (linha 2702). Não é mostrado automaticamente, pois é gerado a partir de golang.org/x/net/http2, acredito.
ANisus
2
Se é noi no diff, então não é novo. Por que você não publica link direto? Enfim. trabalho de detetive suficiente para mim ... eu não vejo isso.
Honzajde 16/05/2019
6
@honzajde Não se preocupe. Suponho que outros saberão como clicar em "Carregar diff" para o arquivo h2_bundle.go.
ANisus 16/05
30

Parece microoptimização prematura. O compilador é livre para produzir o mesmo código para os dois casos ou pelo menos para esses dois

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

e

if s != "" { ... }

porque a semântica é claramente igual.

zzzz
fonte
1
concordou, no entanto, isso realmente depende da implementação da string ... Se strings são implementadas como pascal, então len (s) é executado em o (1) e se como C, então é o (n). ou o que for, já que len () precisa ser executado até o final.
Richard
Você já olhou a geração de código para ver se o compilador antecipa isso ou está apenas sugerindo que um compilador possa implementar isso?
Michael Labbé
19

A verificação do comprimento é uma boa resposta, mas você também pode ter uma string "vazia" que também é apenas espaço em branco. Não "tecnicamente" vazio, mas se você deseja verificar:

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!")
  }
}
Wilhelm Murdoch
fonte
TrimSpacealocará e copiará uma nova string da string original; portanto, essa abordagem introduzirá ineficiências em escala.
Dai
@ Dai olhando para o código fonte, isso seria verdadeiro apenas se, dado o stipo string, s[0:i]retornar uma nova cópia. As strings são imutáveis ​​no Go, então é necessário criar uma cópia aqui?
Michael Paesold
@MichaelPaesold Right - strings.TrimSpace( s )não causará nova alocação de string e cópia de caracteres se a string não precisar de aparar, mas se a string precisar de aparar, a cópia extra (sem caracteres de espaço em branco) será chamada.
Dai #
1
"tecnicamente vazio" é a questão.
Richard
O gocriticlinter sugere usar em strings.TrimSpace(str) == ""vez da verificação de comprimento.
y3sh
12

Supondo que os espaços vazios e todos os espaços em branco iniciais e finais devem ser removidos:

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

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

Edwinner
fonte
2
Por que você tem essa suposição? O cara fala claramente sobre a corda vazia. Da mesma maneira que você pode dizer, supondo que você queira apenas caracteres ascii em uma string e adicione uma função que remove todos os caracteres não-ascii.
Salvador Dali
1
Porque len (""), len ("") e len ("") não são a mesma coisa. Eu estava assumindo que ele queria ter certeza de que uma variável que ele havia inicializado para uma daquelas anteriormente ainda está "tecnicamente" vazia.
Edwinner
Isso é exatamente o que eu precisava neste post. Eu preciso que a entrada do usuário tenha pelo menos 1 caractere que não seja um espaço em branco e esse liner é claro e conciso. Tudo o que preciso fazer é criar a condição if < 1+1
Shadoninja 10/17/17
7

A partir de agora, o compilador Go gera código idêntico nos dois casos, portanto é uma questão de gosto. O GCCGo gera código diferente, mas quase ninguém o usa, então eu não me preocuparia com isso.

https://godbolt.org/z/fib1x1

Timmmm
fonte
1

Seria mais limpo e menos propenso a erros usar uma função como a abaixo:

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

Apenas para adicionar mais para comentar

Principalmente sobre como fazer testes de desempenho.

Eu testei com o seguinte código:

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

E os resultados foram:

% 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

As variantes efetivamente geralmente não atingem o tempo mais rápido e há apenas uma diferença mínima (cerca de 0,01ns / op) entre a velocidade máxima da variante.

E se eu olhar o log completo, a diferença entre as tentativas é maior que a diferença entre as funções de benchmark.

Também não parece haver nenhuma diferença mensurável entre BenchmarkStringCheckEq e BenchmarkStringCheckNe ou BenchmarkStringCheckLen e BenchmarkStringCheckLenGt, mesmo que as últimas variantes devam aumentar c 6 vezes em vez de 2 vezes.

Você pode tentar obter alguma confiança sobre desempenho igual adicionando testes com teste modificado ou loop interno. Isso é mais rápido:

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

Isso não é mais rápido:

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

Ambas as variantes são geralmente mais rápidas ou mais lentas que a diferença entre os testes principais.

Também seria bom gerar strings de teste (ss) usando gerador de strings com distribuição relevante. E tem comprimentos variáveis ​​também.

Portanto, não tenho nenhuma confiança na diferença de desempenho entre os métodos principais para testar a string vazia em movimento.

E posso afirmar com alguma confiança: é mais rápido não testar a string vazia do que testar a string vazia. E também é mais rápido testar uma string vazia do que testar uma string de char (variante de prefixo).

Markus Linnala
fonte
0

De acordo com as diretrizes oficiais e do ponto de vista do desempenho, elas parecem equivalentes ( resposta do ANisus ), o s! = "" Seria melhor devido a uma vantagem sintática. s! = "" falhará no tempo de compilação se a variável não for uma string, enquanto len (s) == 0 passará por vários outros tipos de dados.

Janis Viksne
fonte
Houve um tempo em que contei os ciclos da CPU e revisei o assembler que o compilador C produziu e compreendi profundamente a estrutura das seqüências C e Pascal ... mesmo com todas as otimizações do mundo, é len()preciso apenas um pouco de trabalho extra. No entanto, uma coisa que costumávamos fazer em C era converter o lado esquerdo em a constou colocar a string estática no lado esquerdo do operador para impedir que s == "" se tornasse s = "", o que na sintaxe C é aceitável. .. e provavelmente golang também. (veja o estendido se)
Richard
-1

Isso seria mais eficiente do que aparar toda a cadeia, pois você só precisa verificar se há pelo menos um único caractere não espacial

// 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
}
Brian Leishman
fonte
3
@ Richard pode ser, mas quando pesquisamos no Google sobre "golang, verifique se a string está em branco" ou coisas semelhantes, essa é a única pergunta que surge; portanto, para essas pessoas, é para elas, o que não é algo sem precedentes para se fazer. Stack Exchange
Brian Leishman
-1

Eu acho que a melhor maneira é comparar com a string em branco

BenchmarkStringCheck1 está verificando com uma string em branco

BenchmarkStringCheck2 está verificando com len zero

Verifico com a verificação de cadeia vazia e não vazia. Você pode ver que a verificação com uma sequência em branco é mais rápida.

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

Código

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 {

        }
    }
}
Ketan Parmar
fonte
5
Eu acho que essa prova nada. Como o seu computador faz outras coisas ao testar e a diferença é pequena demais, uma delas é mais rápida. Isso pode sugerir que as duas funções foram compiladas na mesma chamada.
SR