¿Cuál es la mejor manera de probar una cadena vacía en Go?

261

¿Qué método es el mejor (más idomático) para probar cadenas no vacías (en Go)?

if len(mystring) > 0 { }

O:

if mystring != "" { }

¿O algo mas?

Ricardo
fuente

Respuestas:

390

Ambos estilos se usan dentro de las bibliotecas estándar de Go.

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

se puede encontrar en el strconvpaquete: http://golang.org/src/pkg/strconv/atoi.go

if s != "" { ... }

se puede encontrar en el encoding/jsonpaquete: http://golang.org/src/pkg/encoding/json/encode.go

Ambos son idiomáticos y son lo suficientemente claros. Es más una cuestión de gusto personal y de claridad.

Russ Cox escribe en un hilo de nueces de golang :

El que aclara el código.
Si estoy a punto de ver el elemento x, normalmente escribo
len (s)> x, incluso para x == 0, pero si me importa
"¿es esta cadena específica" tiendo a escribir s == "".

Es razonable suponer que un compilador maduro compilará
len (s) == 0 y s == "" en el mismo código eficiente.
...

Haz el código claro.

Como se señaló en la respuesta de Timmmm , el compilador Go genera código idéntico en ambos casos.

ANisus
fuente
1
No estoy de acuerdo con esta respuesta. Simplemente if mystring != "" { }es la mejor, preferida e idiomática HOY. La razón por la que la biblioteca estándar contiene lo contrario es porque fue escrita antes de 2010 cuando la len(mystring) == 0optimización tenía sentido.
honzajde
12
@honzajde Intenté validar su declaración, pero encontré confirmaciones en la biblioteca estándar de menos de 1 año de antigüedad lenpara verificar cadenas vacías / no vacías. Me gusta este compromiso de Brad Fitzpatrick. Me temo que todavía es una cuestión de gusto y claridad;)
ANisus
66
@honzajde No trolling. Hay 3 palabras clave len en el commit. Me refería a len(v) > 0en h2_bundle.go (línea 2702). Creo que no se muestra automáticamente, ya que se genera desde golang.org/x/net/http2.
ANisus
2
Si es noi en el diff, entonces no es nuevo. ¿Por qué no publicas un enlace directo? De todos modos suficiente trabajo de detective para mí ... No lo veo.
honzajde
66
@honzajde No te preocupes. Supongo que otros sabrán cómo hacer clic en "Cargar diff" para el archivo h2_bundle.go.
ANisus
30

Esto parece ser una microoptimización prematura. El compilador es libre de producir el mismo código para ambos casos o al menos para estos dos

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

y

if s != "" { ... }

porque la semántica es claramente igual.

zzzz
fuente
1
de acuerdo, sin embargo, realmente depende de la implementación de la cadena ... Si las cadenas se implementan como pascal, entonces len (s) se ejecuta en o (1) y si, como C, entonces es o (n). o lo que sea, ya que len () tiene que ejecutarse hasta su finalización.
Richard
¿Has mirado la generación de código para ver si el compilador anticipa esto o solo estás sugiriendo que un compilador podría implementar esto?
Michael Labbé
19

Verificar la longitud es una buena respuesta, pero también podría tener en cuenta una cadena "vacía" que también es solo un espacio en blanco. No está "técnicamente" vacío, pero si le interesa 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
fuente
TrimSpaceasignará y copiará una nueva cadena de la cadena original, por lo que este enfoque introducirá ineficiencias a escala.
Dai
@Dai mirando el código fuente, eso solo sería cierto si, dado que ses de tipo cadena, s[0:i]devuelve una nueva copia. Las cadenas son inmutables en Go, entonces, ¿necesita crear una copia aquí?
Michael Paesold
@MichaelPaesold Right: strings.TrimSpace( s )no provocará una nueva asignación de cadena y copia de caracteres si la cadena no necesita recorte, pero si la cadena necesita recorte, se invocará la copia adicional (sin caracteres de espacio en blanco).
Dai
1
"técnicamente vacío" es la pregunta.
Richard
El gocriticlinter sugiere usar en strings.TrimSpace(str) == ""lugar de la verificación de longitud.
y3sh
12

Suponiendo que los espacios vacíos y todos los espacios en blanco iniciales y finales deben eliminarse:

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
fuente
2
¿Por qué tienes esta suposición? El chico claramente habla sobre la cadena vacía. De la misma manera, puede suponer que solo desea caracteres ascii en una cadena y luego agregar una función que elimine todos los caracteres no ascii.
Salvador Dali
1
Debido a que len (""), len ("") y len ("") no son lo mismo. Asumí que quería asegurarse de que una variable que había inicializado a una de esas anteriores todavía esté "técnicamente" vacía.
Edwinner el
En realidad, esto es exactamente lo que necesitaba de esta publicación. Necesito la entrada del usuario para tener al menos 1 carácter que no sea un espacio en blanco y esta línea es clara y concisa. Todo lo que necesito hacer es hacer la condición if < 1+1
Shadoninja
7

A partir de ahora, el compilador Go genera un código idéntico en ambos casos, por lo que es cuestión de gustos. GCCGo genera un código diferente, pero casi nadie lo usa, así que no me preocuparía por eso.

https://godbolt.org/z/fib1x1

Timmmm
fuente
1

Sería más limpio y menos propenso a errores usar una función como la siguiente:

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

Solo para agregar más para comentar

Principalmente sobre cómo hacer pruebas de rendimiento.

Hice pruebas con el siguiente 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)
    }
}

Y los resultados fueron:

% 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

Las variantes efectivas generalmente no alcanzan el tiempo más rápido y solo hay una diferencia mínima (aproximadamente 0.01ns / op) entre la velocidad máxima de la variante.

Y si miro el registro completo, la diferencia entre los intentos es mayor que la diferencia entre las funciones de referencia.

Además, no parece haber ninguna diferencia apreciable entre BenchmarkStringCheckEq y BenchmarkStringCheckNe o BenchmarkStringCheckLen y BenchmarkStringCheckLenGt, incluso si estas últimas variantes deberían aumentar 6 veces en lugar de 2 veces.

Puede intentar obtener algo de confianza sobre el mismo rendimiento agregando pruebas con prueba modificada o bucle interno. Esto es más 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)
    }
}

Esto no es más 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 variantes suelen ser más rápidas o más lentas que la diferencia entre las pruebas principales.

También sería bueno generar cadenas de prueba (ss) utilizando un generador de cadenas con distribución relevante. Y tienen longitudes variables también.

Por lo tanto, no tengo ninguna confianza en la diferencia de rendimiento entre los métodos principales para probar la cadena vacía en go.

Y puedo decir con cierta confianza, es más rápido no probar una cadena vacía que probar una cadena vacía. Y también es más rápido probar una cadena vacía que probar 1 cadena de caracteres (variante de prefijo).

Markus Linnala
fuente
0

Según las pautas oficiales y desde el punto de vista del rendimiento, parecen equivalentes ( respuesta ANisus ), s! = "" Sería mejor debido a una ventaja sintáctica. s! = "" fallará en tiempo de compilación si la variable no es una cadena, mientras que len (s) == 0 pasará por varios otros tipos de datos.

Janis Viksne
fuente
Hubo un tiempo en el que contaba los ciclos de CPU y revisaba el ensamblador que el compilador de C producía y entendía profundamente la estructura de las cadenas de C y Pascal ... incluso con todas las optimizaciones en el mundo, len()requiere un poco de trabajo extra. SIN EMBARGO, una cosa que solíamos hacer en C era lanzar el lado izquierdo a una consto colocar la cadena estática en el lado izquierdo del operador para evitar que s == "" se convierta en s = "", lo cual en la sintaxis de C es aceptable. .. y probablemente golang también. (ver el extendido si)
Richard
-1

Esto sería más eficaz que recortar toda la cadena, ya que solo necesita verificar al menos un carácter que no sea un espacio existente

// 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
fuente
3
@Richard puede ser, pero cuando buscas en Google "verifica si la cadena está en blanco" o algo similar, esta es la única pregunta que surge, así que para esas personas esto es para ellos, lo cual no es algo sin precedentes que hacer en Intercambio de pila
Brian Leishman,
-1

Creo que la mejor manera es comparar con una cadena en blanco

BenchmarkStringCheck1 está comprobando con una cadena en blanco

BenchmarkStringCheck2 está comprobando con len zero

Compruebo con la comprobación de cadena vacía y no vacía. Puede ver que verificar con una cadena en blanco es más rápido.

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
fuente
55
Creo que esta prueba nada. Dado que su computadora hace otras cosas cuando realiza las pruebas, la diferencia es demasiado pequeña para decir que una es más rápida que la otra. Esto podría indicar que ambas funciones se compilaron en la misma llamada.
SR