Go efectivo
Introducción
Go es un nuevo lenguaje. Aunque toma prestadas ideas de lenguajes existentes, tiene propiedades inusuales que hacen que los eficaces programas en Go tengan un carácter diferente de los programas escritos en sus parientes. Es poco probable que una traducción directa de un programa en C++ o Java a Go produzca un resultado satisfactorio—los programas Java están escritos en Java, no en Go. Por otro lado, pensar en el problema desde la perspectiva de Go podría producir un programa exitoso pero bastante diferente. En otras palabras, para escribir Go bien, es importante comprender sus propiedades y modismos. También es importante conocer las convenciones establecidas para programar en Go, como nombres, formatos, construcción de programas, etc., para que los programas que escribas sean fáciles de entender para otros programadores de Go.
Este documento brinda consejos para escribir código Go claro e idiomático. Complementa la Especificación del lenguaje ⬀, el Tour por Go ⬀ y Cómo escribir código Go ⬀, todo lo cual debes leer primero.
Nota agregada en enero de 2022: este documento fue escrito para el lanzamiento de Go en 2009 y no se ha actualizado significativamente desde entonces. Aunque es una buena guía para comprender cómo usar el lenguaje en sí, gracias a la estabilidad del lenguaje, dice poco sobre las bibliotecas y nada sobre cambios significativos en el ecosistema Go desde que fue escrito, como el sistema de compilación, los tests, los módulos y el polimorfismo. No hay planes para actualizarlo, por lo tanto han sucedido muchas cosas y un conjunto cada vez mayor de documentación, blogs y libros hacen un excelente trabajo al describir el uso moderno de Go. Go efectivo sigue siendo útil, pero el lector debe entender que está lejos de ser una guía completa. Ver issue28782 para más contexto.
Ejemplos
Las fuentes del paquete Go ⬀ están destinadas a servir no solo como biblioteca central sino también como ejemplos de cómo usar el lenguaje. Muchos de los paquetes contienen ejemplos ejecutables independientes y funcionales que puedes ejecutar directamente desde el sitio web golang.org, como este ⬀ (si es necesario, haz clic en la palabra "Ejemplo" para abrirlo). Si tienes alguna pregunta sobre cómo abordar un problema o cómo se podría implementar algo, la documentación, el código y los ejemplos de la biblioteca pueden proporcionar respuestas, ideas y antecedentes.
Formatear
Los problemas de formato son los más polémicos pero los menos trascendentes. Las personas pueden adaptarse a diferentes estilos de formato, pero es mejor si no es necesario, y se dedica menos tiempo al tema si todos se adhieren al mismo estilo. El problema es cómo abordar esta utopía sin una larga guía de estilo prescriptiva.
Con Go adoptamos un enfoque inusual y dejamos que la máquina se encargue de la mayoría de los problemas de formato. El programa gofmt
(también disponible como go fmt
), que opera a nivel de paquete en lugar de a nivel de archivo fuente) lee un programa Go y emite el código fuente en un estilo estándar de sangría y alineación vertical, reteniendo y, si es necesario, reformateando los comentarios. Si deseas saber cómo manejar alguna situación de diseño nueva, ejecuta gofmt
; Si la respuesta no te parece correcta, reorganiza tu programa (o presenta un error sobre gofmt
).
Como ejemplo, no hay necesidad de perder tiempo alineando los comentarios en los campos de una estructura.
Gofmt
lo hará por ti. Dada la declaración
type T struct { name string // name of the object value int // its value }
gofmt
alineará las columnas:
type T struct { name string // name of the object value int // its value }
Todo el código Go en los paquetes estándar ha sido formateado con gofmt
.
Quedan algunos detalles de formato. Muy corto:
- Sangría
- Usamos tabulaciones para la sangría y
gofmt
las emite de forma predeterminada. Utiliza espacios sólo si es necesario. - Longitud de línea
- Go no tiene límite de longitud de línea. No te preocupes por desbordar una tarjeta perforada. Si una línea parece demasiado larga, envuélvala y sangra con una tabulación adicional.
- Paréntesis
-
Go necesita menos paréntesis que C y Java: estructuras de control (
if
,for
,switch
) no tienen paréntesis en su sintaxis. Además, la jerarquía de precedencia de operadores es más corta y clara, por lo quex<<8 + y<<16
significa lo que implica el espaciado, a diferencia de otros lenguajes.
Comentario
Go proporciona comentarios de bloque /* */
estilo C y comentarios de línea //
estilo C++. Los comentarios de línea son la norma; los comentarios de bloque aparecen principalmente como comentarios de paquetes, pero son útiles dentro de una expresión o para deshabilitar grandes extensiones de código.
Se considera que los comentarios que aparecen antes de las declaraciones de nivel superior, sin nuevas líneas intermedias, documentan la declaración en sí. Estos "comentarios de documentación" son la documentación principal para un paquete o comando de Go determinado. Para obtener más información sobre los comentarios de documentación, consulta “Comentarios de Go Doc ⬀”.
Nombres
Los nombres son tan importantes en Go como en cualquier otro lenguaje. Incluso tienen un efecto semántico: la visibilidad de un nombre fuera de un paquete está determinada por si su primer carácter está en mayúscula. Por lo tanto, vale la pena dedicar un tiempo a hablar sobre convenciones de nomenclatura en programas Go.
Nombres de paquetes
Cuando se importa un paquete, el nombre del paquete se convierte en un acceso al contenido. Después
import "bytes"
el paquete de importación puede hablar de bytes.Buffer
. Es útil si todos los que usan el paquete pueden usar el mismo nombre para referirse a su contenido, lo que implica que el nombre del paquete debe ser bueno: breve, conciso y evocador. Por convención, los paquetes reciben nombres de una sola palabra en minúsculas; no debería haber necesidad de guiones bajos o mayúsculas mixtas. Err por el lado de la brevedad, ya que todos los que usen su paquete escribirán ese nombre. Y no te preocupes por las colisiones a priori. El nombre del paquete es solo el nombre predeterminado para las importaciones; no necesita ser único en todo el código fuente y, en el raro caso de una colisión, el paquete importador puede elegir un nombre diferente para usar localmente. En cualquier caso, la confusión es rara porque el nombre del archivo en la importación determina exactamente qué paquete se está utilizando.
Otra convención es que el nombre del paquete es el nombre base de su directorio fuente; el paquete en src/encoding/base64
se importa como "encoding/base64"
pero tiene el nombre base64
, no encoding_base64
ni encodingBase64
.
El importador de un paquete usará el nombre para referirse a su contenido, por lo que los nombres exportados en el paquete pueden usar ese hecho para evitar repeticiones. (No uses la notación import .
, que puede simplificar los tests que deben ejecutarse fuera del paquete que están testeando, pero que de lo contrario deben evitarse.) Por ejemplo, el tipo buffered reader en el paquete bufio
se llama Reader
, no BufReader
, porque los usuarios lo ven como bufio.Reader
, que es un nombre claro y conciso. Además, porque las entidades importadas siempre se abordan con su nombre del paquete, bufio.Reader
no entra en conflicto con io.Reader
. De manera similar, la función para crear nuevas instancias de ring.Ring
, que es la definición de un constructor en Go—normalmente se llamaría NewRing
, pero dado que
Ring
es el único tipo exportado por el paquete y dado que el paquete se llama ring
, se llama simplemente New
, que los clientes del paquete ven como ring.New
. Utiliza la estructura del paquete para ayudarte a elegir buenos nombres.
Otro ejemplo breve es once.Do
;
once.Do(setup)
se lee bien y no se mejoraría escribiendo once.DoOrWaitUntilDone(setup)
. Los nombres largos no hacen que las cosas sean más legibles automáticamente. Un comentario de documentación útil a menudo puede ser más valioso que un nombre extra largo.
Getters
Go no proporciona soporte automático para getters y setters. No hay nada de malo en proporcionar getters y setters tú mismo, y a menudo es apropiado hacerlo, pero no es ni idiomático ni necesario poner Get
en el nombre del getter. Si tienes un campo llamado owner
(minúscula, no exportado), el método getter debe llamarse Owner
(mayúscula, exportado), no GetOwner
. El uso de nombres en mayúsculas para la exportación proporciona el gancho para discriminar el campo del método. Una función setter, si es necesaria, probablemente se llamará SetOwner
. Ambos nombres se leen bien en la práctica:
owner := obj.Owner() if owner != user { obj.SetOwner(user) }
Nombres de interface
Por convención, las interfaces de un método se nombran con el nombre del método más un sufijo -er o una modificación similar para construir un nombre de agente: Reader
,
Writer
, Formatter
,
CloseNotifier
etc.
Existen varios nombres de este tipo y es productivo respetarlos y los nombres de funciones que capturan.
Read
, Write
, Close
, Flush
,
String
y así sucesivamente tienen firmas y significados canónicos. Para evitar confusiones, no le des a tu método uno de esos nombres a menos que tenga la misma firma y significado. Por el contrario, si tu tipo implementa un método con el mismo significado que un método en un tipo conocido, asígnale el mismo nombre y firma; llama a tu método convertidor a cadenas String
, no ToString
.
MixedCaps
Finalmente, la convención en Go es usar MixedCaps
o mixedCaps
en lugar de guiones bajos para escribir nombres de varias palabras.
Punto y coma
Al igual que C, la gramática formal de Go usa punto y coma para terminar declaraciones, pero a diferencia de C, esos puntos y coma no aparecen en la fuente. En cambio, el lexer usa una regla simple para insertar punto y coma automáticamente mientras escanea, por lo que el texto de entrada está prácticamente libre de ellos.
La regla es esta. Si el último token antes de una nueva línea es un identificador (que incluye palabras como int
y float64
), un literal básico como un número o una cadena constante, o uno de los tokens
break continue fallthrough return ++ -- ) }
el lexer siempre inserta un punto y coma después del token. Esto podría resumirse como, "si la nueva línea viene después de un token que podría finalizar una declaración, inserta un punto y coma".
Un punto y coma también se puede omitir inmediatamente antes de una llave de cierre, por lo que una declaración como
go func() { for { dst <- <-src } }()
no necesita punto y coma. Los programas en Go tienen punto y coma solo en lugares como las cláusulas de bucle
for
, para separar los elementos inicializador, condición y continuación. También son necesarios para separar varias declaraciones en una línea, en caso de que escribas el código de esa manera.
Una consecuencia de las reglas de inserción de punto y coma es que no puedes poner la llave de apertura de una estructura de control (if
, for
, switch
, o select
) en la siguiente línea. Si lo haces, se insertará un punto y coma antes de la llave, lo que podría provocar efectos no deseados. Escríbelos así
if i < f() { g() }
así no
if i < f() // wrong! { // wrong! g() }
Estructuras de control
Las estructuras de control de Go están relacionadas con las de C pero difieren en aspectos importantes. No hay un bucle do
o while
, solamente un for
ligeramente generalizado; switch
es más flexible; if
y switch
aceptan una declaración de inicialización opcional como la de for
; las declaraciones break
y continue
toman una etiqueta opcional para identificar qué interrumpir o continuar; y hay nuevas estructuras de control que incluyen un type switch y un multiplexor de comunicaciones multidireccional, select
. La sintaxis también es ligeramente diferente: no hay paréntesis y los cuerpos siempre deben estar delimitados por llaves.
If
En Go, un if
simple se ve así:
if x > 0 { return y }
Las llaves obligatorias alientan a escribir declaraciones if
simples en varias líneas. Es un buen estilo hacerlo de todos modos, especialmente cuando el cuerpo contiene una declaración de control como return
o break
.
Dado que if
y switch
aceptan una declaración de inicialización, es común ver una usada para configurar una variable local.
if err := file.Chmod(0664); err != nil { log.Print(err) return err }
En las bibliotecas de Go, encontrarás que cuando una declaración if
no fluye hacia la siguiente declaración, es decir, el cuerpo termina en break
, continue
, goto
o return
—el innecesario else
se omite.
f, err := os.Open(name) if err != nil { return err } codeUsing(f)
Este es un ejemplo de una situación común en la que el código debe protegerse contra una secuencia de condiciones de error. El código se lee bien si el flujo de control exitoso recorre la página, eliminando los casos de error a medida que surgen. Dado que los casos de error tienden a terminar en declaraciones return
, el código resultante no necesita declaraciones else
.
f, err := os.Open(name) if err != nil { return err } d, err := f.Stat() if err != nil { f.Close() return err } codeUsing(f, d)
Redeclaración y reasignación
Un comentario aparte: el último ejemplo de la sección anterior demuestra un detalle de cómo funciona la declaración corta :=
. La declaración que llama a os.Open
lee,
f, err := os.Open(name)
Esta declaración declara dos variables, f
y err
. Unas líneas más tarde, la llamada a f.Stat
lee,
d, err := f.Stat()
que parece declarar d
y err
. Sin embargo, observa que aparece err
en ambas declaraciones. Esta duplicación es legal: err
es declarado por la primera declaración, pero sólo reasignado en la segunda. Esto significa que la llamada a f.Stat
utiliza la variable err
existente declarada anteriormente y simplemente le asigna un nuevo valor.
En una declaración :=
puede aparecer una variable v
incluso si ya ha sido declarada, siempre que:
- esta declaración está en el mismo alcance que la declaración existente de
v
(siv
ya está declarado en un alcance externo, la declaración creará una nueva variable §), - el valor correspondiente en la inicialización se puede asignar a
v
, y - hay al menos otra variable creada por la declaración.
Esta propiedad inusual es puro pragmatismo, lo que facilita el uso de un único valor err
, por ejemplo, en una cadena if-else
. Verás que se usa con frecuencia.
§ Vale la pena señalar aquí que en Go el alcance de los parámetros de la función y los valores de retorno es el mismo que el del cuerpo de la función, aunque aparecen léxicamente fuera de las llaves que encierran el cuerpo.
For
El bucle Go for
es similar, pero no igual, al de C. Unifica for
y while
y no hay do-while
. Hay tres formas, solo uno de los cuales tiene punto y coma.
// Like a C for for init; condition; post { } // Like a C while for condition { } // Like a C for(;;) for { }
Las declaraciones cortas facilitan la declaración de la variable de índice directamente en el bucle.
sum := 0 for i := 0; i < 10; i++ { sum += i }
Si estás haciendo un bucle sobre una array, slice, string o map, o leyendo desde un channel, una cláusula range
puede administrar el bucle.
for key, value := range oldMap { newMap[key] = value }
Si solo necesitas el primer elemento del rango (la clave o índice), deshazte del segundo:
for key := range m { if key.expired() { delete(m, key) } }
Si solo necesitas el segundo elemento del rango (el valor), usa el identificador en blanco, un guión bajo, para descartar el primero:
sum := 0 for _, value := range array { sum += value }
El identificador en blanco tiene muchos usos, como se describe en una sección posterior.
Para cadenas, el range
hace más trabajo por ti, desglosando puntos de código Unicode individuales analizando el UTF-8. Las codificaciones erróneas consumen un byte y producen la runa U+FFFD de reemplazo. (El nombre (con el tipo incorporado asociado) rune
es la terminología de Go para un único punto de código Unicode. Consulta la especificación del lenguaje ⬀ para más detalles). El bucle
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding fmt.Printf("character %#U starts at byte position %d\n", char, pos) }
imprime
character U+65E5 '日' starts at byte position 0 character U+672C '本' starts at byte position 3 character U+FFFD '�' starts at byte position 6 character U+8A9E '語' starts at byte position 7
Finalmente, Go no tiene operador de coma y ++
y --
son declaraciones, no expresiones. Por lo tanto, si quieres ejecutar múltiples variables en un for
debes utilizar una asignación paralela (aunque eso excluye ++
y --
).
// Reverse a for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] }
Switch
El switch
de Go es más general que el de C. Las expresiones no necesitan ser constantes ni siquiera enteras, los casos se evalúan de arriba a abajo hasta que se encuentra una coincidencia, y si switch
no tiene expresión se considera true
. Por lo tanto, es posible (e idiomático) escribir una cadena if
-else
-if
-else
como un switch
.
func unhex(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0 }
No existe un fall through (caso por defecto) automático, pero los casos se pueden presentar en listas separadas por comas.
func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false }
Aunque no son tan comunes en Go como otros lenguajes similares a C, las sentencias break
se pueden usar para terminar un switch
temprano. Sin embargo, a veces es necesario salir de un bucle circundante, no el switch, y en Go eso se puede lograr colocando una etiqueta en el bucle y "rompiendo" esa etiqueta. Este ejemplo muestra ambos usos.
Loop: for n := 0; n < len(src); n += size { switch { case src[n] < sizeOne: if validateOnly { break } size = 1 update(src[n]) case src[n] < sizeTwo: if n+1 >= len(src) { err = errShortInput break Loop } if validateOnly { break } size = 2 update(src[n] + src[n+1]<<shift) } }
Por supuesto, la instrucción continue
también acepta una etiqueta opcional, pero se aplica solo a los bucles.
Para cerrar esta sección, aquí tienes una rutina de comparación para porciones de bytes que utiliza dos sentencias switch
:
// Compare returns an integer comparing the two byte slices, // lexicographically. // The result will be 0 if a == b, -1 if a < b, and +1 if a > b func Compare(a, b []byte) int { for i := 0; i < len(a) && i < len(b); i++ { switch { case a[i] > b[i]: return 1 case a[i] < b[i]: return -1 } } switch { case len(a) > len(b): return 1 case len(a) < len(b): return -1 } return 0 }
Switch de tipo
También se puede usar un switch para descubrir el tipo dinámico de una variable de interfaz. Un switch de tipo utiliza la sintaxis de una afirmación de tipo con la palabra clave type
entre paréntesis. Si el switch declara una variable en la expresión, la variable tendrá el tipo correspondiente en cada cláusula. También es idiomático reutilizar el nombre en tales casos, declarando de hecho una nueva variable con el mismo nombre pero de un tipo diferente en cada caso.
var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has case bool: fmt.Printf("boolean %t\n", t) // t has type bool case int: fmt.Printf("integer %d\n", t) // t has type int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int }
Funciones
Múltiples valores de retorno
Una de las características inusuales de Go es que las funciones y los métodos pueden devolver múltiples valores. Esta forma se puede utilizar para mejorar un par de modismos torpes en programas C: retornos de error dentro de las restricciones del valor de retorno como -1
para EOF
y modificación de un argumento pasado por dirección.
En C, un error de escritura se indica mediante un recuento negativo con el código de error escondido en una ubicación volátil. En Go, Write
puede devolver un recuento y un error: "Sí, escribiste algunos bytes pero no todos porque llenaste el dispositivo". La firma del método Write
en archivos del paquete os
es:
func (file *File) Write(b []byte) (n int, err error)
y como dice la documentación, devuelve el número de bytes escritos y un error
no nulo cuando n
!=
len(b)
. Este es un estilo común; consulta la sección sobre manejo de errores para obtener más ejemplos.
Un enfoque similar evita la necesidad de pasar un puntero a un valor de retorno para simular un parámetro de referencia. Aquí hay una función sencilla para tomar un número de una posición en un slice de bytes, devolver el número y la siguiente posición.
func nextInt(b []byte, i int) (int, int) { for ; i < len(b) && !isDigit(b[i]); i++ { } x := 0 for ; i < len(b) && isDigit(b[i]); i++ { x = x*10 + int(b[i]) - '0' } return x, i }
Puedes usarlo para escanear los números en un segmento de entrada b
como este:
for i := 0; i < len(b); { x, i = nextInt(b, i) fmt.Println(x) }
Parámetros de resultados con nombre
Los "parámetros" de retorno o resultado de una función Go pueden recibir nombres y usarse como variables regulares, al igual que los parámetros entrantes. Cuando se nombran, se inicializan a los valores cero para su tipo cuándo comienza la función; si la función ejecuta una instrucción return
sin argumentos, los valores actuales de los parámetros del resultado se utilizan como valores devueltos.
Los nombres no son obligatorios pero pueden hacer que el código sea más corto y claro: son documentación. Si nombramos los resultados de nextInt
, resulta obvio que devolvió int
es cuál.
func nextInt(b []byte, pos int) (value, nextPos int) {
Debido a que los resultados con nombre se inicializan y se vinculan a un retorno sin adornos, pueden simplificar y aclarar. Aquí hay una versión de io.ReadFull
que los usa bien:
func ReadFull(r Reader, buf []byte) (n int, err error) { for len(buf) > 0 && err == nil { var nr int nr, err = r.Read(buf) n += nr buf = buf[nr:] } return }
Defer
La instrucción de Go defer
programa una llamada de función (la función diferida) para que se ejecute inmediatamente antes de que la función que ejecuta el defer
finalice devolviendo un retorno. Es una manera inusual pero efectiva de lidiar con situaciones tales como recursos que deben liberarse independientemente del camino a retornar que tome una función. Los ejemplos canónicos son desbloquear un mutex o cerrar un archivo.
// Contents returns the file's contents as a string. func Contents(filename string) (string, error) { f, err := os.Open(filename) if err != nil { return "", err } defer f.Close() // f.Close will run when we're finished. var result []byte buf := make([]byte, 100) for { n, err := f.Read(buf[0:]) result = append(result, buf[0:n]...) // append is discussed later. if err != nil { if err == io.EOF { break } return "", err // f will be closed if we return here. } } return string(result), nil // f will be closed if we return here. }
Aplazar una llamada a una función como Close
tiene dos ventajas. Primero, garantiza que nunca olvidarás cerrar el archivo, un error que es fácil de cometer si luego editas la función para agregar una nueva ruta de retorno. En segundo lugar, significa que el cierre se sitúa cerca de la apertura, lo cual es mucho más claro que colocarlo al final de la función.
Los argumentos de la función diferida (que incluyen el receptor si la función es un método) se evalúan cuando se ejecuta defer, no cuando se ejecuta call. Además de evitar preocupaciones acerca de que las variables cambien los valores a medida que se ejecuta la función, esto significa que un único sitio de llamada diferida puede diferir múltiples ejecuciones de funciones. He aquí un ejemplo tonto.
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }
Las funciones diferidas se ejecutan en orden LIFO, por lo que este código hará que se imprima 4 3 2 1 0
cuando la función retorne. Un ejemplo más plausible es una forma sencilla de rastrear la ejecución de funciones a través del programa. Podríamos escribir un par de rutinas de rastreo simples como esta:
func trace(s string) { fmt.Println("entering:", s) } func untrace(s string) { fmt.Println("leaving:", s) } // Use them like this: func a() { trace("a") defer untrace("a") // do something.... }
Podemos hacerlo mejor aprovechando el hecho de que los argumentos de las funciones diferidas se evalúan cuando se ejecuta defer
. La rutina de seguimiento puede configurar el argumento de la rutina de desrastreo. Este ejemplo:
func trace(s string) string { fmt.Println("entering:", s) return s } func un(s string) { fmt.Println("leaving:", s) } func a() { defer un(trace("a")) fmt.Println("in a") } func b() { defer un(trace("b")) fmt.Println("in b") a() } func main() { b() }
imprime
entering: b in b entering: a in a leaving: a leaving: b
Para programadores acostumbrados a la gestión de recursos a nivel de bloques de otros lenguajes, defer
puede parecer peculiar, pero sus aplicaciones más interesantes y potentes provienen precisamente de que no es basado en bloque sino basado en funciones. En el apartado de panic
y recover
veremos otro ejemplo de sus posibilidades.
Datos
Asignación con new
Go tiene dos primitivas de asignación, las funciones integradas new
y make
. Hacen cosas diferentes y se aplican a diferentes tipos, lo que puede resultar confuso, pero las reglas son simples. Hablemos primero de new
. Es una función incorporada que asigna memoria, pero a diferencia de sus homónimos en otros lenguajes, no inicializa la memoria, solo la pone a cero. Es decir, new(T)
asigna almacenamiento puesto a cero para un nuevo elemento de tipo T
y devuelve su dirección, un valor de tipo *T
. En la terminología de Go, devuelve un puntero a un valor cero recién asignado de tipo T
.
Dado que la memoria devuelta por new
está puesta a cero, es útil disponer al diseñar tus estructuras de datos que el valor cero de cada tipo pueda usarse sin inicialización adicional. Esto significa que un usuario de la estructura de datos puede crear una con new
y ponerse manos a la obra. Por ejemplo, la documentación para bytes.Buffer
establece que "el valor cero para Buffer
es un buffer vacío listo para usar". De manera similar, sync.Mutex
no tiene un constructor explícito ni un método Init
. En cambio, el valor cero para un sync.Mutex
se define como un mutex desbloqueado.
La útil propiedad de valor cero funciona de forma transitiva. Considera este tipo de declaración.
type SyncedBuffer struct { lock sync.Mutex buffer bytes.Buffer }
Los valores de tipo SyncedBuffer
también están listos para usarse inmediatamente después de la asignación o simplemente de la declaración. En el siguiente fragmento, tanto p
como v
funcionarán correctamente sin más arreglos.
p := new(SyncedBuffer) // type *SyncedBuffer var v SyncedBuffer // type SyncedBuffer
Constructores y literales compuestos
A veces el valor cero no es lo suficientemente bueno y es necesario un constructor de inicialización, como en este ejemplo derivado del paquete os
.
func NewFile(fd int, name string) *File { if fd < 0 { return nil } f := new(File) f.fd = fd f.name = name f.dirinfo = nil f.nepipe = 0 return f }
Hay muchos textos repetitivos ahí. Podemos simplificarlo usando un literal compuesto, que es una expresión que crea una nueva instancia cada vez que se evalúa.
func NewFile(fd int, name string) *File { if fd < 0 { return nil } f := File{fd, name, nil, 0} return &f }
Ten en cuenta que, a diferencia de C, está perfectamente bien devolver la dirección de una variable local; el almacenamiento asociado con la variable sobrevive después de que la función retorna. De hecho, al tomar la dirección de un literal compuesto se asigna una nueva instancia cada vez que se evalúa, por lo que podemos combinar estas dos últimas líneas.
return &File{fd, name, nil, 0}
Los campos de un literal compuesto están dispuestos en orden y todos deben estar presentes. Sin embargo, al etiquetar los elementos explícitamente como pares field:
value, los inicializadores pueden aparecer en cualquier orden, dejando los que faltan con sus respectivos valores cero. Así podríamos decir
return &File{fd: fd, name: name}
Como caso límite, si un literal compuesto no contiene ningún campo, crea un valor cero para el tipo. Las expresiones new(File)
y &File{}
son equivalentes.
También se pueden crear literales compuestos para arrays, slices y maps, siendo las etiquetas de los campos índices o claves de mapa, según corresponda. En estos ejemplos, las inicializaciones funcionan independientemente de los valores de Enone
, Eio
y Einval
, siempre que sean distintos.
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
Asignación con make
Volviendo a la asignación. La función incorporada make(T,
args)
tiene un propósito diferente de new(T)
. Crea slices, maps y channels únicamente, y devuelve un valor de tipo inicializado (no puesto a cero) T
(no *T
). El motivo de la distinción es que estos tres tipos representan, encubiertamente, referencias a estructuras de datos que deben inicializarse antes de su uso. Un slice por ejemplo, es un descriptor de tres elementos que contiene un puntero a los datos (dentro de un array), la longitud y la capacidad, y hasta que esos elementos se inicialicen, el slice es nil
. Para slices, maps y channels, make
inicializa la estructura de datos interna y prepara el valor para su uso. Por ejemplo,
make([]int, 10, 100)
asigna un array de 100 entradas y luego crea una estructura slice con una longitud de 10 y una capacidad de 100 apuntando a los primeros 10 elementos del array. (Al crear un slice, la capacidad puede omitirse; consulta la sección sobre slices para obtener más información). Por el contrario, new([]int)
devuelve un puntero a una estructura de slices puesta a cero recién asignada, es decir, un puntero a un valor de slice nil
.
Estos ejemplos ilustran la diferencia entre new
y make
.
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints // Unnecessarily complex: var p *[]int = new([]int) *p = make([]int, 100, 100) // Idiomatic: v := make([]int, 100)
Recuerda que make
se aplica solo a maps, slices y channels y no devuelve un puntero. Para obtener un puntero explícito, asigna con new
o toma la dirección de una variable explícitamente.
Arrays
Los arrays son útiles al planificar el diseño detallado de la memoria y, a veces, pueden ayudar a evitar la asignación, pero principalmente son un bloque de construcción para los slices, el tema de la siguiente sección. Para sentar las bases de ese tema, aquí hay algunas palabras sobre arrays.
Existen grandes diferencias entre la forma en que funcionan los arrays en Go y C. En Go,
- Los arrays son valores. Asignar un array a otro copia todos los elementos.
- En particular, si pasas un array a una función, recibirá una copia del array, no un puntero a el.
-
El tamaño de un array es parte de su tipo. Los tipos
[10]int
y[20]int
son distintos.
La propiedad de valor puede ser útil pero también costosa; Si deseas un comportamiento y eficiencia similares a los de C, puedes pasar un puntero al array.
func Sum(a *[3]float64) (sum float64) { for _, v := range *a { sum += v } return } array := [...]float64{7.0, 8.5, 9.1} x := Sum(&array) // Note the explicit address-of operator
Pero incluso este estilo no es idiomático. En su lugar, usa slices.
Slices
Los slices envuelven arrays para brindar una interfaz más general, potente y conveniente a las secuencias de datos. Excepto por elementos con dimensiones explícitas, como arrays de transformación, la mayor parte de la programación de arrays en Go se realiza con slices en lugar de arrays simples.
Los slices contienen referencias a una array subyacente, y si asignas un slice a otro, ambos se refieren al mismo array. Si una función toma un argumento de slice, realiza cambios en los elementos del slice será visible el llamador de la función, de forma análoga a pasar un puntero al array subyacente. Por lo tanto, una función Read
puede aceptar un argumento de slice en lugar de un puntero y un recuento; la longitud dentro del slice establece un límite superior de la cantidad de datos que se leerán. Aquí está la firma del método Read
del tipo File
en el paquete os
:
func (f *File) Read(buf []byte) (n int, err error)
El método devuelve el número de bytes leídos y un valor de error, si corresponde. Para leer en los primeros 32 bytes de un buffer más grande buf
, slice (aquí usado como verbo) el búfer.
n, err := f.Read(buf[0:32])
Este tipo de slice es común y eficiente. De hecho, dejando de lado la eficiencia por el momento, el siguiente fragmento también leería los primeros 32 bytes del búfer.
var n int var err error for i := 0; i < 32; i++ { nbytes, e := f.Read(buf[i:i+1]) // Read one byte. n += nbytes if nbytes == 0 || e != nil { err = e break } }
La longitud de un slice puede cambiarse siempre y cuando todavía se ajuste a los límites del array subyacente; simplemente asígnalo a una porción de sí mismo. La capacidad de un slice, accesible mediante la función incorporada cap
, informa la longitud máxima que puede asumir el slice. Aquí hay una función para agregar datos a un slice. Si los datos exceden la capacidad, se reasigna el slice. Se devuelve el slice resultante. La función utiliza el hecho de que
len
y cap
son legales cuando se aplican al slice nil
y devuelven 0.
func Append(slice, data []byte) []byte { l := len(slice) if l + len(data) > cap(slice) { // reallocate // Allocate double what's needed, for future growth. newSlice := make([]byte, (l+len(data))*2) // The copy function is predeclared and works for any slice type. copy(newSlice, slice) slice = newSlice } slice = slice[0:l+len(data)] copy(slice[l:], data) return slice }
Debemos devolver el slice después porque, aunque Append
puede modificar los elementos de slice
, el slice en sí (la estructura de datos que contiene el puntero, la longitud y la capacidad) se pasa por valor.
La idea de añadir a un slice es tan útil que se captura mediante la función incorporada append
. Sin embargo, para comprender el diseño de esa función necesitamos un poco más de información, por lo que volveremos a ello más adelante.
Slices bidimensionales
Los arrays y slices de Go son unidimensionales. Para crear el equivalente de un array o slice 2D, es necesario definir un array de arrays o un slice de slices, como este:
type Transform [3][3]float64 // A 3x3 array, really an array of arrays. type LinesOfText [][]byte // A slice of byte slices.
Debido a que los slices tienen una longitud variable, es posible que cada slice interno tenga una longitud diferente. Esa puede ser una situación común, como en nuestro ejemplo LinesOfText
: cada línea tiene una longitud independiente.
text := LinesOfText{ []byte("Now is the time"), []byte("for all good gophers"), []byte("to bring some fun to the party."), }
A veces es necesario asignar un slice 2D, una situación que puede surgir al procesar líneas de escaneo de píxeles, por ejemplo. Hay dos formas de lograr esto. Una es asignar cada slice de forma independiente; la otra es asignar un único array y apuntar los slices individuales a el. Cuál usar depende de tu aplicación. Si los slices pueden crecer o reducirse, deben asignarse de forma independiente para evitar sobrescribir la siguiente línea; si no, puede ser más eficiente construir el objeto con una sola asignación. Como referencia, aquí hay bocetos de los dos métodos. Primero, una línea a la vez:
// Allocate the top-level slice. picture := make([][]uint8, YSize) // One row per unit of y. // Loop over the rows, allocating the slice for each row. for i := range picture { picture[i] = make([]uint8, XSize) }
Y ahora como una asignación, dividida en líneas:
// Allocate the top-level slice, the same as before. picture := make([][]uint8, YSize) // One row per unit of y. // Allocate one large slice to hold all the pixels. pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8. // Loop over the rows, slicing each row from the front of the remaining pixels slice. for i := range picture { picture[i], pixels = pixels[:XSize], pixels[XSize:] }
Maps
Los maps son una estructura de datos incorporada conveniente y poderosa que asocia valores de un tipo (la clave) con valores de otro tipo (el elemento o valor). La clave puede ser de cualquier tipo para el cual esté definido el operador de igualdad, como números enteros, números de coma flotante y complejos, cadenas, punteros, interfaces (siempre que el tipo dinámico admita igualdad), estructuras y arrays. Los slices no se pueden utilizar como claves de map, porque la igualdad no está definida en ellos. Al igual que los slices, los maps contienen referencias a una estructura de datos subyacente. Si pasas un map a una función que cambia el contenido del map, los cambios serán visibles para el llamador.
Los maps se pueden construir usando la sintaxis literal compuesta habitual con pares clave-valor separados por dos puntos, por lo que es fácil construirlos durante la inicialización.
var timeZone = map[string]int{ "UTC": 0*60*60, "EST": -5*60*60, "CST": -6*60*60, "MST": -7*60*60, "PST": -8*60*60, }
Asignar y recuperar valores de maps se ve sintácticamente como hacer lo mismo con arrays y slices, excepto que no es necesario que el índice sea un número entero.
offset := timeZone["EST"]
Un intento de obtener un valor de map con una clave que no está presente en el map devolverá el valor cero para el tipo de entradas en el map. Por ejemplo, si el map contiene números enteros, al buscar una clave inexistente se devolverá 0
. Un set se puede implementar como un map con el tipo de valor bool
. Establece la entrada del map a true
para poner el valor en el set y luego probarlo mediante una simple indexación.
attended := map[string]bool{ "Ann": true, "Joe": true, ... } if attended[person] { // will be false if person is not in the map fmt.Println(person, "was at the meeting") }
A veces necesitas distinguir una entrada faltante de un valor cero. ¿Hay una entrada para "UTC"
o es 0 porque no está en el map? Puedes discriminar con una forma de asignación múltiple.
var seconds int var ok bool seconds, ok = timeZone[tz]
Por razones obvias, esto se llama el modismo "coma ok". En este ejemplo, si tz
está presente, seconds
se configurará apropiadamente y ok
será verdadero; de lo contrario, seconds
se establecerá en cero y ok
será falso. Aquí hay una función que lo combina con un bonito informe de errores:
func offset(tz string) int { if seconds, ok := timeZone[tz]; ok { return seconds } log.Println("unknown time zone:", tz) return 0 }
Para probar la presencia en el map sin preocuparte por el valor real, puedes usar el identificador en blanco (_
) en lugar de la variable habitual para el valor.
_, present := timeZone[tz]
Para eliminar una entrada del map, usa la función incorporada delete
, cuyos argumentos son el map y la clave que se eliminará. Es seguro hacer esto, incluso si la llave ya no está en el map.
delete(timeZone, "PDT") // Now on Standard Time
Impresión
La impresión formateada en Go usa un estilo similar a la familia printf
de C, pero es más rica y más general. Las funciones se encuentran en el paquete fmt
y tienen nombres en mayúscula: fmt.Printf
, fmt.Fprintf
, fmt.Sprintf
y así sucesivamente. Las funciones de cadena (Sprintf
, etc.) devuelven una cadena en lugar de completar un búfer proporcionado.
No necesitas proporcionar una cadena de formato. Para cada una de Printf
, Fprintf
y Sprintf
hay otro par de funciones, por ejemplo Print
y Println
. Estas funciones no toman una cadena de formato sino que generan un formato predeterminado para cada argumento. Las versiones Println
también insertan un espacio en blanco entre los argumentos y agregan una nueva línea a la salida, mientras que las versiones Print
agregan espacios en blanco solo si el operando en ninguno de los lados es una cadena. En este ejemplo, cada línea produce el mismo resultado.
fmt.Printf("Hello %d\n", 23) fmt.Fprint(os.Stdout, "Hello ", 23, "\n") fmt.Println("Hello", 23) fmt.Println(fmt.Sprint("Hello ", 23))
Las funciones de impresión formateadas fmt.Fprint
y amigos toman como primer argumento cualquier objeto que implemente la interfaz io.Writer
; las variables os.Stdout
y os.Stderr
son ejemplos familiares.
Aquí las cosas comienzan a diferir de C. Primero, los formatos numéricos como %d
no aceptan indicadores de signo o tamaño; en cambio, las rutinas de impresión utilizan el tipo de argumento para decidir estas propiedades.
var x uint64 = 1<<64 - 1 fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
imprime
18446744073709551615 ffffffffffffffff; -1 -1
Si sólo quieres la conversión predeterminada, como decimal para números enteros, puedes usar el formato general %v
(para “valor”); el resultado es exactamente lo que producirían Print
y Println
. Además, ese formato puede imprimir cualquier valor, incluso arrays, slices, estructuras y maps. Aquí hay una declaración impresa para el map de zona horaria definido en la sección anterior.
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
lo que da como resultado:
map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
Para maps, Printf
y sus amigos ordenan la salida lexicográficamente por clave.
Al imprimir una estructura, el formato modificado %+v
anota los campos de la estructura con sus nombres, y para cualquier valor el formato alternativo %#v
imprime el valor en la sintaxis completa de Go.
type T struct { a int b float64 c string } t := &T{ 7, -2.35, "abc\tdef" } fmt.Printf("%v\n", t) fmt.Printf("%+v\n", t) fmt.Printf("%#v\n", t) fmt.Printf("%#v\n", timeZone)
imprime
&{7 -2.35 abc def} &{a:7 b:-2.35 c:abc def} &main.T{a:7, b:-2.35, c:"abc\tdef"} map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
(Ten en cuenta los símbolos). Ese formato de cadena entre comillas también está disponible a través de %q
cuando se aplica a un valor de tipo string
o []byte
. El formato alternativo %#q
utilizará comillas inversas si es posible. (El formato %q
también se aplica a números enteros y runas, produciendo una constante rúnica entre comillas simples.) Además, %x
funciona en cadenas, arrays de bytes y porciones de bytes, así como en números enteros, generando una cadena hexadecimal larga y con un espacio en el formato (% x
) coloca espacios entre los bytes.
Otro formato útil es %T
, que imprime el tipo de un valor.
fmt.Printf("%T\n", timeZone)
imprime
map[string]int
Si quieres controlar el formato predeterminado para un tipo personalizado, todo lo que necesitas es definir un método con la firma String() string
en el tipo. Para nuestro tipo simple T
, podría verse así.
func (t *T) String() string { return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c) } fmt.Printf("%v\n", t)
imprimir en el formato
7/-2.35/"abc\tdef"
(Si necesitas imprimir valores de tipo T
así como punteros a T
, el receptor de String
debe ser de tipo valor; en este ejemplo se utilizó un puntero porque es más eficiente e idiomático para tipos de estructuras. Consulta la sección siguiente sobre punteros frente a receptores de valores. para obtener más información.)
Nuestro método String
puede llamar a Sprintf
porque las rutinas de impresión son completamente reentrantes y se pueden empaquetar de esta manera. Hay una, sin embargo, un importante detalle a comprender sobre este enfoque: no construyas un método String
llamando a
Sprintf
de una manera que se repetirá en tu método String
indefinidamente. Esto puede suceder si la llamada a Sprintf
intenta imprimir el receptor directamente como una cadena, lo que a su vez invocará el método nuevamente. Es un error común y fácil de cometer, como muestra este ejemplo.
type MyString string func (m MyString) String() string { return fmt.Sprintf("MyString=%s", m) // Error: will recur forever. }
También es fácil de arreglar: convierte el argumento al tipo de cadena básico, que no tiene el método.
type MyString string func (m MyString) String() string { return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion. }
En la sección de inicialización veremos otra técnica que evita esta recursividad.
Otra técnica de impresión es pasar los argumentos de una rutina de impresión directamente a otra rutina similar. La firma de Printf
usa el tipo ...interface{}
para que su argumento final especifique que un número arbitrario de parámetros (de tipo arbitrario) puede aparecer después del formato.
func Printf(format string, v ...interface{}) (n int, err error) {
Dentro de la función Printf
, v
actúa como una variable de tipo []interface{}
pero si se pasa a otra función variada, actúa como una lista normal de argumentos. Aquí está la implementación de la función log.Println
que usamos anteriormente. Pasa sus argumentos directamente a fmt.Sprintln
para el formato real.
// Println prints to the standard logger in the manner of fmt.Println. func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string) }
Escribimos ...
después de v
en la llamada anidada a Sprintln
para decirle al compilador que trate v
como una lista de argumentos; de lo contrario, simplemente pasaría v
como argumento de un solo slice.
La impresión implica aún más de lo que hemos cubierto aquí. Consulta la documentación godoc
para el paquete fmt
para obtener más detalles.
Por cierto, un parámetro ...
puede ser de un tipo específico, por ejemplo ...int
para una función min que elige el menor de una lista de números enteros:
func Min(a ...int) int { min := int(^uint(0) >> 1) // largest int for _, i := range a { if i < min { min = i } } return min }
Append
Ahora tenemos la pieza que faltaba y que necesitábamos para explicar el diseño de la función incorporada append
. La firma de append
es diferente de nuestra función personalizada Append
anterior. Esquemáticamente, es así:
func append(slice []T, elements ...T) []T
donde T es un marcador de posición para cualquier tipo determinado. En realidad, no se puede escribir una función en Go donde el tipo T
lo determina el llamador. Es por eso que append
está integrado: necesita soporte del compilador.
Lo que hace append
es agregar los elementos al final del slice y devolver el resultado. Es necesario devolver el resultado porque, al igual que con nuestro Append
escrito a mano, el array subyacente puede cambiar. Este sencillo ejemplo
x := []int{1,2,3} x = append(x, 4, 5, 6) fmt.Println(x)
imprime [1 2 3 4 5 6]
. Así que append
funciona un poco como Printf
, recopilando un número arbitrario de argumentos.
Pero ¿qué pasaría si quisiéramos hacer lo que hace nuestro Append
y agregar un slice a otro? Fácil: usa ...
en el sitio de llamada, tal como lo hicimos en la llamada a Output
anterior. Este fragmento produce un resultado idéntico al anterior.
x := []int{1,2,3} y := []int{4,5,6} x = append(x, y...) fmt.Println(x)
Sin ese ...
, no se compilaría porque los tipos estarían equivocados; y
no es del tipo int
.
Inicialización
Aunque superficialmente no parece muy diferente de la inicialización en C o C++, la inicialización en Go es más poderosa. Se pueden construir estructuras complejas durante la inicialización y los problemas de orden entre los objetos inicializados, incluso entre diferentes paquetes, se manejan correctamente.
Constantes
Las constantes en Go son solo eso: constantes. Se crean en tiempo de compilación, incluso cuando se definen como locales en funciones, y solo pueden ser números, caracteres (runas), cadenas o valores booleanos. Debido a la restricción del tiempo de compilación, las expresiones que los definen deben ser expresiones constantes, evaluables por el compilador. Por ejemplo, 1<<3
es una expresión constante, mientras que math.Sin(math.Pi/4)
no lo es porque la función llama a math.Sin
debe ocurrir en tiempo de ejecución.
En Go, las constantes enumeradas se crean usando el enumerador iota
. Dado que iota
puede ser parte de una expresión y las expresiones pueden repetirse implícitamente, es fácil crear complejos conjuntos de valores.
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
La capacidad de adjuntar un método como String
a cualquier tipo definido por el usuario hace posible que valores arbitrarios se formatee automáticamente para imprimir. Aunque ya verás se aplica con mayor frecuencia a estructuras, esta técnica también es útil para tipos escalares como tipos de punto flotante como ByteSize
.
func (b ByteSize) String() string { switch { case b >= YB: return fmt.Sprintf("%.2fYB", b/YB) case b >= ZB: return fmt.Sprintf("%.2fZB", b/ZB) case b >= EB: return fmt.Sprintf("%.2fEB", b/EB) case b >= PB: return fmt.Sprintf("%.2fPB", b/PB) case b >= TB: return fmt.Sprintf("%.2fTB", b/TB) case b >= GB: return fmt.Sprintf("%.2fGB", b/GB) case b >= MB: return fmt.Sprintf("%.2fMB", b/MB) case b >= KB: return fmt.Sprintf("%.2fKB", b/KB) } return fmt.Sprintf("%.2fB", b) }
La expresión YB
se imprime como 1.00YB
, mientras que ByteSize(1e13)
se imprime como 9.09TB
.
El uso aquí de Sprintf
para implementar el método ByteSize
de String
es seguro (evita la recurrencia indefinidamente) no debido a una conversión sino porque llama a Sprintf
con %f
, que no es un formato de cadena: Sprintf
solo llamará al String
cuando quiere una cadena, y %f
cuando quiere un valor de punto flotante.
Variables
Las variables se pueden inicializar como constantes, pero el inicializador puede ser una expresión general calculada en tiempo de ejecución.
var ( home = os.Getenv("HOME") user = os.Getenv("USER") gopath = os.Getenv("GOPATH") )
La función init
Finalmente, cada archivo fuente puede definir su propia función init
para configurar cualquier estado que sea necesario. (En realidad, cada archivo puede tener múltiples funciones init
). Y significa finalmente: init
se llama después de que todas las declaraciones de variables en el paquete hayan evaluado sus inicializadores, y estos se evalúan sólo después de que se hayan inicializado todos los paquetes importados.
Además de las inicializaciones que no se pueden expresar como declaraciones, un uso común de las funciones init
es verificar o reparar la corrección del estado del programa antes de que comience la ejecución real.
func init() { if user == "" { log.Fatal("$USER not set") } if home == "" { home = "/home/" + user } if gopath == "" { gopath = home + "/go" } // gopath may be overridden by --gopath flag on command line. flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH") }
Métodos
Punteros vs. Valores
Como vimos con ByteSize
, los métodos se pueden definir para cualquier tipo con nombre (excepto un puntero o una interfaz); el receptor no tiene que ser una estructura.
En la discusión anterior sobre los slices, escribimos una función Append
. En su lugar, podemos definirlo como un método en slices. Para hacer esto, primero declaramos un tipo con nombre al que podemos vincular el método y luego hacemos que el receptor del método sea un valor de ese tipo.
type ByteSlice []byte func (slice ByteSlice) Append(data []byte) []byte { // Body exactly the same as the Append function defined above. }
Esto aún requiere que el método devuelva el slice actualizado. Podemos eliminar esa torpeza redefiniendo el método para tomar un puntero a un ByteSlice
como su receptor, de modo que el método pueda sobrescribir el slice de el llamador.
func (p *ByteSlice) Append(data []byte) { slice := *p // Body as above, without the return. *p = slice }
De hecho, podemos hacerlo aún mejor. Si modificamos nuestra función para que parezca un método estándar Write
, como este,
func (p *ByteSlice) Write(data []byte) (n int, err error) { slice := *p // Again as above. *p = slice return len(data), nil }
entonces el tipo *ByteSlice
satisface la interfaz estándar io.Writer
, lo cual es útil. Por ejemplo, podemos imprimir en uno.
var b ByteSlice fmt.Fprintf(&b, "This hour has %d days\n", 7)
Pasamos la dirección de un ByteSlice
porque solo *ByteSlice
satisface io.Writer
. La regla sobre punteros versus valores para receptores es que los métodos de valor se pueden invocar en punteros y valores, pero los métodos de puntero solo se pueden invocar en punteros.
Esta regla surge porque los métodos de puntero pueden modificar el receptor; invocarlos en un valor haría que el método recibiera una copia del valor, por lo que cualquier modificación se descartaría. Por lo tanto, el lenguaje no permite este error. Sin embargo, existe una útil excepción. Cuando el valor es direccionable, el lenguaje se encarga del caso común de invocar un método de puntero en un valor insertando el operador de dirección automáticamente. En nuestro ejemplo, la variable b
es direccionable, por lo que podemos llamar a su método Write
con solo b.Write
. El compilador lo reescribirá en (&b).Write
por nosotros.
Por cierto, la idea de usar Write
en una porción de bytes es fundamental para la implementación de bytes.Buffer
.
Interfaces y otros tipos
Interfaces
Las interfaces en Go proporcionan una forma de especificar el comportamiento de un objeto: si algo puede hacer esto, entonces se puede usar aquí. Ya hemos visto un par de ejemplos sencillos; los printers personalizados se pueden implementar mediante un método String
, mientras que Fprintf
puede generar resultados para cualquier cosa con un método Write
. Las interfaces con solo uno o dos métodos son comunes en el código Go y generalmente reciben un nombre derivado del método, como io.Writer
para algo que implementa Write
.
Un tipo puede implementar múltiples interfaces. Por ejemplo, una colección se puede ordenar mediante las rutinas del paquete sort
si implementa sort.Interface
, que contiene Len()
, Less(i, j int) bool
y Swap(i, j int)
, y también podría tener un formateador personalizado. En este ejemplo artificial, Sequence
satisface ambos.
type Sequence []int // Methods required by sort.Interface. func (s Sequence) Len() int { return len(s) } func (s Sequence) Less(i, j int) bool { return s[i] < s[j] } func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Copy returns a copy of the Sequence. func (s Sequence) Copy() Sequence { copy := make(Sequence, 0, len(s)) return append(copy, s...) } // Method for printing - sorts the elements before printing. func (s Sequence) String() string { s = s.Copy() // Make a copy; don't overwrite argument. sort.Sort(s) str := "[" for i, elem := range s { // Loop is O(N²); will fix that in next example. if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" }
Conversiones
El método String
de Sequence
está recreando el trabajo que Sprint
ya hace para los slices. (También tiene complejidad O(N²), lo cual es pobre). Podemos compartir el esfuerzo (y también acelerarlo) si convertimos la Sequence
a un simple []int
antes de llamar a Sprint
.
func (s Sequence) String() string { s = s.Copy() sort.Sort(s) return fmt.Sprint([]int(s)) }
Este método es otro ejemplo de la técnica de conversión para llamar a Sprintf
de forma segura desde un método String
. Debido a que los dos tipos (Sequence
y []int
) son iguales si ignoramos el nombre del tipo, es legal realizar conversiones entre ellos. La conversión no crea un valor nuevo, solo actúa temporalmente como si el valor existente tuviera un tipo nuevo. (Existen otras conversiones legales, como de entero a punto flotante, que crean un nuevo valor).
Es un modismo en los programas Go para convertir el tipo de una expresión para acceder a un conjunto diferente de métodos. Como ejemplo, podríamos usar el tipo existente sort.IntSlice
para reducir el ejemplo completo a esto:
type Sequence []int // Method for printing - sorts the elements before printing func (s Sequence) String() string { s = s.Copy() sort.IntSlice(s).Sort() return fmt.Sprint([]int(s)) }
Ahora, en lugar de que Sequence
implemente múltiples interfaces (ordenamiento e impresión), estamos usando la capacidad de un elemento de datos para convertirse en múltiples tipos. (Sequence
, sort.IntSlice
y []int
), cada uno de los cuales hace una parte del trabajo. Esto es más inusual en la práctica, pero puede ser efectivo.
Conversiones de interfaz y assertions de tipo
Switches de tipo son una forma de conversión: toman una interfaz y, para cada caso en el switch, en cierto sentido convierten al tipo de ese caso. Aquí hay una versión simplificada de cómo el código en fmt.Printf
convierte un valor en un string usando un interruptor de tipo. Si ya es una cadena, queremos el valor de cadena real mantenido por la interfaz, mientras que si tiene un método String
queremos el resultado de llamar al método.
type Stringer interface { String() string } var value interface{} // Value provided by caller. switch str := value.(type) { case string: return str case Stringer: return str.String() }
El primer caso encuentra un valor concreto; el segundo convierte la interfaz en otra interfaz. Está perfectamente bien mezclar tipos de esta manera.
¿Qué pasa si solo hay un tipo que nos interesa? ¿Si sabemos que el valor contiene un string
y solo queremos extraerlo? Un cambio de tipo de un caso sería suficiente, pero también lo sería una aserción de tipo. Una aserción de tipo toma un valor de interfaz y extrae de él un valor del tipo explícito especificado. La sintaxis se basa en la cláusula que abre un cambio de tipo, pero con un tipo explícito en lugar de la palabra clave type
:
value.(typeName)
y el resultado es un nuevo valor con el tipo estático typeName
. Ese tipo debe ser el tipo concreto que contiene la interfaz o un segundo tipo de interfaz al que se puede convertir el valor. Para extraer la cadena que sabemos que está en el valor, podríamos escribir:
str := value.(string)
Pero si resulta que el valor no contiene una cadena, el programa fallará con un error de tiempo de ejecución. Para protegerse contra eso, usa el modismo "coma, ok" para probar, de manera segura, si el valor es una cadena:
str, ok := value.(string) if ok { fmt.Printf("string value is: %q\n", str) } else { fmt.Printf("value is not a string\n") }
Si la aserción de tipo falla, str
seguirá existiendo y será de tipo cadena, pero tendrá el valor cero, una cadena vacía.
Como ilustración de la capacidad, aquí tienes una declaración if
-else
que es equivalente al cambio de tipo que abrió esta sección.
if str, ok := value.(string); ok { return str } else if str, ok := value.(Stringer); ok { return str.String() }
Generalidad
Si un tipo existe solo para implementar una interfaz y nunca habrá exportado métodos más allá de esa interfaz, no hay necesidad de exportar el tipo en sí. Exportar solo la interfaz deja claro que el valor no tiene un comportamiento interesante más allá de lo que se describe en la interfaz. También evita la necesidad de repetir la documentación en cada instancia de un método común.
En tales casos, el constructor debería devolver un valor de interfaz en lugar del tipo de implementación. Como ejemplo, en las bibliotecas hash, tanto crc32.NewIEEE
como adler32.New
devuelven el tipo de interfaz hash.Hash32
. Sustituir el algoritmo CRC-32 por Adler-32 en un programa Go solo requiere cambiar la llamada al constructor; el resto del código no se ve afectado por el cambio de algoritmo.
Un enfoque similar permite que los algoritmos de cifrado de transmisión en los diversos paquetes crypto
se separen de los cifrados de bloque que encadenan. La interfaz Block
en el paquete crypto/cipher
especifica el comportamiento de un cifrado de bloque, que proporciona cifrado de un único bloque de datos. Luego, por analogía con el paquete bufio
, los paquetes de cifrado que implementan esta interfaz se pueden usar para construir cifrados de transmisión, representados por la interfaz Stream
, sin conocer los detalles del cifrado del bloque.
Las interfaces crypto/cipher
se ven así:
type Block interface { BlockSize() int Encrypt(dst, src []byte) Decrypt(dst, src []byte) } type Stream interface { XORKeyStream(dst, src []byte) }
Aquí está la definición de flujo en modo contador (CTR), que convierte un cifrado de bloque en un cifrado de flujo; observa que los detalles del cifrado de bloque están abstraídos:
// NewCTR returns a Stream that encrypts/decrypts using the given Block in // counter mode. The length of iv must be the same as the Block's block size. func NewCTR(block Block, iv []byte) Stream
NewCTR
se aplica no solo a un algoritmo de cifrado y fuente de datos específicos, sino a cualquier implementación de la interfaz Block
y cualquier Stream
. Debido a que devuelven valores de interfaz, reemplazar el cifrado CTR con otros modos de cifrado es un cambio localizado. Las llamadas al constructor deben editarse, pero debido a que el código circundante debe tratar el resultado solo como un Stream
, no notarás la diferencia.
Interfaces y métodos
Dado que casi cualquier cosa puede tener métodos adjuntos, casi cualquier cosa puede satisfacer una interfaz. Un ejemplo ilustrativo se encuentra en el paquete http
, que define la interfaz Handler
. Cualquier objeto que implemente Handler
puede atender solicitudes HTTP.
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ResponseWriter
es en sí mismo una interfaz que proporciona acceso a los métodos necesarios para devolver la respuesta al cliente. Esos métodos incluyen el método estándar Write
, por lo que se puede usar un http.ResponseWriter
siempre que se pueda usar un io.Writer
. Request
es una estructura que contiene una representación analizada de la solicitud del cliente.
Por brevedad, ignoremos los POST y supongamos que las solicitudes HTTP son siempre GET; esa simplificación no afecta la forma en que se configuran los manejadores. Aquí hay una implementación trivial de un controlador (handler) para contar la cantidad de veces que se visita la página.
// Simple counter server. type Counter struct { n int } func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctr.n++ fmt.Fprintf(w, "counter = %d\n", ctr.n) }
(Siguiendo con nuestro tema, observa cómo Fprintf
puede imprimir en un http.ResponseWriter
.) En un servidor real, acceder a ctr.n
necesitaría protección contra el acceso concurrente. Consulta los paquetes sync
y atomic
para obtener sugerencias.
Como referencia, aquí se explica cómo conectar dicho servidor a un nodo en el árbol de URL.
import "net/http" ... ctr := new(Counter) http.Handle("/counter", ctr)
Pero ¿por qué hacer de Counter
una estructura? Un número entero es todo lo que se necesita. (El receptor debe ser un puntero para que el incremento sea visible para el llamador).
// Simpler counter server. type Counter int func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { *ctr++ fmt.Fprintf(w, "counter = %d\n", *ctr) }
¿Qué pasa si tu programa tiene algún estado interno que necesita ser notificado de que se ha visitado una página? Vincular un channel a la página web.
// A channel that sends a notification on each visit. // (Probably want the channel to be buffered.) type Chan chan *http.Request func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) { ch <- req fmt.Fprint(w, "notification sent") }
Finalmente, digamos que queremos presentar en /args
los argumentos utilizados al invocar el binario del servidor. Es fácil escribir una función para imprimir los argumentos.
func ArgServer() { fmt.Println(os.Args) }
¿Cómo convertimos eso en un servidor HTTP? Podríamos hacer de ArgServer
un método de algún tipo cuyo valor ignoremos, pero hay una forma más sencilla. Como podemos definir un método para cualquier tipo excepto punteros e interfaces, podemos escribir un método para una función. El paquete http
contiene este código:
// The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler object that calls f. type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, req). func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) { f(w, req) }
HandlerFunc
es un tipo con un método, ServeHTTP
, por lo que los valores de ese tipo pueden atender solicitudes HTTP. Observa la implementación del método: el receptor es una función, f
, y el método llama a f
. Esto puede parecer extraño, pero no es tan diferente de, digamos, que el receptor sea un channel y el método de envío en el channel.
Para convertir ArgServer
en un servidor HTTP, primero lo modificamos para que tenga la firma correcta.
// Argument server. func ArgServer(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, os.Args) }
ArgServer
ahora tiene la misma firma que HandlerFunc
, por lo que se puede convertir a ese tipo para acceder a sus métodos, tal como convertimos Sequence
a IntSlice
para acceder a IntSlice.Sort
. El código para configurarlo es conciso:
http.Handle("/args", http.HandlerFunc(ArgServer))
Cuando alguien visita la página /args
, el controlador instalado en esa página tiene el valor ArgServer
y tipo HandlerFunc
. El servidor HTTP invocará el método ServeHTTP
de ese tipo, con ArgServer
como receptor, que a su vez llamará a ArgServer
(mediante la invocación f(w, req)
dentro de HandlerFunc.ServeHTTP
). A continuación se mostrarán los argumentos.
En esta sección hemos creado un servidor HTTP a partir de una estructura, un número entero, un channel y una función, todo porque las interfaces son solo conjuntos de métodos, que se pueden definir para (casi) cualquier tipo.
El identificador en blanco
Hemos mencionado el identificador en blanco un par de veces, en el contexto de for
range
(bucles) y maps. El identificador en blanco se puede asignar o declarar con cualquier valor de cualquier tipo y el valor se descarta sin causar daño. Es un poco como escribir en el archivo /dev/null
de Unix: representa un valor de sólo escritura que se utilizará como marcador de posición donde se necesita una variable pero el valor real es irrelevante. Tiene usos más allá de los que ya hemos visto.
El identificador en blanco en asignación múltiple
El uso de un identificador en blanco en un bucle for
range
es un caso especial de una situación general: asignación múltiple.
Si una tarea requiere múltiples valores en el lado izquierdo, pero uno de los valores no será utilizado por el programa, un identificador en blanco en el lado izquierdo de la tarea evita la necesidad de crea una variable ficticia y deja claro que el valor debe descartarse. Por ejemplo, cuando llame a una función que devuelve un valor y un error, pero sólo el error es importante, utilice el identificador en blanco para descartar el valor irrelevante.
if _, err := os.Stat(path); os.IsNotExist(err) { fmt.Printf("%s does not exist\n", path) }
Ocasionalmente verás un código que descarta el valor del error para ignorar el error; Esta es una práctica terrible. Siempre verifique las devoluciones de errores; se proporcionan por una razón.
// Bad! This code will crash if path does not exist. fi, _ := os.Stat(path) if fi.IsDir() { fmt.Printf("%s is a directory\n", path) }
Importaciones y variables no utilizadas
Es un error importar un paquete o declarar una variable sin usarla. Las importaciones no utilizadas sobrecargan el programa y ralentizan la compilación, mientras que una variable que se inicializa pero no se utiliza es al menos un cálculo desperdiciado y tal vez indicativo de un error mayor. Sin embargo, cuando un programa está en desarrollo activo, a menudo surgen importaciones y variables no utilizadas y puede resultar molesto eliminarlas sólo para que continúe la compilación, sólo para volver a necesitarlas más adelante. El identificador en blanco proporciona una solución alternativa.
Este programa a medio escribir tiene dos importaciones no utilizadas (fmt
y io
) y una variable no utilizada (fd
), por lo que no se compilará, pero sería bueno ver si el código hasta ahora es correcto.
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
}
Para silenciar las quejas sobre las importaciones no utilizadas, utiliza un identificador en blanco para hacer referencia a un símbolo del paquete importado. De manera similar, asignar la variable no utilizada fd
al identificador en blanco silenciará el error de la variable no utilizada. Esta versión del programa se compila.
package main import ( "fmt" "io" "log" "os" ) var _ = fmt.Printf // For debugging; delete when done. var _ io.Reader // For debugging; delete when done. func main() { fd, err := os.Open("test.go") if err != nil { log.Fatal(err) } // TODO: use fd. _ = fd }
Por convención, las declaraciones globales para silenciar los errores de importación deben aparecer justo después de las importaciones y comentarse, tanto para que sean fáciles de encontrar como como recordatorio para limpiar las cosas más tarde.
Importar para efectos secundarios
Una importación no utilizada como fmt
o io
en el ejemplo anterior eventualmente debería usarse o eliminarse: las asignaciones en blanco identifican el código como un trabajo en progreso. Pero a veces es útil importar un paquete sólo por sus efectos secundarios, sin ningún uso explícito. Por ejemplo, durante su función init
, el paquete net/http/pprof ⬀
registra handlers HTTP que proporcionan información de depuración. Tiene una API exportada, pero la mayoría de los clientes solo necesitan registrarse como administrador y acceder a los datos a través de una página web. Para importar el paquete solo por sus efectos secundarios, cambia el nombre del paquete al identificador en blanco:
import _ "net/http/pprof"
Esta forma de importación deja claro que el paquete se está importando por sus efectos secundarios, porque no hay otro uso posible del paquete: en este archivo, no tiene nombre. (Si así fuera y no usáramos ese nombre, el compilador rechazaría el programa).
Comprobaciones de interfaz
Como vimos en la discusión anterior sobre interfaces, un tipo no necesita declarar explícitamente que implementa una interfaz. En cambio, un tipo implementa la interfaz simplemente implementando los métodos de la interfaz. En la práctica, la mayoría de las conversiones de interfaz son estáticas y, por lo tanto, se verifican en el momento de la compilación. Por ejemplo, pasar un *os.File
a una función que espera un io.Reader
no se compilará a menos que *os.File
implemente el io.Reader
.
Sin embargo, algunas comprobaciones de la interfaz se realizan en tiempo de ejecución. Un ejemplo está en el paquete encoding/json ⬀
, que define una interfaz Marshaler ⬀
. Cuando el codificador JSON recibe un valor que implementa esa interfaz, el codificador invoca el método de cálculo del valor para convertirlo a JSON en lugar de realizar la conversión estándar. El codificador comprueba esta propiedad en tiempo de ejecución con una aserción de tipo como:
m, ok := val.(json.Marshaler)
Si solo es necesario preguntar si un tipo implementa una interfaz, sin usar realmente la interfaz en sí, tal vez como parte de una verificación de errores, usa el identificador en blanco para ignorar el valor afirmado por el tipo:
if _, ok := val.(json.Marshaler); ok { fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val) }
Un lugar donde surge esta situación es cuando es necesario garantizar dentro del paquete que implementa el tipo que realmente satisface la interfaz. Si un tipo (por ejemplo, json.RawMessage ⬀
) necesita una representación JSON personalizada, debe implementar json.Marshaler
, pero no hay conversiones estáticas que hagan que el compilador verifique esto automáticamente. Si el tipo inadvertidamente no satisface la interfaz, el codificador JSON seguirá funcionando, pero no utilizará la implementación personalizada. Para garantizar que la implementación sea correcta, se puede utilizar en el paquete una declaración global utilizando el identificador en blanco:
var _ json.Marshaler = (*RawMessage)(nil)
En esta declaración, la asignación que implica una conversión de un *RawMessage
a un Marshaler
requiere que *RawMessage
implemente Marshaler
y esa propiedad se verificará en el momento de la compilación. Si la interfaz json.Marshaler
cambia, este paquete ya no se compilará y se nos avisará que debe actualizarse.
La aparición del identificador en blanco en esta construcción indica que la declaración existe solo para la verificación de tipos, no para crear una variable. Sin embargo, no hagas esto para cada tipo que satisfaga una interfaz. Por convención, dichas declaraciones solo se utilizan cuando no hay conversiones estáticas presentes en el código, lo cual es un evento poco común.
Embedding
Go no proporciona la noción típica de subclases basada en tipos, pero tiene la capacidad de “tomar prestadas” partes de una implementación mediante la incrustación de tipos dentro de una estructura o interfaz.
La incrustación de interfaces es muy simple. Hemos mencionado las interfaces io.Reader
y io.Writer
antes; aquí están sus definiciones.
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }
El paquete io
también exporta varias otras interfaces que especifican objetos que pueden implementar varios de estos métodos. Por ejemplo, existe io.ReadWriter
, una interfaz que contiene tanto Read
como Write
. Podríamos especificar io.ReadWriter
enumerando los dos métodos explícitamente, pero es más fácil y evocador incrustar las dos interfaces para formar una nueva, así:
// ReadWriter is the interface that combines the Reader and Writer interfaces. type ReadWriter interface { Reader Writer }
Esto dice exactamente lo que parece: Un ReadWriter
puede hacer lo que un Reader
hace y qué hace un Writer
; es una unión de las interfaces integradas. Sólo se pueden incrustar interfaces dentro de interfaces.
La misma idea básica se aplica a las estructuras, pero con implicaciones de mayor alcance. El paquete bufio
tiene dos tipos de estructuras, bufio.Reader
y bufio.Writer
, cada una de las cuales, por supuesto, implementa las interfaces análogas del paquete io
. Y bufio
también implementa un lector/escritor almacenado en búfer, lo que hace combinando un lector y un escritor en una estructura mediante incrustación: enumera los tipos dentro de la estructura pero no les da nombres de campo.
// ReadWriter stores pointers to a Reader and a Writer. // It implements io.ReadWriter. type ReadWriter struct { *Reader // *bufio.Reader *Writer // *bufio.Writer }
Los elementos incrustados son punteros a estructuras y, por supuesto, deben inicializarse para que apunten a estructuras válidas antes de que puedan usarse. La estructura ReadWriter
podría escribirse como
type ReadWriter struct { reader *Reader writer *Writer }
pero luego para promover los métodos de los campos y satisfacer las interfaces io
, también necesitaríamos proporcionar métodos de reenvío, como este:
func (rw *ReadWriter) Read(p []byte) (n int, err error) { return rw.reader.Read(p) }
Al incrustar las estructuras directamente, evitamos esta contabilidad. Los métodos de tipos incrustados vienen gratis, lo que significa que bufio.ReadWriter
no solo tiene los métodos de bufio.Reader
y bufio.Writer
, también satisface las tres interfaces: io.Reader
, io.Writer
y io.ReadWriter
.
Hay una forma importante en la que la incrustación se diferencia de la subclasificación. Cuando incrustamos un tipo, los métodos de ese tipo se convierten en métodos del tipo externo, pero cuando se invocan, el receptor del método es el tipo interno, no el externo. En nuestro ejemplo, cuando se invoca el método Read
de un bufio.ReadWriter
, tiene exactamente el mismo efecto que el método de reenvío escrito anteriormente; el receptor es el campo reader
del ReadWriter
, no el ReadWriter
en sí.
La incrustación también puede ser una simple conveniencia. Este ejemplo muestra un campo incrustado junto a un campo normal con nombre.
type Job struct { Command string *log.Logger }
El tipo Job
ahora tiene el Print
, Printf
, Println
y otros métodos de *log.Logger
. Por supuesto, podríamos haberle dado al Logger
un nombre de campo, pero no es necesario hacerlo. Y ahora, una vez inicializado, podemos iniciar sesión en el Job
:
job.Println("starting now...")
El Logger
es un campo normal de la estructura Job
, por lo que podemos inicializarlo de la forma habitual dentro del constructor para Job
, así,
func NewJob(command string, logger *log.Logger) *Job { return &Job{command, logger} }
o con un literal compuesto,
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
Si necesitamos hacer referencia a un campo incrustado directamente, el nombre de tipo del campo, ignorando el calificador del paquete, sirve como nombre de campo, como lo hizo en el Read
de nuestra estructura ReadWriter
. Aquí, si necesitáramos acceder al *log.Logger
de un Job
variable job
, escribiríamos job.Logger
, lo cual sería útil si quisiéramos refinar los métodos de Logger
.
func (job *Job) Printf(format string, args ...interface{}) { job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...)) }
La incrustación de tipos introduce el problema de los conflictos de nombres, pero las reglas para resolverlos son simples. Primero, un campo o método X
oculta cualquier otro elemento X
en una parte más profundamente anidada del tipo. Si log.Logger
contuviera un campo o método llamado Command
, el campo Command
de Job
lo dominaría.
En segundo lugar, si el mismo nombre aparece en el mismo nivel de anidamiento, suele ser un error; Sería erróneo incrustar log.Logger
si la estructura Job
contuviera otro campo o método llamado Logger
. Sin embargo, si el nombre duplicado nunca se menciona en el programa fuera de la definición de tipo, está bien. Esta calificación proporciona cierta protección contra cambios realizados en tipos incorporados desde el exterior; no hay problema si se agrega un campo que entra en conflicto con otro campo en otro subtipo si ninguno de los campos se usa nunca.
Concurrencia
Comparte comunicando
La programación concurrente es un tema amplio y aquí solo hay espacio para algunos aspectos destacados específicos de Go.
La programación concurrente en muchos entornos se ve dificultada por las sutilezas necesarias para implementar el acceso correcto a las variables compartidas. Go fomenta un enfoque diferente en el que los valores compartidos se transmiten a través de channels y, de hecho, nunca se comparten activamente mediante hilos de ejecución separados. Sólo una gorutina tiene acceso al valor en un momento dado. Las carreras de datos no pueden ocurrir, por diseño. Para fomentar esta forma de pensar lo hemos reducido a un eslogan:
No te comuniques compartiendo memoria; en su lugar, comparte la memoria comunicándote.
Este enfoque puede llevarse demasiado lejos. La mejor manera de realizar los recuentos de referencias es colocando un mutex alrededor de una variable entera, por ejemplo. Pero como enfoque de alto nivel, el uso de channels para controlar el acceso facilita la escritura de programas claros y correctos.
Una forma de pensar en este modelo es considerar un programa típico de un solo subproceso que se ejecuta en una CPU. No necesita primitivas de sincronización. Ahora ejecuta otra instancia similar; tampoco necesita sincronización. Ahora deja que esos dos se comuniquen; Si la comunicación es el sincronizador, todavía no hay necesidad de otra sincronización. Los pipelines de Unix, por ejemplo, encajan perfectamente en este modelo. Aunque el enfoque de Go hacia la concurrencia se origina en los procesos de comunicación secuencial (CSP) de Hoare, también puede verse como una generalización de tipos seguros de las canalizaciones de Unix.
Goroutines
Se llaman gorrutinas porque los términos existentes (hilos, corrutinas, procesos, etc.) transmiten connotaciones inexactas. Una gorutina tiene un modelo simple: es una función que se ejecuta simultáneamente con otras gorutinas en el mismo espacio de direcciones. Es liviano y cuesta poco más que la asignación de espacio en la pila. Y las pilas empiezan siendo pequeñas, por lo que son económicas y crecen asignando (y liberando) almacenamiento en montón según sea necesario.
Las gorutinas se multiplexan en múltiples subprocesos del sistema operativo, por lo que si uno se bloquea, por ejemplo, mientras espera E/S, otros continúan ejecutándose. Su diseño oculta muchas de las complejidades de la creación y gestión de subprocesos.
Prefija una llamada de función o método con la palabra clave go
para ejecutar la llamada en una nueva rutina. Cuando se completa la llamada, la rutina sale silenciosamente. (El efecto es similar a la notación &
del shell Unix para ejecutar un comando en segundo plano).
go list.Sort() // run list.Sort concurrently; don't wait for it.
Una función literal puede ser útil en una invocación de rutina.
func Announce(message string, delay time.Duration) { go func() { time.Sleep(delay) fmt.Println(message) }() // Note the parentheses - must call the function. }
En Go, los literales de funciones son clousures: la implementación garantiza que las variables a las que hace referencia la función sobrevivan mientras estén activas.
Estos ejemplos no son demasiado prácticos porque las funciones no tienen forma de señalar su finalización. Para eso necesitamos channels.
Channels
Al igual que los maps, los channels se asignan con make
y el valor resultante actúa como referencia a una estructura de datos subyacente. Si se proporciona un parámetro entero opcional, establece el tamaño del búfer para el channel. El valor predeterminado es cero, para un channel sin búfer o síncrono.
ci := make(chan int) // unbuffered channel of integers cj := make(chan int, 0) // unbuffered channel of integers cs := make(chan *os.File, 100) // buffered channel of pointers to Files
Los channels sin búfer combinan comunicación (el intercambio de un valor) con sincronización, lo que garantiza que dos cálculos (gorrutinas) estén en un estado conocido.
Hay muchos modismos agradables que usan channels. Aquí hay uno para comenzar. En la sección anterior iniciamos una clasificación en segundo plano. Un channel puede permitir que la rutina de lanzamiento espere a que se complete el ordenamiento.
c := make(chan int) // Allocate a channel. // Start the sort in a goroutine; when it completes, signal on the channel. go func() { list.Sort() c <- 1 // Send a signal; value does not matter. }() doSomethingForAWhile() <-c // Wait for sort to finish; discard sent value.
Los receptores siempre bloquean hasta que haya datos para recibir. Si el channel no tiene buffer, el remitente se bloquea hasta que el receptor haya recibido el valor. Si el channel tiene un búfer, el remitente bloquea solo hasta que el valor se haya copiado al búfer; si el buffer está lleno, esto significa esperar hasta que algún receptor haya recuperado un valor.
Un channel almacenado en buffer puede usarse como un semáforo, por ejemplo, para limitar el rendimiento. En este ejemplo, las solicitudes entrantes se pasan a handle
, que envía un valor al channel, procesa la solicitud y luego recibe un valor del channel para preparar el "semáforo" para el siguiente consumidor. La capacidad del buffer del channel limita el número de llamadas simultáneas al process
.
var sem = make(chan int, MaxOutstanding) func handle(r *Request) { sem <- 1 // Wait for active queue to drain. process(r) // May take a long time. <-sem // Done; enable next request to run. } func Serve(queue chan *Request) { for { req := <-queue go handle(req) // Don't wait for handle to finish. } }
Una vez que los handlers MaxOutstanding
estén ejecutando process
, más bloquearán el intento de enviar al búfer del channel lleno, hasta que uno de los handlers existentes finalice y reciba del búfer.
Sin embargo, este diseño tiene un problema: Serve
crea una nueva rutina para cada solicitud entrante, aunque solo MaxOutstanding
de ellas puede ejecutarse en cualquier momento. Como resultado, el programa puede consumir recursos ilimitados si las solicitudes llegan demasiado rápido. Podemos abordar esa deficiencia cambiando Serve
para controlar la creación de las gorutinas. Aquí tienes una solución obvia, pero ten cuidado, tiene un error que solucionaremos más adelante:
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func() { process(req) // Buggy; see explanation below. <-sem }() } }
El error es que en un bucle Go for
, la variable del bucle se reutiliza para cada iteración, por lo que la variable req
es compartido en todas las gorutinas. Eso no es lo que queremos. Necesitamos asegurarnos de que req
sea único para cada rutina. Aquí hay una forma de hacerlo, pasando el valor de req
como argumento para el cierre en la rutina:
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func(req *Request) { process(req) <-sem }(req) } }
Compara esta versión con la anterior para ver la diferencia en cómo se declara y ejecuta el cierre. Otra solución es simplemente crear una nueva variable con el mismo nombre, como en este ejemplo:
func Serve(queue chan *Request) { for req := range queue { req := req // Create new instance of req for the goroutine. sem <- 1 go func() { process(req) <-sem }() } }
Puede parecer extraño escribir
req := req
pero es legal e idiomático en Go hacer esto. Obtiene una versión nueva de la variable con el mismo nombre, que deliberadamente oculta la variable de bucle localmente pero es única para cada rutina.
Volviendo al problema general de escribir el servidor, otro enfoque que administra bien los recursos es iniciar un número fijo de rutinas handle
, todas leyendo desde el channel de solicitud. El número de gorutinas limita el número de llamadas simultáneas al process
. Esta función Serve
también acepta un channel en el que se le indicará que salga; después de iniciar las gorutinas, bloquea la recepción de ese channel.
func handle(queue chan *Request) { for r := range queue { process(r) } } func Serve(clientRequests chan *Request, quit chan bool) { // Start handlers for i := 0; i < MaxOutstanding; i++ { go handle(clientRequests) } <-quit // Wait to be told to exit. }
Channels de channels
Una de las propiedades más importantes de Go es que un channel es un valor de primera clase que puede asignarse y transmitirse como cualquier otro. Un uso común de esta propiedad es implementar una demultiplexación paralela segura.
En el ejemplo de la sección anterior, handle
era un controlador ideal para una solicitud, pero no definimos el tipo que manejaba. Si ese tipo incluye un channel para responder, cada cliente puede proporcionar su propia ruta para la respuesta. A continuación se muestra una definición esquemática del tipo Request
.
type Request struct { args []int f func([]int) int resultChan chan int }
El cliente proporciona una función y sus argumentos, así como un channel dentro del objeto de solicitud para recibir la respuesta.
func sum(a []int) (s int) { for _, v := range a { s += v } return } request := &Request{[]int{3, 4, 5}, sum, make(chan int)} // Send request clientRequests <- request // Wait for response. fmt.Printf("answer: %d\n", <-request.resultChan)
En el lado del servidor, la función del controlador es lo único que cambia.
func handle(queue chan *Request) { for req := range queue { req.resultChan <- req.f(req.args) } }
Claramente hay mucho más por hacer para que sea realista, pero este código es un framework para un sistema RPC sin bloqueo, paralelo y de velocidad limitada, y no hay un mutex a la vista.
Paralelización
Otra aplicación de estas ideas es paralelizar un cálculo en múltiples núcleos de CPU. Si el cálculo se puede dividir en partes separadas que se pueden ejecutar de forma independiente, se puede paralelizar, con un channel para señalar cuándo se completa cada parte.
Digamos que tenemos que realizar una operación costosa en un vector de elementos y que el valor de la operación en cada elemento es independiente, como en este ejemplo idealizado.
type Vector []float64 // Apply the operation to v[i], v[i+1] ... up to v[n-1]. func (v Vector) DoSome(i, n int, u Vector, c chan int) { for ; i < n; i++ { v[i] += u.Op(v[i]) } c <- 1 // signal that this piece is done }
Lanzamos las piezas de forma independiente en un bucle, una por CPU. Pueden completar en cualquier orden pero no importa; simplemente contamos las señales de finalización drenando el channel después de lanzar todas las gorutinas.
const numCPU = 4 // number of CPU cores func (v Vector) DoAll(u Vector) { c := make(chan int, numCPU) // Buffering optional but sensible. for i := 0; i < numCPU; i++ { go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c) } // Drain the channel. for i := 0; i < numCPU; i++ { <-c // wait for one task to complete } // All done. }
En lugar de crear un valor constante para numCPU, podemos preguntarle al motor de ejecución qué valor es apropiado. La función runtime.NumCPU ⬀
devuelve el número de núcleos de CPU de hardware en la máquina, por lo que podríamos escribir
var numCPU = runtime.NumCPU()
También hay una función runtime.GOMAXPROCS ⬀
, que informa (o establece ) el número de núcleos especificado por el usuario que un programa Go puede ejecutar simultáneamente. Su valor predeterminado es runtime.NumCPU
pero se puede sobrescribir configurando la variable de entorno de Shell con un nombre similar o llamando a la función con un número positivo. Llamarlo con cero solo consulta el valor. Por lo tanto, si queremos honrar la solicitud de recursos del usuario, debemos escribir
var numCPU = runtime.GOMAXPROCS(0)
Asegúrate de no confundir las ideas de concurrencia (estructurar un programa como componentes que se ejecutan independientemente) y paralelismo (ejecutar cálculos en paralelo para lograr eficiencia en múltiples CPU). Aunque las características de concurrencia de Go pueden hacer que algunos problemas sean fáciles de estructurar como cálculos paralelos, Go es un lenguaje concurrente, no paralelo, y no todos los problemas de paralelización se ajustan al modelo de Go. Para una discusión sobre la distinción, consulte la charla citada en esta publicación de blog.
Un buffer con fugas
Las herramientas de programación concurrente pueden incluso hacer que las ideas no concurrentes sean más fáciles de expresar. A continuación se muestra un ejemplo extraído de un paquete RPC. La rutina del cliente realiza un bucle que recibe datos de alguna fuente, tal vez una red. Para evitar asignar y liberar buffers, mantiene una lista libre y utiliza un channel almacenado en buffer para representarla. Si el channel está vacío, se asigna un nuevo búfer. Una vez que el búfer de mensajes está listo, se envía al servidor en serverChan
.
var freeList = make(chan *Buffer, 100) var serverChan = make(chan *Buffer) func client() { for { var b *Buffer // Grab a buffer if available; allocate if not. select { case b = <-freeList: // Got one; nothing more to do. default: // None free, so allocate a new one. b = new(Buffer) } load(b) // Read next message from the net. serverChan <- b // Send to server. } }
El bucle del servidor recibe cada mensaje del cliente, lo procesa y devuelve el búfer a la lista libre.
func server() { for { b := <-serverChan // Wait for work. process(b) // Reuse buffer if there's room. select { case freeList <- b: // Buffer on free list; nothing more to do. default: // Free list full, just carry on. } } }
El cliente intenta recuperar un búfer de freeList
; si no hay ninguno disponible, asigna uno nuevo. El envío del servidor a freeList
coloca a b
nuevamente en la lista libre a menos que la lista esté llena, en cuyo caso el buffer se deja caer al suelo para que el recolector de basura lo recupere. (Las cláusulas default
en las sentencias select
se ejecutan cuando ningún otro caso está listo, lo que significa que selects
nunca se bloquea). Esta implementación crea una lista libre de depósitos con fugas en solo unas pocas líneas, basándose en el channel almacenado en búfer y el recolector de basura para la contabilidad.
Errores
Las rutinas de biblioteca a menudo deben devolver algún tipo de indicación de error a quien llama. Como se mencionó anteriormente, la devolución de valores múltiples de Go facilita la devolución de una descripción detallada del error junto con el valor de devolución normal. Es un buen estilo utilizar esta función para proporcionar información detallada sobre errores. Por ejemplo, como veremos, os.Open
no solo devuelve un puntero nil
en caso de error, sino que también devuelve un valor de error que describe lo que salió mal.
Por convención, los errores tienen el tipo error
, una interfaz incorporada simple.
type error interface { Error() string }
Un escritor de biblioteca es libre de implementar esta interfaz con un modelo más rico bajo las sábanas, lo que hace posible no sólo ver el error sino también proporcionar algo de contexto. Como se mencionó, junto con el valor de retorno habitual *os.File
, os.Open
también devuelve un valor de error. Si el archivo se abre correctamente, el error será nil
, pero cuando haya un problema, contendrá un os.PathError
:
// PathError records an error and the operation and // file path that caused it. type PathError struct { Op string // "open", "unlink", etc. Path string // The associated file. Err error // Returned by the system call. } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
PathError
-> Error
genera una cadena como esta:
open /etc/passwx: no such file or directory
Un error de este tipo, que incluye el nombre del archivo problemático, la operación y el error del sistema operativo que desencadenó, es útil incluso si se imprime lejos de la llamada que lo causó; es mucho más informativo que el simple "no existe tal archivo o directorio".
Cuando sea posible, las cadenas de error deben identificar su origen, por ejemplo, teniendo un prefijo que nombre la operación o paquete que generó el error. Por ejemplo, en el paquete image
, la representación de cadena para un error de decodificación debido a un formato desconocido es "imagen: formato desconocido".
Los llamadores que se preocupan por los detalles precisos del error pueden usar un cambio de tipo o una aserción de tipo para buscar errores específicos y extraer detalles. Para PathErrors
, esto podría incluir examinar el campo interno Err
en busca de errores recuperables.
for try := 0; try < 2; try++ { file, err = os.Create(filename) if err == nil { return } if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC { deleteTempFiles() // Recover some space. continue } return }
La segunda declaración if
aquí es otra afirmación de tipo. Si falla, ok
será falso y e
será nil
. Si tiene éxito, ok
será verdadero, lo que significa que el error fue de tipo *os.PathError
, y luego también lo será e
, que puedes examinarlo para obtener más información sobre el error.
Panic
La forma habitual de informar un error a un llamador es devolver un error
como valor de retorno adicional. El método canónico Read
es un ejemplo bien conocido; devuelve un recuento de bytes y un error
. ¿Pero qué pasa si el error es irrecuperable? A veces el programa simplemente no puede continuar.
Para este propósito, hay una función incorporada panic
que de hecho crea un error de tiempo de ejecución que detendrá el programa (pero mira la siguiente sección). La función toma un único argumento de tipo arbitrario (a menudo una cadena) que se imprimirá cuando el programa finalice. También es una forma de indicar que ha sucedido algo imposible, como salir de un bucle infinito.
// A toy implementation of cube root using Newton's method. func CubeRoot(x float64) float64 { z := x/3 // Arbitrary initial value for i := 0; i < 1e6; i++ { prevz := z z -= (z*z*z-x) / (3*z*z) if veryClose(z, prevz) { return z } } // A million iterations has not converged; something is wrong. panic(fmt.Sprintf("CubeRoot(%g) did not converge", x)) }
Esto es sólo un ejemplo, pero las funciones reales de la biblioteca deberían evitar el panic
. Si el problema se puede enmascarar o solucionar, siempre es mejor dejar que todo siga funcionando en lugar de cerrar todo el programa. Un posible contraejemplo es durante la inicialización: si la biblioteca realmente no puede configurarse por sí sola, podría ser razonable entrar en pánico, por así decirlo.
var user = os.Getenv("USER") func init() { if user == "" { panic("no value for $USER") } }
Recover
Cuando se llama a panic
, incluso implícitamente para errores de tiempo de ejecución, como indexar una porción fuera de límites o fallar en una aserción de tipo, detiene inmediatamente la ejecución de la función actual y comienza a desenrollar la pila de la rutina, ejecutando cualquier función diferida a lo largo del camino. Si ese desenrollado llega a la parte superior de la pila de la rutina, el programa muere. Sin embargo, es posible utilizar la función incorporada recover
para recuperar el control de la rutina y reanudar la ejecución normal.
Una llamada a recover
detiene el desenredado y devuelve el argumento pasado a panic
. Debido a que el único código que se ejecuta mientras se desenrolla está dentro de las funciones diferidas, recover
solo es útil dentro de las funciones diferidas.
Una aplicación de recover
es cerrar una gorutina defectuosa dentro de un servidor sin matar las otras gorutinas en ejecución.
func server(workChan <-chan *Work) { for work := range workChan { go safelyDo(work) } } func safelyDo(work *Work) { defer func() { if err := recover(); err != nil { log.Println("work failed:", err) } }() do(work) }
En este ejemplo, si do(work)
entra en pánico, el resultado se registrará y la rutina saldrá limpiamente sin molestar a los demás. No es necesario hacer nada más en el cierre diferido; llamar a recover
maneja la condición por completo.
Debido a que recover
siempre devuelve nil
a menos que se llame directamente desde una función diferida, el código diferido puede llamar a rutinas de biblioteca que usan panic
y recover
sin fallar. Como ejemplo, la función diferida en safelyDo
podría llamar a una función de registro antes de llamar a recover
, y ese código de registro se ejecutaría sin verse afectado por el estado de pánico.
Con nuestro patrón de recuperación implementado, la función do
(y cualquier cosa que llame) puede salir de cualquier situación mala limpiamente llamando a panic
. Podemos utilizar esa idea para simplificar el manejo de errores en software complejo. Veamos una versión idealizada de un paquete regexp
, que informa errores de análisis llamando a panic
con un tipo de error local. Aquí está la definición de Error
, un método error
y la función Compile
.
// Error is the type of a parse error; it satisfies the error interface. type Error string func (e Error) Error() string { return string(e) } // error is a method of *Regexp that reports parsing errors by // panicking with an Error. func (regexp *Regexp) error(err string) { panic(Error(err)) } // Compile returns a parsed representation of the regular expression. func Compile(str string) (regexp *Regexp, err error) { regexp = new(Regexp) // doParse will panic if there is a parse error. defer func() { if e := recover(); e != nil { regexp = nil // Clear return value. err = e.(Error) // Will re-panic if not a parse error. } }() return regexp.doParse(str), nil }
Si doParse
entra en pánico, el bloque de recuperación establecerá el valor de retorno en nil
; las funciones diferidas pueden modificar los valores de retorno con nombre. Luego comprobará, en la asignación a err
, que el problema fue un error de análisis afirmando que tiene el tipo local Error
. Si no es así, la aserción de tipo fallará, provocando un error de tiempo de ejecución que continúa desenrollándose la pila como si nada la hubiera interrumpido. Esta verificación significa que si sucede algo inesperado, como un índice fuera de límites, el código fallará aunque estemos usando panic
y recover
para manejar los errores de análisis.
Con el manejo de errores implementado, el método error
(debido a que es un método vinculado a un tipo, está bien, incluso natural, que tenga el mismo name como el tipo de error
incorporado) facilita informar errores de análisis sin preocuparse por desenrollar la pila de análisis a mano:
if pos == 0 { re.error("'*' illegal at start of expression") }
Aunque este patrón es útil, solo debe usarse dentro de un paquete. Parse
convierte sus llamadas internas de panic
en valores de error
; no expone panics
a su cliente. Ésa es una buena regla a seguir.
Por cierto, este modismo re-panic cambia el valor de pánico si ocurre un error real. Sin embargo, tanto los fallos originales como los nuevos se presentarán en el informe de fallos, por lo que la causa raíz del problema seguirá siendo visible. Por lo tanto, este simple método de volver a entrar en pánico suele ser suficiente (después de todo, es un bloqueo), pero si desea mostrar solo el valor original, puede escribir un poco más de código para filtrar problemas inesperados y volver a entrar en pánico con el error original. Esto se deja como ejercicio para el lector.
Un servidor web
Terminemos con un programa Go completo, un servidor web. Este es en realidad una especie de servidor web. Google proporciona un servicio en chart.apis.google.com
que formatea automáticamente los datos en tablas y gráficos. Sin embargo, es difícil de usar de forma interactiva porque es necesario colocar los datos en la URL como una consulta. El programa aquí proporciona una interfaz más agradable para una forma de datos: dado un breve fragmento de texto, solicita al servidor de gráficos que genere un código QR, una array de cuadros que codifican el texto. Esa imagen puede capturarse con la cámara de su teléfono celular e interpretarse como, por ejemplo, una URL, lo que le ahorrará tener que escribir la URL en el pequeño teclado del teléfono.
Aquí tienes el programa completo. Sigue una explicación.
package main
import (
"flag"
"html/template"
"log"
"net/http"
)
var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18
var templ = template.Must(template.New("qr").Parse(templateStr))
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func QR(w http.ResponseWriter, req *http.Request) {
templ.Execute(w, req.FormValue("s"))
}
const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET">
<input maxLength=1024 size=70 name=s value="" title="Text to QR Encode">
<input type=submit value="Show QR" name=qr>
</form>
</body>
</html>
`
Las piezas hasta main
deben ser fáciles de seguir. La bandera establece un puerto HTTP predeterminado para nuestro servidor. La variable de plantilla templ
es donde ocurre la diversión. Crea una plantilla HTML que el servidor ejecutará para mostrar la página; Más sobre eso en un momento.
La función main
analiza las banderas y, usando el mecanismo del que hablamos anteriormente, vincula la función QR
a la ruta raíz. para el servidor. Luego se llama a http.ListenAndServe
para iniciar el servidor; se bloquea mientras el servidor se ejecuta.
QR
simplemente recibe la solicitud, que contiene datos del formulario, y ejecuta la plantilla sobre los datos en el valor del formulario llamado s
.
El paquete de plantillas html/template
es poderoso; este programa solo toca sus capacidades. En esencia, reescribe un fragmento de texto HTML sobre la marcha sustituyendo elementos derivados de elementos de datos pasados a templ.Execute
, en este caso el valor del formulario. Dentro del texto de la plantilla (templateStr
), las partes delimitadas por doble llave denotan acciones de la plantilla. La parte de {{if .}}
a {{end}}
se ejecuta solo si el valor del elemento de datos actual, llamado .
( punto), no está vacío. Es decir, cuando la cadena está vacía, esta parte de la plantilla se suprime.
Los dos fragmentos {{.}}
dicen mostrar los datos presentados a la plantilla (la cadena de consulta) en la página web. El paquete de plantillas HTML proporciona automáticamente un escape apropiado para que sea seguro mostrar el texto.
El resto de la cadena de la plantilla es solo el HTML que se muestra cuando se carga la página. Si esta es una explicación demasiado rápida, consulta la documentación ⬀ del paquete de plantilla para obtener una explicación más detallada.
Y ahí lo tienes: un servidor web útil en unas pocas líneas de código más texto HTML basado en datos. Go es lo suficientemente poderoso como para hacer que sucedan muchas cosas en unas pocas líneas.