Go eficiente
Puedes ver el documento original aquí
- Introducción
- Formateo
- Comentarios
- Nombres
- Puntos y comas
- Estructuras de control
- Funciones
- Datos
- Iniciación
- Métodos
- Interfaces y otros tipos
- El identificador blanco
- Incrustando
- Concurrencia
- Errores
- Un servidor web
Introducción
Go es un nuevo lenguaje. A pesar de que toma ideas prestadas de lenguajes existentes, tiene propiedades inusuales que hacen que los eficientes programas Go sean diferentes en carácter a los programas escritos en sus parientes. Una sencilla traducción de un programa C++ o Java a Go probablemente no pueda producir un resultado satisfactorio —los programas Java están escritos en Java, no en Go. Por otro lado, pensando sobre el problema desde la perspectiva de Go podría producir un programa exitoso pero bastante diferente. En otras palabras, para escribir buen Go, es importante entender sus propiedades y modismos. Además, es importante conocer las convenciones establecidas para programar en Go, tal como la nomenclatura, formateo, construcción del programa, etc., de modo tal que los programas que escribas los entiendan fácilmente otros programadores de Go.
Este documento proporciona consejos para escribir código Go claro e idiomático. Este complementa la especificación del lenguaje, Un paseo por Go y Cómo escribir código Go, mismos que deberías leer primero.
Ejemplos
La fuente de los paquetes Go se pretende sirva no solo como la biblioteca del núcleo sino también como ejemplos de cómo utilizar el lenguaje. Además, muchos de los paquetes contienen arquetipos ejecutables autocontenidos que puedes correr directamente desde el sitio web golang.org, tal 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 en la biblioteca te pueden proporcionar respuestas, ideas y antecedentes.
Formateo
El tema del formateo es el más contencioso pero al que se le presta menos importancia. La gente puede adoptar diferentes estilos de formateo pero sería mejor si no tuvieran que hacerlo y dedicarían menos tiempo al tema si todo mundo se adhiriese al mismo estilo. El problema es cómo abordar esta utopía sin una gran guía de estilo prescriptiva.
En Go adoptamos un enfoque inusual y dejamos que la máquina cuide de la
mayoría de los problemas de formateo. El programa gofmt
(también
disponible como go fmt
, el cual opera a nivel de paquete más que a nivel
de archivo fuente) lee un programa Go y emite la fuente en un estilo de
sangría y alineación vertical estándar, reteniendo y si es necesario
reformateando los comentarios. Si quieres saber cómo manejar algunas
nuevas situaciones de diseño, ejecuta gofmt
; si el resultado no te
parece bien, reacomoda tu programa (opcionalmente registra un fallo sobre
gofmt
) y no te preocupes más de ello.
Por ejemplo, no hay ninguna necesidad de desperdiciar tiempo alineando los
comentarios en los campos de una estructura. Gofmt
lo hará por ti.
Dada la declaración:
type T struct {
nombre string // nombre del objeto
valor int // su valor
}
gofmt
alineará las columnas:
type T struct {
nombre string // nombre del objeto
valor int // su valor
}
Todo el código Go en los paquetes estándar se ha formateado con gofmt
.
Aquí tienes algunos detalles de formato. Muy brevemente:
Sangría
Utilizamos tabuladores para la sangría y de manera predeterminada
gofmt
los emite. Usa espacios solo si es necesario.
Longitud de línea
Go no tiene ningún límite de longitud de línea. No te preocupes por desbordar una tarjeta perforada. Si sientes que una línea es demasiado larga, envuélvela y sángrala con un tabulador extra.
Paréntesis
Go necesita menos paréntesis que C y Java: las estructuras de control (
if
,for
,switch
) no tienen paréntesis en su sintaxis. Además, la jerarquía del operador de precedencia es más corta y más clara, por lo tanto:
x<<8 + y<<16
significa lo que el espaciado implica, a diferencia de los otros lenguajes.
Comentarios
Go proporciona comentarios de bloque al estilo de C /* */
y comentarios
de línea al estilo de C++ //
. Los comentarios de línea son la norma;
los comentarios de bloque generalmente aparecen como comentarios de
paquete, pero son útiles dentro de una expresión o para desactivar grandes
franjas de código.
El programa —y servidor web— godoc
procesa archivos fuente
de Go para extraer la documentación desde el contenido del paquete.
Extrae los comentarios que aparecen antes de las declaraciones de nivel
superior, sin tomar en cuenta los saltos de línea, junto con su
declaración para que sirvan como el texto explicativo del elemento. La
naturaleza y estilo de estos comentarios determina la calidad de la
documentación que godoc
produce.
Cada paquete debería tener un comentario de paquete, un comentario de
bloque que preceda a la cláusula package
. Para paquetes multiarchivo,
el comentario de paquete únicamente debe estar presente en un archivo, en
cualquiera de ellos funcionará. El comentario de paquete debe introducir
el paquete y proporcionar información relevante sobre el paquete en su
conjunto. Esta aparecerá primero en la página godoc
y debería
configurar la documentación detallada que sigue.
/*
regexp. Este paquete implementa una sencilla biblioteca para expresiones
regulares.
La sintaxis aceptada de las expresiones regulares es:
regexp:
Concatenación { '|' concatenación }
concatenación:
{ cierre }
cierre:
término [ '*' | '+' | '?' ]
término:
'^'
'$'
'.'
carácter
'[' [ '^' ] rangos de caracteres ']'
'(' regexp ')'
*/
package regexp
Si el paquete es sencillo, el comentario de paquete puede ser breve.
// path. Este paquete implementa útiles rutinas para manipular
// rutas a nombres de archivo separados por barras inclinadas.
Los comentarios no necesitan formateo extra tal como viñetas de estrellas.
La salida generada incluso puede no presentarse en un tipo de letra de
ancho fijo, así que no depende del espaciado para alineamiento de godoc
,
puesto que gofmt
, cuida de ello. Los comentarios son texto sencillo no
interpretado, así que HTML y otras anotaciones como _esta_
se
reproducirán literalmente y no se deberían utilizar. Un ajuste que hace
godoc
es mostrar texto sangrado en un tipo de letra de ancho fijo,
propio para fragmentos de programa. El comentario principal del
paquete fmt
utiliza este buen efecto.
Dependiendo del contexto, godoc
incluso podría reformatear los
comentarios, así que asegúrate que se vean bien desde el principio: usa
ortografía, puntuación y estructura de frases correcta, envuelve líneas
largas, etc.
Dentro de un paquete, cualquier comentario precedido inmediatamente por una declaración de nivel superior sirve como comentario doc para esa declaración. Cada nombre exportado (con mayúscula inicial) en un programa debería tener un comentario doc.
Los comentarios doc trabajan mejor en frases completas, las cuales dejan una amplia variedad de presentaciones automatizadas. La primera frase debería ser un resumen de una oración que inicie con el nombre a declarar.
// Compile analiza una expresión regular y devuelve, de ser exitosa, un
// objeto Regexp que suele usarse para encontrar el texto buscado.
func Compile(str string) (regexp *Regexp, err error) {
Si el comentario siempre empieza con el nombre, la salida de godoc
se
puede correr convenientemente a travez de grep
. Imagina que no puedes
recordar el nombre "Compile" pero estás buscando la función para analizar
expresiones regulares, así que ejecutas la orden:
$ godoc regexp | grep analiza
Si todos los comentarios doc en el paquete empiezan con "Esta función...",
grep
no te ayudará a recordar el nombre. Pero debido a que en el paquete
cada comentario doc empieza con el nombre, verás algo cómo esto, lo cual
te recuerda la palabra que estás buscando.
$ godoc regexp | grep analiza
Compile analiza una expresión regular y devuelve, de ser exitosa, una Regexp
analizada sintácticamente. Esto simplifica la iniciación segura de las variables globales
que no se pueden analizar. Esto simplifica la iniciación segura de variables globales
$
La sintaxis de declaración Go te permite agrupar declaraciones. Un solo comentario doc puede introducir un grupo relacionado de constantes o variables. Debido a que se presenta la declaración completa, a menudo tal comentario puede ser superficial.
// Códigos de error devueltos por fallos al analizar una expresión.
var (
ErrInternal = errors.New("regexp: error interno")
ErrUnmatchedLpar = errors.New("regexp: incompatible '('")
ErrUnmatchedRpar = errors.New("regexp: incompatible ')'")
...
)
El agrupamiento también puede indicar relaciones entre elementos, tal como el hecho de que un conjunto de variables esté protegido por una exclusión mutua.
var (
countLock sync.Mutex
inputCount uint32
outputCount uint32
errorCount uint32
)
Nombres
Los nombres son tan importantes en Go cómo en cualquier otro lenguaje. Incluso tienen 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 tanto dedicaremos un poco de tiempo para hablar sobre las convenciones de nomenclatura en los programas Go.
Nomenclatura de paquetes
Cuando se importa un paquete, el nombre del paquete proviene de un método de acceso al contenido. Después de:
import "bytes"
El paquete importador puede hablar sobre bytes.Buffer
. Es útil si todos
los que utilizan el paquete pueden usar el mismo nombre para referirse a
su contenido, lo cual implica que el nombre del paquete tendría que ser
bueno: corto, conciso, evocador. Por convención, a los paquetes se les
dan nombres de una sola palabra en minúsculas; no debería haber necesidad
de guiones bajos o mayúsculas intercaladas. Errando por el lado de la
brevedad, puesto que cualquiera que utilice tu paquete tendrá que escribir
ese nombre. Y a priori no te preocupan las colisiones. El nombre del
paquete es solo el nombre predeterminado para importaciones; este no tiene
que ser único entre todo el código fuente y en el raro caso de una
colisión el paquete importador puede elegir un nombre diferente para
utilizarlo localmente. En cualquier caso, la confusión es rara porque el
nombre de archivo en la importación determina justo 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/pkg/encoding/base64
se importa como
"encoding/base64"
pero se llama base64
, no encoding_base64
ni
encodingBase64
.
El importador de un paquete utilizará el nombre para referirse a su
contenido, por lo tanto los nombres exportados en el paquete pueden
utilizar este hecho para evitar confusiones. (No utilices la notación
import .
, la cual puede simplificar pruebas que debes correr fuera del
paquete que estás probando, al contrario debes evitarla). Por ejemplo, el
tipo lector de búfer en el paquete bufio
se llama Reader
, no
BufReader
, debido a que los usuarios ven bufio.Reader
, porque es un
nombre claro y conciso. Además, dado que las entidades importadas siempre
van precedidas por el nombre del paquete, bufio.Reader
no choca con
io.Reader
. Del mismo modo, 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 puesto que Ring
es el único tipo
exportado por el paquete y dado que el paquete se llama ring
, justamente
estás llamando a New
, cuyos clientes del paquete ven como ring.New
.
Usa la estructura del paquete para ayudarte a escoger buenos nombres.
Otro breve ejemplo es unavez.Haz
; unavez.Haz(ajusta)
se lee bien y no
mejoraría al escribir unavez.HazOEsperaHastaQueEsteHecho(ajusta)
. Los
nombres largos no hacen las cosas más legibles automáticamente. A menudo,
un útil comentario doc puede ser más valioso que un nombre extralargo.
Captadores
Go no proporciona soporte automático para captadores y definidores. No
hay nada incorrecto en proporcionar captadores y definidores y a menudo es
apropiado hacerlo, pero tampoco es idiomático ni necesario poner Obt
al
nombre del captador. Si tienes un campo llamado propietario
(en
minúsculas, no exportado), el método captador se tendría que llamar
Propietario
(en mayúsculas, exportado), no ObtPropietario
. El uso de
mayúsculas en los nombres para exportación proporciona el gancho para
diferenciar un campo de un método. Una función definidora, si la
necesitas, probablemente se llamará EstPropietario
. Ambos nombres se
leen bien en la práctica:
propietario := obj.Propietario()
if propietario != usuario {
obj.EstPropietario(usuario)
}
Nombre de interfaces
Por convención, para denominar un método de interfaz se utiliza el nombre
del método más un sufijo -er o modificación similar para construir un
sustantivo del agente: Reader
, Writer
, Formatter
, CloseNotifier
,
etc.
Para honrrarlos hay una serie de nombres productivos y nombres de
funciones captadoras. Read
, Write
, Close
, Flush
, String
y así
por el estilo que tienen significado y firmas canónicas. Para evitar
confusión, no denomines tus métodos con uno de esos nombres a no ser que
tenga la misma firma y significado. En cambio, si tu tipo implementa un
método con el mismo significado que un método en un tipo bien conocido,
dale el mismo nombre y firma; ponle el nombre String
a tu método
convertidor a cadena no ToString
.
Mayúsculas intercaladas
Finalmente, la convención en Go es utilizar MayúsculasIntercaladas
o
mayúsculasIntercaladas
en lugar de guiones bajos para escribir nombres
multipalabra.
Puntos y comas
Como en C, la gramática formal de Go usa puntos y comas para terminar declaraciones, pero a diferencia de C, esos puntos y comas no aparecen en la fuente. En su lugar el analizador léxico al escanear la fuente utiliza una sencilla regla para insertar puntos y comas automáticamente, así que de entrada, el texto mayoritariamente está libre de ellos.
La regla es esta. Si el último segmento antes de una nueva línea es un
identificador (el cuál incluye palabras como int
y float64
), un
literal básico tal como un número, una cadena constante o uno de los
símbolos
break continue fallthrough return ++ -- ) }
El analizador léxico siempre inserta un punto y coma después del símbolo. Esto se podría resumir como, “si la nueva línea viene después de un segmento que pudiera acabar una declaración, inserta un punto y coma”.
Un punto y coma también se puede omitir inmediatamente antes de una llave de cierre, así que una declaración como
go func() { for { dst <- <-fnt } }()
no necesita punto y coma. Los programas idiomáticos de Go tienen puntos y
comas solo en sitios como las cláusulas del bucle for
, para separar el
iniciador, la condición y los elementos de continuación. También son
necesarios para separar múltiples declaraciones en una línea, debes
escribir código de esa manera.
Una consecuencia de las reglas de inserción automática del 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, el cual podría causar efectos
no deseados. Escríbelos así
if i < f() {
g()
}
no así
if i < f() // ¡Incorrecto!
{ // ¡Incorrecto!
g()
}
Estructuras de control
Las estructuras de control de Go están relacionadas a las de C pero
difieren en importantes maneras. No hay bucles do
o while
, solo un
ligeramente generalizado for
; switch
es más flexible; if
y switch
aceptan una declaración de inicio opcional como la del for
; las
declaraciones break
y continue
toman una etiqueta opcional para
identificar qué interrumpir o continuar; y hay nuevas estructuras de
control incluyendo un tipo switch
y un multiplexor de comunicaciones
multivía, select
. La sintaxis también es ligeramente diferente: no hay
paréntesis y los cuerpos siempre tienen que estar delimitados por llaves.
If
En Go una sencilla if
tiene esta apariencia:
if x > 0 {
return y
}
Las llaves obligatorias alientan la escritura de sencillas declaraciones
if
en múltiples líneas. Es buen estilo hacerlo en todo caso,
especialmente cuando el cuerpo contiene una instrucción de control tal
como return
o break
.
Dado que if
y switch
aceptan una declaración de iniciació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
ifluye en la siguiente instrucción —es decir, el cuerpo termina en
break
, continue
, goto
o return
— se omite el else
innecesario.
f, err := os.Open(nombre)
if err != nil {
return err
}
códigoUsando(f)
Este es un ejemplo de una situación común donde el código tiene que
vigilar una secuencia de condiciones de error. El código se lee bien si
el flujo de control es exitoso adelanta la página, eliminando casos de
error cuando surgen. Puesto que los casos de error tienden a terminar en
declaraciones return
, el código resultante no necesita declaraciones
else
.
f, err := os.Open(nombre)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
códigoUsando(f, d)
Redeclaración y reasignación
Aparte: El último ejemplo en la sección anterior demuestra un detalle de
cómo trabaja la declaración corta de variables :=
. La declaración que
llama a os.Open
dice:
f, err := os.Open(nombre)
Esta declaración crea dos variables, f
y err
.
Unas cuantas líneas más abajo, la llamada a f.Stat
dice,
d, err := f.Stat()
la cual se ve como si declarara d
y err
. Observa que, no obstante, err
aparece en ambas declaraciones. Esta duplicidad es legal: err
fue
creada en la primera declaración, pero únicamente reasignada en la
segunda. Esto significa que la llamada a f.Stat
utiliza la variable
err
existente declarada arriba y solo le da un nuevo valor.
En una declaración :=
puede aparecer una variable v
incluso si ya se
ha declarado, siempre y cuando:
- Esa declaración esté en el mismo ámbito que la declaración existente de
v
(siv
ya estuviera declarada en un ámbito exterior, la declaración creará una nueva variable), - El valor correspondiente en la iniciación es asignable a
v
y - Cuándo menos se crea una nueva variable en esa declaración.
Esta inusual propiedad es pragmatismo puro, facilitando la utilización de
un solo valor de err
, por ejemplo, en una larga cadena if-else
. Verás
que esto se utiliza a menudo.
Aquí, vale la pena recalcar que en Go el ámbito de los parámetros de función y el de los valores de retorno es el mismo que en el cuerpo de la función, incluso aunque léxicamente aparecen fuera de las llaves que encierran el cuerpo.
For
El bucle for
de Go es similar —a pero no igual— al de C.
Este unifica for
y while
y no hay do-while
. Hay tres formas, solo
una de las cuales tiene puntos y comas.
// Como un for C
for inicio; condición; incremento { }
// Como un while C
for condición { }
// Como un for(;;) C
for { }
Las instrucciones cortas facilitan la declaración de la variable inicial en el bucle.
suma := 0
for i := 0; i < 10; i++ {
suma += i
}
Si estás recorriendo un arreglo, sector, cadena o mapa, o estás leyendo
desde un canal, una cláusula range
puede manejar el bucle.
for clave, valor := range mapaAnterior {
nuevoMapa[clave] = valor
}
Si solo necesitas el primer elemento en el rango (la clave o índice), quita el segundo:
for clave := range m {
if clave.expira() {
delete(m, clave)
}
}
Si solo necesitas el segundo elemento en el rango (el valor), usa el identificador blanco, un guión bajo, para desechar el primero:
suma := 0
for _, valor := range array {
suma += valor
}
El identificador blanco tiene muchos usos, como se describe en una sección más adelante.
Para las cadenas, range
hace más trabajo para ti, dividiendo los
caracteres Unicode individuales mediante el análisis del UTF-8. Las
codificaciones erróneas consumen un byte y producen la sustitución del
rune U+FFFD. (El nombre —con tipo incorporado asociado—
rune
es terminología Go para un solo carácter Unicode. Ve los detalles
en la especificación del lenguaje). El bucle:
for pos, carácter := range "日本\x80語" { // \x80 es una codificación UTF-8
// illegal
fmt.Printf("el carácter %#U empieza en el byte %d\n", carácter, pos)
}
imprime:
el carácter U+65E5 '日' empieza en el byte 0
el carácter U+672C '本' empieza en el byte 3
el carácter U+FFFD '�' empieza en el byte 6
el carácter U+8A9E '語' empieza en el byte 7
Finalmente, Go no tiene el operador coma, además, ++
y --
son
instrucciones, no expresiones. Por lo que si quieres usar múltiples
variables en un for
tendrás que utilizar asignación paralela (a pesar de
que excluye a ++
y --
).
// Reverso 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 es
necesario que sean constantes o incluso enteros, los casos se evalúan de
arriba hacia abajo hasta encontrar una coincidencia y si el switch
no
tiene una expresión este cambia a true
. Por lo tanto es posible
—e idiomático— escribir una cadena de 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 hay compatibilidad automática hacia atrás, pero se pueden presentar casos en listas separadas por comas.
func seDebeEscapar(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
A pesar de que no son tan comunes en Go como en algunos otros lenguajes
como C, las declaraciones break
se pueden usar para terminar
anticipadamente un switch
. A veces, no obstante, es necesario romper un
bucle redundante, no el switch
y en Go se puede cumplimentar poniendo
una etiqueta en el bucle y "rompiendo" hacia esa etiqueta. Este ejemplo
muestra ambos usos.
Repite:
for n := 0; n < len(src); n += tamaño {
switch {
case src[n] < tamañoUno:
if soloValida {
break
}
tamaño = 1
actualiza(src[n])
case src[n] < tamañoDos:
if n+1 >= len(src) {
err = errEntradaCorta
break Repite
}
if soloValida {
break
}
tamaño = 2
actualiza(src[n] + src[n+1]<<shift)
}
}
Naturalmente, la instrucción continue
también acepta una etiqueta
opcional pero esta solo aplica en bucles.
Para cerrar esta sección, aquí está una rutina de comparación para
sectores de byte que usa dos instrucciones switch
:
// Compara devuelve un entero comparando lexicográficamente
// los dos sectores de byte.
// El resultado será 0 si a == b, -1 si a < b y +1 si a > b
func Compara(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
Un switch
también puede descubrir dinámicamente el tipo de una variable
de interfaz. Tal switch de tipo utiliza la sintaxis de una aserción de
tipo con la palabra clave type
dentro de los 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, en efecto declarando una nueva variable con el
mismo nombre pero un diferente tipo en cada caso.
var t interface{}
t = funcionDeAlgunTipo()
switch t := t.(type) {
default:
fmt.Printf("tipo inesperado %T\n", t) // imprime %T cuando t tiene tipo
case bool:
fmt.Printf("lógico %t\n", t) // t es de tipo bool
case int:
fmt.Printf("entero %d\n", t) // t es de tipo int
case *bool:
fmt.Printf("puntero a lógico %t\n", *t) // t es de tipo *bool
case *int:
fmt.Printf("puntero a entero %d\n", *t) // t es de tipo *int
}
Funciones
Retorno de múltiples valores
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 usar para
mejorar un par de torpes modismos en programas C: en devoluciones de error
en banda tal como -1
para EOF
y modificando un argumento pasado por
dirección.
En C, un error de escritura es señalado por un contador negativo con el
código de error oculto en una ubicación volátil. En Go, Write
puede
devolver un contador 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 (archivo *File) Write(b []byte) (n int, err error)
y como dice la documentación, devuelve el número de bytes escritos y un
error
distinto de nil
cuando n != len(b)
. Este es un estilo común;
ve la sección sobre el manejo de errores para más ejemplos.
Un enfoque similar evita la necesidad de pasar un puntero a un valor de retorno para simular una referencia al parámetro. Aquí tienes una ingenua función para grabar un número desde una posición en el byte de un sector, devolviendo el número y la siguiente posición.
func sigEnt(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
}
La podrías utilizar para escanear los números en un sector de entrada b
de la siguiente manera:
for i := 0; i < len(b); {
x, i = sigEnt(b, i)
fmt.Println(x)
}
Parámetros de retorno nombrados
Los “parámetros” de retorno o de resultado de una función Go
pueden ser nombrados y los puedes utilizar como variables regulares, igual
que los parámetros de entrada. Cuando son nombrados, al empezar la
función se inician a los valores cero para sus tipos correspondientes; si
la función ejecuta una instrucción return
sin argumentos, los valores
actuales de los parámetros de retorno serán utilizados como los valores
devueltos.
Los nombres no son obligatorios pero pueden hacer que el código sea
más corto y más claro: puesto que son documentación. Si nombramos los
resultados de sigEnt
se vuelve obvio cuál int
devuelto es qué.
func sigEnt(b []byte, pos int) (valor, sigPos int) {
Debido a que los resultados nombrados se inician y vinculan sin adornos a
un retorno, estos pueden simplificar además de clarificar. Aquí tienes
una versión de io.ReadFull
que los utiliza 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 defer
de Go programa una llamada a función (la función
diferida) para ejecutarla inmediatamente antes de que la función que
está ejecutando defer
regrese. Es una inusual pero eficiente manera
para tratar situaciones como cuando existen recursos que se tienen que
liberar a toda costa en la cual una función toma otra ruta para regresar.
Los ejemplos canónicos son desbloquear un exclusión mutua o cerrar un
archivo.
// Contenido regresa como cadena lo que contiene el archivo.
func Contenido(nombrearchivo string) (string, error) {
f, err := os.Open(nombrearchivo)
if err != nil {
return "", err
}
defer f.Close() // f.Close se ejecutará cuando haya terminado.
var resultado []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
resultado = append(resultado, buf[0:n]...) // append se explica más adelante.
if err != nil {
if err == io.EOF {
break
}
return "", err // f se cerrará si regresamos aquí.
}
}
return string(resultado), nil // f se cerrará si regresamos aquí.
}
Diferir una llamada a una función como Close
tiene dos ventajas.
Primero, garantiza que nunca olvidarás cerrar el archivo, una equivocación
que se comete fácilmente si más tarde editas la función para añadir una
nueva ruta de retorno. Segundo, significa que el Close
se sitúa cerca
del Open
, lo cual es mucho más claro que colocarlo al final de la
función.
Los argumentos de la función diferida (que incluyen al receptor si la función es un método) se evalúan cuando se ejecuta la función diferida, no al invocarla llamada. Más allá de evitar preocupaciones sobre el cambio de valores de las variables conforme se ejecuta la función, esto significa que una sola llamada diferida en el sitio puede posponer la ejecución de múltiples funciones. Aquí tienes un tonto ejemplo.
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
Las funciones diferidas se ejecutan en orden UEPS (último en entrar,
primero en salir), así que este código provocará que se imprima 4 3 2 1
0
cuando la función regrese. Un más plausible ejemplo es una sencilla
manera de rastrear la ejecución de una función a través del programa.
Podríamos escribir un par de sencillas rutinas de rastreo como estas:
func sigue(s string) { fmt.Println("entrando a:", s) }
func abandona(s string) { fmt.Println("dejando:", s) }
// Úsalas de la siguiente manera:
func a() {
sigue("a")
defer abandona("a")
// hace algo....
}
Podemos hacer mejores funciones explotando el hecho de que los argumentos
de las funciones diferidas se evalúan cuando se ejecuta el defer
. La
rutina de rastreo puede configurar el argumento de la rutina que deja de
rastrear. Este ejemplo:
func sigue(s string) string {
fmt.Println("entrando a:", s)
return s
}
func abandona(s string) {
fmt.Println("dejando:", s)
}
func a() {
defer abandona(sigue("a"))
fmt.Println("en a")
}
func b() {
defer abandona(sigue("b"))
fmt.Println("en b")
a()
}
func main() {
b()
}
imprime esto:
entrando a: b
en b
entrando a: a
en a
dejando: a
dejando: b
Para los programadores acostumbrados a administrar recursos a nivel de
bloque de otros lenguajes, defer
les puede parecer extraño, pero sus más
interesantes y potentes aplicaciones vienen precisamente del hecho de que
no se basa en bloques sino que está basado en funciones. En la sección
sobre pánico
y recuperación
veremos otro ejemplo de sus posibilidades.
Datos
Asignación con new
Go tiene dos primitivas de asignación, las funciones incorporadas new
y
make
. Son dos cosas diferentes y aplican a diferentes tipos, lo cual
puede ser confuso, pero las reglas son sencillas. Primero hablemos sobre
new
. Es una función incorporada que reserva memoria, pero a diferencia
de su homónima en algunos otros lenguajes no inicia la memoria, solo
la pone a ceros. Es decir, new(T)
reserva almacenamiento establecido
a cero para un nuevo elemento de tipo T
y regresa su dirección, un valor
de tipo *T
. En terminología Go, regresa un puntero a un recién alojado
valor cero de tipo T
.
Puesto que la memoria devuelta por new
se pone a cero, es útil que el
valor cero de cada tipo se pueda utilizar sin más iniciación para
organizar el diseño de las estructuras de datos. Esto significa para un
usuario de la estructura de datos que puede crear una con new
y
conseguir que trabaje correctamente. Por ejemplo, la documentación para
bytes.Buffer
declara que “el valor de cero para Buffer
es un
búfer vacío listo para utilizarlo”. De modo parecido, sync.Mutex
no
tiene un método constructor explícito o Init
. En cambio, el valor de
cero para un sync.Mutex
está definido que sea un mutex desbloqueado.
El valor cero es una propiedad útil que trabaja transitivamente. Considera esta declaración de tipo.
type BúferSincronizado struct {
cerrado sync.Mutex
búfer bytes.Buffer
}
Los valores de tipo BúferSincronizado
también están listos para
utilizarse inmediatamente solo reservándolos o declarándolos. En el
siguiente fragmento, ambos p
y v
trabajarán correctamente sin más
complicaciones.
p := new(BúferSincronizado) // tipo *BúferSincronizado
var v BúferSincronizado // tipo BúferSincronizado
Constructores y literales compuestos
A veces, el valor cero no es lo suficientemente bueno y es necesario un
constructor para iniciarlo, 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 mucha redundancia allí. Lo podemos simplificar utilizando un literal compuesto, es decir, una expresión que crea una nueva instancia cada vez que es evaluada.
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, es perfectamente válido regresar la dirección de una variable local; el almacenamiento asociado a la variable sobrevive después de regresar de la función. De hecho, al tomar la dirección de un literal compuesto reserva una instancia fresca cada vez que se evalúa, por lo tanto podemos combinar estas últimas dos líneas.
return &File{fd, name, nil, 0}
Los campos de un literal compuesto se muestran en orden y todos deben
estar presentes. Sin embargo, al etiquetar explícitamente los elementos
en pares campo:
valor, la iniciación puede aparecer en cualquier
orden y los que falten se dejan con sus respectivos valores cero. De este
modo podríamos decir:
return &File{fd: fd, name: name}
Como caso límite, si un literal compuesto no contiene campos en absoluto,
este crea un valor cero para el tipo. Las expresiones new(File)
y
&File{}
son equivalentes.
Los literales compuestos también se pueden crear para arreglos, sectores y
mapas, con las etiquetas de campo cómo índices o claves de mapa según
corresponda. En estos ejemplos, la iniciación trabaja independientemente
de los valores de Enone
, Eio
y Einval
, siempre y cuando sean
distintos.
a := [...]string {Enone: "sin error", Eio: "Eio", Einval: "argumento no válido"}
s := []string {Enone: "sin error", Eio: "Eio", Einval: "argumento no válido"}
m := map[int]string{Enone: "sin error", Eio: "Eio", Einval: "argumento no válido"}
Asignación con make
Volviendo a la asignación. La función incorporada make(T,
args)
sirve a un propósito diferente de new(T)
. Esta solamente crea sectores,
mapas y canales y regresa un valor de tipo T (no *T) iniciado
(no
con ceros
). La razón para tal distinción es que estos tres tipos, bajo
la cubierta, representan referencias a estructuras de datos que se tienen
que iniciar antes de usarlas. Un sector, por ejemplo, es un descriptor de
tres elementos que contiene un puntero al dato (dentro de un arreglo), su
longitud y capacidad y hasta que esos elementos sean iniciados, el sector
es nil
. Para sectores, mapas y canales, make
inicia la estructura de
datos interna y prepara el valor para usarlo. Por ejemplo:
make([]int, 10, 100)
reserva memoria para un arreglo de 100 enteros y luego crea una estructura
de sector con longitud 10 y una capacidad de 100 apuntando a los primeros
10 elementos del arreglo. (Cuando creas un sector, la capacidad se puede
omitir; ve la sección sobre sectores para más
información). En contraste, new([]int)
regresa un puntero a una, recién
asignada, estructura de sectores iniciada en ceros, es decir, un puntero a
un sector con valor nil
.
Estos ejemplos ilustran la diferencia entre new
y make
.
var p *[]int = new([]int) // reserva la estructura del sector; *p == nil; raramente útil
var v []int = make([]int, 100) // el sector v ahora se refiere a un nuevo arreglo de 100 ints
// Innecesariamente complejo:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomático:
v := make([]int, 100)
Recuerda que make
solo aplica a mapas, sectores y canales, además de que
no regresa un puntero. Para obtener un puntero explícito asígnalo con
new
o toma la dirección de una variable explícitamente.
Arreglos
Los arreglos son útiles cuándo planeas detallados esquemas de memoria y a veces, pueden ayudar a evitar la asignación, pero principalmente son un bloque de construcción para sectores, el tema de la siguiente sección. Para preparar los fundamentos de ese tema, aquí tienes unas cuantas palabras sobre arreglos.
Hay importantes diferencias entre la manera en que trabajan los arreglos de Go a como lo hacen en C. En Go:
- Los arreglos son valores. Al asignar un arreglo a otro se copian todos los elementos.
- En particular, si pasas un arreglo a una función, esta recibe una copia del arreglo, no un puntero a él.
- El tamaño de un arreglo es parte de su tipo. Los tipos
[10]int
y[20]int
son distintos.
La propiedad valor
puede ser útil pero también muy cara; si quieres el
comportamiento y eficiencia de C, puedes pasar un puntero al arreglo.
func Suma(a *[3]float64) (suma float64) {
for _, v := range *a {
suma += v
}
return
}
arreglo := [...]float64{7.0, 8.5, 9.1}
x := Suma(&arreglo) // Observa el operador de dirección explícito
Pero incluso este estilo no es idiomático de Go. En su lugar usa sectores.
Sectores
Los sectores envuelven arreglos para dotarlos de una interfaz más general, potente y conveniente para secuencias de datos. Salvo los elementos con dimensión explícita tal como arreglos de transformación, la mayoría de la programación de arreglos en Go está hecha con sectores en lugar de arreglos sencillos.
Los sectores mantienen referencias a un arreglo subyacente y si asignas un
sector a otro, ambos se refieren al mismo arreglo. Si una función toma un
sector como argumento, los cambios que hace a los elementos del sector
serán visibles al llamador, análogo a pasar un puntero al arreglo
subyacente. Una función Read
por lo tanto puede aceptar un sector como
argumento en lugar de un puntero y un contador; la longitud dentro del
sector impone un límite máximo de cuantos datos leer. Aquí está la firma
del método Read
del tipo File
en el paquete os
:
func (file *File) Read(buf []byte) (n int, err error)
El método regresa el número de bytes leídos y un valor de error, si lo
hubiera. Para leer los primeros 32 bytes de un búfer buf
más grande, el
sector búfer (aquí utilizado como verbo).
n, err := f.Read(buf[0:32])
Tal sector es común y eficiente. De hecho, dejando aparte —por el momento— la eficiencia, el siguiente fragmento también lee 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]) // Lee un byte.
if nbytes == 0 || e != nil {
err = e
break
}
n += nbytes
}
La longitud de un sector se puede cambiar mientras todavía quepa dentro de
los límites del arreglo subyacente; justo asignándolo a un sector de sí
mismo. La capacidad de un sector, es accesible por medio de la función
incorporada cap
, esta informa la longitud máxima que el sector puede
adoptar. Aquí está una función para añadir datos a un sector. Si el dato
supera la capacidad, el sector es reasignado. El sector resultante es
devuelto. La función utiliza el hecho que len
y cap
son legales
cuando se aplican al sector nil
y regresa 0.
func Append(sector, datos[]byte) []byte {
l := len(sector)
if l + len(datos) > cap(sector) { // reasignado
// Asigna el doble de lo necesario, para futuro crecimiento.
nuevoSector := make([]byte, (l+len(datos))*2)
// La función copy está predeclarada y trabaja para cualquier tipo de sector.
copy(nuevoSector, sector)
sector = nuevoSector
}
sector = sector[0:l+len(datos)]
for i, c := range datos {
sector[l+i] = c
}
return sector
}
Tenemos que regresar al sector más adelante porque, a pesar de que
Append
puede modificar los elementos del sector
, el sector en sí mismo
(la estructura de datos en tiempo de ejecución que contiene el puntero,
longitud y capacidad) es pasado por valor.
La idea de añadir a un sector es tan útil que está capturada por la
función incorporada append
. Para entender el diseño de esa función, sin
embargo, necesitamos un poco más de información, así que regresaremos a
ella más adelante.
Sectores bidimensionales
En Go los arreglos y sectores son unidimensionales. Para crear el equivalente de un arreglo o sector 2D, es necesario definir un arreglo de arreglos o un sector de sectores, de la siguiente manera:
type Transformación [3][3]float64 // Un arreglo 3x3, en realidad un arreglo de arreglos.
type LíneasDeTexto [][]byte // Un sector de sectores de byte.
Debido a que los sectores son arreglos de cierta longitud, es posible
hacer que cada sector interior sea de una longitud diferente. Esta puede
ser una situación común, como en nuestro ejemplo de LíneasDeTexto
: cada
línea tiene una longitud independiente.
texto := LíneasDeTexto{
[]byte("Ahora es el momento"),
[]byte("Para que todos los buenos gophers"),
[]byte("traigan un poco de diversión a la fiesta."),
}
A veces, es necesario reservar un sector 2D, una situación que puede surgir al procesar líneas de píxeles escaneados, por ejemplo. Hay dos maneras en que lo podemos lograr. Una es alojar cada sector independientemente; la otra es alojar un solo arreglo y apuntar a los sectores individuales en él. Cuál usar depende de tu aplicación. Si los sectores pueden crecer o reducirse, los deberías alojar independientemente para evitar sobrescribir la siguiente línea; si no, posiblemente sea más eficiente construir el objeto con una sola asignación. Para referencia, aquí tienes un croquis de los dos métodos. Primero, una línea a la vez:
// Asigna el sector de nivel superior.
foto := make([][]uint8, altura) // Un renglón por unidad de y.
// Recorre los renglones, alojando el sector de cada renglón.
for i := range foto {
foto[i] = make([]uint8, ancho)
}
Y ahora como una asignación, dividido en líneas:
// Asigna el sector de nivel superior, igual a cómo lo hicimos antes.
foto := make([][]uint8, altura) // Un renglón por unidad de y.
// Reserva un gran sector que contiene todos los píxeles.
pixeles := make([]uint8, ancho*altura) // Tiene tipo []uint8 incluso aunque foto es [][]uint8.
// Recorre las filas, dividiendo cada fila desde el frente del sector a los píxeles restantes.
for i := range foto {
foto[i], pixeles = pixeles[:ancho], pixeles[ancho:]
}
Mapas
Los mapas son una conveniente y potente estructura de datos incorporada que asocia los 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 el operador de igualdad esté definido, tal como enteros, números de coma flotante y complejos, cadenas, punteros, interfaces (siempre y cuando la igualdad apoye el tipo dinámico), estructuras y arreglos. Los sectores no se pueden utilizar como claves de mapa, porque la igualdad no está definida en ellos. Como los sectores, los mapas contienen referencias a una estructura de datos subyacente. Si pasas un mapa a una función que cambia el contenido del mapa, los cambios serán visibles en el llamador.
Los mapas se pueden construir utilizando la sintaxis habitual del literal compuesto con pares clave-valor separados por comas, por tanto es fácil iniciarlos durante su construcción.
var zonaHoraria = 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 del mapa se ve sintácticamente igual como se hace para los arreglos y sectores salvo que no es necesario que el índice sea un entero.
offset := zonaHoraria["EST"]
Un intento para recuperar un valor del mapa con una clave que no esté
presente en él regresará el valor cero para el tipo de entrada en el mapa.
Por ejemplo, si el mapa contiene enteros, al buscar una clave inexistente
regresará 0
. Un conjunto se puede implementar como mapa con valor de
tipo lógico
. Pon la entrada del mapa a true
para colocar el valor en
el conjunto y entonces pruébalo por medio de indexación sencilla.
asistio := map[string]bool{
"Ana": true,
"José": true,
...
}
if asistio[persona] { // será false si persona no está en el mapa
fmt.Println(persona, "estaba en la reunión")
}
A veces necesitas distinguir entre una entrada ausente y un valor cero.
¿Hay una entrada para "UTC"
o la cadena está vacía porque no está en el
mapa en absoluto? Lo puedes diferenciar con una forma de asignación
múltiple.
var segundos int
var ok bool
segundos, ok = zonaHoraria[zh]
Por obvias razones a esto se le conoce como el modismo de “coma
ok”. En este ejemplo, si zh
está presente, los segundos
serán
ajustados apropiadamente y ok
será cierto; si no, segundos
se pondrá a
cero y ok
será falso. Aquí está una función que junta todo con un buen
informe de error:
func compensa(zh string) int {
if segundos, ok := zonaHoraria[zh]; ok {
return segundos
}
log.Println("zona horaria desconocida:", zh)
return 0
}
Para probar la presencia en el mapa sin preocuparte del valor real, puedes
utilizar el identificador blanco (_
) en lugar
de la variable habitual para el valor.
_, presente := zonaHoraria[zh]
Para eliminar una entrada del mapa, usa la función incorporada delete
,
cuyos argumentos son el mapa y la clave a eliminar. Es seguro usarla
incluso si la clave ya no existe en el mapa.
delete(zonaHoraria, "PDT") // Ahora en tiempo estándar
Impresión
La impresión formateada en Go usa un estilo similar al de la familia
printf
de C pero es más rica y más general. Estas funciones viven en el
paquete fmt
y tienen nombres capitalizados: fmt.Printf
, fmt.Fprintf
,
fmt.Sprintf
y así por el estilo. Las funciones de cadena (Sprintf
,
etc.) regresan una cadena en lugar de rellenar el búfer proporcionado.
No necesitas proporcionar una cadena de formato. Por cada Printf
,
Fprintf
y Sprintf
hay otro par de funciones, por ejemplo Print
y
Println
. Estas funciones no toman una cadena de formato pero en cambio
generan un formato predefinido para cada argumento. Las versiones
Println
también insertan un espacio entre argumentos y añaden un nuevo
salto de línea al resultado mientras que las versiones Print
solo añaden
espacios si el operando en alguno de los lados es una cadena. En este
ejemplo cada línea produce el mismo resultado.
fmt.Printf("Hola %d\n", 23)
fmt.Fprint(os.Stdout, "Hola ", 23, "\n")
fmt.Println("Hola", 23)
fmt.Println(fmt.Sprint("Hola ", 23))
Las funciones de impresión formateada fmt.Fprint
y amigas toman como
primer argumento cualquier objeto que implemente la interfaz io.Writer
;
las variables os.Stdout
y os.Stderr
son instancias familiares.
Aquí empiezan a divergir las cosas de C. Primero, los formatos numéricos
tal como %d
no toman banderas de signo o tamaño; en cambio, las rutinas
de impresión utilizan el tipo del 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 solo quieres la conversión predefinida, tal como decimal a enteros,
puedes utilizar el formato multipropósito %v
(por “valor”);
el resultado es exactamente el que producirían Print
y Println
.
Además, ese formato puede imprimir cualquier valor, incluso arreglos,
sectores, estructuras y mapas. Aquí tienes una declaración de impresión
para el mapa de husos horarios definido en la sección anterior.
fmt.Printf("%v\n", zonaHoraria) // o solo fmt.Println(zonaHoraria)
La cuál produce:
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
En los mapas, las claves se pueden organizar en cualquier orden, por
supuesto. Cuando imprimas una estructura, el formato modificado %+v
anota los campos de la estructura con sus nombres y para cualquier valor
el formato alterno %#v
imprime el valor en sintaxis Go completa.
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", zonaHoraria)
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, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
(Toma en cuenta los ampersands). La cadena de formato entrecomillada
también está disponible con %q
cuando se aplica a un valor de tipo
string
o []byte
. El formato alterno %#q
en su lugar utilizará
comillas inversas si es posible. (El formato %q
también aplica a
enteros y runes, produciendo una sola constante rune
entrecomillada). Además, %x
trabaja en cadenas, arreglos de bytes y
sectores de bytes así como en enteros, generando una larga cadena
hexadecimal y, con un espacio en el formato (% x
) pone espacios
entre los bytes.
Otro útil formato es %T
, el cual imprime el tipo de un valor.
fmt.Printf("%T\n", zonaHoraria)
imprime:
map[string] int
Si quieres controlar el formato predefinido para un tipo personalizado,
todo lo que se requiere es definir un método con la firma String()
string
en el tipo. Para nuestro sencillo tipo T
, este podría tener
esta apariencia.
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
para imprimir en el formato:
7/-2.35/"abc\tdef"
(Si necesitas imprimir valores de tipo T
así como punteros a *T
, el
receptor para String
tiene que ser de tipo valor; este ejemplo utiliza
un puntero porque es más eficiente e idiomático para tipos estructura. Ve
la sección punteros versus receptores de valor
para más información).
Nuestro método String
es capaz de llamar a Sprintf
porque las rutinas
de impresión son completamente reentrantes y se pueden envolver de este
modo. Sin embargo, hay un importante detalle para entender este enfoque:
no construyas un método String
que llame a Sprintf
en una manera que
se repita indefinidamente tu método String
. Esto puede ocurrir si
Sprintf
intenta llamar a print
para imprimir el receptor como cadena
directamente, el cual a su vez invocará al método de nuevo. Es una fácil
y común equivocación, tal como muestra este ejemplo.
type MiCadena string
func (m MiCadena) String() string {
return fmt.Sprintf("MiCadena=%s", m) // Error: recurrirá indefinidamente.
}
También es fácil corregirlo: convierte el argumento al tipo string básico, el cual no tiene este método.
type MiCadena string
func (m MiCadena) String() string {
return fmt.Sprintf("MiCadena=%s", string(m)) // BIEN: observa la conversión.
}
En la sección de iniciación veremos otra técnica que evita esta recursión.
Otra técnica de impresión es pasar los argumentos de una rutina de
impresión directamente a otra rutina. La firma de Printf
usa el tipo
...interface{}
en su argumento final para especificar que después del
formato puede aparecer una cantidad arbitraria de parámetros (de cualquier
tipo).
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 esta se pasa a otra función con número de
argumentos variable, actúa como una lista de argumentos regular. Aquí
está la implementación de la función log.Println
que utilizamos
arriba. Esta pasa sus argumentos directamente a fmt.Sprintln
para el
formateo real.
// Println imprime al registro estándar a la manera de fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output toma los parámetros (int, string)
}
Escribimos ...
después de v
en la llamada anidada a Sprintln
para
decir al compilador que trate a v
como una lista de argumentos; de lo
contrario solo pasa v
como un solo argumento sector.
Incluso hay mucho más para imprimir de lo que hemos cubierto aquí. Ve la
documentación godoc
del paquete fmt
para más detalles.
Por cierto, un parámetro ...
puede ser de un tipo específico, por
ejemplo ...int
para una función Min
que escoge el menor de una lista
de enteros:
func Min(a ...int) int {
min := int(^uint(0) >> 1) // el int más grande
for _, i := range a {
if i < min {
min = i
}
}
return min
}
Append
Ahora tenemos la pieza perdida que necesitábamos para explicar el diseño
de la función incorporada append
. La firma de append
es diferente de
nuestra anterior función Append
personalizada. Esquemáticamente, es
así:
func append(sector []_T_, elementos ..._T_) []_T_
Dónde T es un marcador de posición para cualquier tipo dado. De hecho,
en Go no puedes escribir una función donde el tipo T
esté determinado
por el llamador. Es por eso que append
está incorporada: necesita
respaldo del compilador.
Lo que hace append
es añadir los elementos hasta el final del sector y
regresar el resultado. Es necesario devolver el resutado porque, como con
nuestra Append
personalizada, el arreglo 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
trabaja un poco como Printf
,
recolectando una arbitraria cantidad de argumentos.
Pero, ¿qué pasa si quisiéramos hacer lo que hace nuestra Append
y anexar
un sector a un sector? Fácil: usa ...
en el sitio de la llamada, tal
como lo hicimos en la llamada a Output
arriba. Este fragmento produce
idéntico resultado que el anterior.
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
Sin esos ...
, no compila porque los tipos serían incorrectos; y
no es
de tipo int
.
Iniciación
A pesar de que superficialmente la iniciación no se ve muy diferente en C o C++, la iniciación en Go es mucho más potente. Puedes construir estructuras complejas durante la iniciación y los problemas de ordenación entre objetos iniciados, incluso entre diferentes paquetes, se manejan correctamente.
Constantes
Las constantes en Go solo son eso —constantes. Estás se crean en
tiempo de compilación, incluso cuando se definan como locales en funciones
y solo pueden ser números, caracteres (runes), cadenas o lógicas. Debido
a la restricción de tiempo de compilación, las expresiones que las definen
tienen que 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 llamada a la función math.Sin
necesita ocurrir en tiempo de ejecución.
En Go, las constantes enumeradas se crean utilizando el enumerador iota
.
Debido a que iota
puede ser parte de una expresión y las expresiones se
pueden repetir implícitamente es fácil construir intrincados conjuntos de
valores.
type MagnitudByte float64
const (
// descarta el primer valor asignándolo al identificador blanco
_ = iota
KB MagnitudByte = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
La habilidad de agregar un método tal como String
a cualquier tipo
definido por el usuario hace posible formatear automáticamente valores
arbitrarios para impresión. A pesar de que a menudo verás aplicada esta
técnica a estructuras, también es útil para tipos escalares como tipos de
coma flotante tal como MagnitudByte
.
func (b MagnitudByte) 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
imprime 1.00YB
, mientras que MagnitudByte(1e13)
imprime 9.09TB
.
Aquí el uso de Sprintf
para implementar el método String
en
MagnitudByte
es seguro (evita recurrir indefinidamente) no debido a una
conversión sino a que se llama a Sprintf
con %f
, que no es una cadena
de formato: Sprintf
solo llamará al método String
cuándo quiere una
cadena y %f
requiere un valor de coma flotante.
Variables
Las variables se pueden iniciar justo como las constantes pero el iniciador 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
Eventualmente, cada archivo fuente puede definir su propia niladica
función init
para configurar cualquier estado requerido. (De hecho,
cada archivo puede tener múltiples funciones init
). Y finalmente
significa que: init
se llama después de que se han evaluado todas las
declaraciones de variables en el paquete y estas son evaluadas solo
después de iniciar todos los paquetes importados.
Más allá de las iniciaciones que no se pueden expresar como declaraciones,
un uso común de las funciones init
es para verificar o reparar opciones
de estado del programa antes de comenzar su ejecución.
func init() {
if usuario == "" {
log.Fatal("$USER no configurado")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath se pudo haber reemplazado por la bandera --gopath en la línea de órdenes.
flag.StringVar(&gopath, "gopath", gopath, "sustituye al GOPATH predefinido")
}
Métodos
Punteros versus valores
Como vimos con MagnitudByte
, los métodos se pueden definir para
cualquier tipo nombrado (excepto un puntero o una interfaz); el receptor
no tiene que ser una estructura.
Anteriormente, en la explicación de sectores, escribimos una
función Append
. En su lugar, la podemos definir como un método en
sectores. Para ello, primero declaramos un tipo nombrado al cual podamos
vincular el método y entonces hacemos que el receptor del método tenga un
valor de ese tipo.
type SectorByte []byte
func (sector SectorByte) Append(datos []byte) []byte {
// Cuerpo exactamente igual que arriba
}
Esto todavía requiere que el método devuelva el sector actualizado.
Podemos eliminar esa torpeza redefiniendo el método para que tome un
puntero a un SectorByte
como su receptor, por tanto el método puede
sobrescribir el sector llamador.
func (p *SectorByte) Append(datos []byte) {
sector := *p
// Cuerpo cómo el de arriba, sin el return.
*p = sector
}
De hecho, incluso lo podemos hacer mejor. Si modificamos nuestra función
para que se parezca al método Write
estándar, de la siguiente manera:
func (p *SectorByte) Write(datos []byte) (n int, err error) {
sector := *p
// Una vez más como arriba.
*p = sector
return len(datos), nil
}
Entonces el tipo *SectorByte
satisface la interfaz del io.Write
estándar, lo cual es práctico. Por ejemplo, podemos imprimir en uno.
var b SectorByte
fmt.Fprintf(&b, "Esta hora tiene %d días\n", 7)
Pasamos la dirección de un SectorByte
porque únicamente un *SectorByte
satisface al io.Writer
. La regla sobre punteros versus valores para
receptores es que los valores de los métodos se puedan invocar en punteros
y valores, pero los métodos del puntero solo se pueden invocar en
punteros.
Esta regla surge porque los métodos del puntero pueden modificar el
receptor; invocándolos con un valor causaría que el método reciba una
copia del valor, así que cualquier modificación sería desechada. El
lenguaje por tanto rechaza esta equivocación. No obstante, hay una útil
excepción. Cuando el valor es direccionable, el lenguaje cuida del caso
común de invocar un método de puntero en un valor insertando
automáticamente el operador de dirección. En nuestro ejemplo, la variable
b
es direccionable, así que podemos llamar a su método Write
justo con
b.Write
. El compilador lo reescribirá por nosotros como (&b).Write
.
Por cierto, la idea de utilizar Write
en un sector de bytes es central
para la implementación de bytes.Buffer
.
Interfaces y otros tipos
Interfaces
Las interfaces en Go proporcionan una manera de especificar el
comportamiento de un objeto: si algo puede hacer este, entonces se puede
utilizar aquí. Ya hemos visto un par de ejemplos sencillos; las
impresiones personalizadas se pueden implementar con un método String
mientras que Fprintf
puede generar su salida a cualquier cosa con un
método Write
. Las interfaces con únicamente uno o dos métodos son
comunes en código Go y normalmente se les otorga un nombre derivado del
método, tal como io.Writer
para algo que implementa Write
.
Un tipo puede implementar múltiples interfaces. Por ejemplo, puedes
ordenar una colección con las rutinas del paquete sort
si este
implementa la sort.Interface
, la cual contiene Len()
, Less(i, j int)
bool
y Swap(i, j int)
, además puede tener un formateador personalizado.
En este ejemplo inventado Secuencia
satisface ambos.
type Secuencia []int
// Métodos requeridos por sort.Interface.
func (s Secuencia) Len() int {
return len(s)
}
func (s Secuencia) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Secuencia) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Método para impresión - ordena los elementos antes de imprimir.
func (s Secuencia) String() string {
sort.Sort(s)
cadena := "["
for i, elem := range s {
if i > 0 {
cadena += " "
}
cadena += fmt.Sprint(elem)
}
return cadena + "]"
}
Conversiones
El método String
de Secuencia
está recreando el trabajo que Sprint
ya hace para sectores. Podemos compartir el esfuerzo si convertimos la
Secuencia
a un sencillo []int
antes de llamar a Sprint
.
func (s Secuencia) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
Este método es otro ejemplo de la técnica de conversión para llamar sin
peligro a Sprintf
desde un método String
. Debido a que los dos tipos
(Secuencia
e []int
) son el mismo si ignoramos el nombre de tipo, es
legal la conversión entre ellos. La conversión no crea un nuevo valor,
solo actúa temporalmente como si el valor existente tuviera un nuevo tipo.
(Hay otras conversiones legales, tal como de entero a coma flotante, esta
sí crea un nuevo valor).
Es un modismo en programas Go convertir el tipo de una expresión para
acceder a un diferente conjunto de métodos. Como ejemplo, podríamos
utilizar el tipo existente sort.IntSlice
para reducir el ejemplo entero
a esto:
type Secuencia []int
// Método para impresión - ordena los elementos antes de imprimirlos.
func (s Secuencia) String() string {
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
Ahora, en vez de dejar que Secuencia
implemente múltiples interfaces
(ordenando e imprimiendo), utilizamos la habilidad de un elemento del dato
para convertirlo a múltiples tipos (Secuencia
, sort.IntSlice
e
[]int
), cada uno de los cuales hace alguna parte del trabajo. Esto es
muy inusual en la práctica pero puede ser eficiente.
Conversión de interfaz y aserción de tipo
Los switch de tipo son una forma de conversión: toman una
interfaz y, para cada caso en el switch, en un sentido lo convierte al
tipo de ese caso. Aquí está una versión simplificada de cómo el código
bajo fmt.Printf
cambia un valor a una cadena utilizando un switch de
tipo. Si ya es una cadena, queremos el valor real de la cadena que tiene
la interfaz, aunque esta tiene un método String
queremos el resultado de
llamar al método.
type Stringer interface {
String() string
}
var valor interface{} // Valor proporcionado por el llamador.
switch cadena := valor.(type) {
case string:
return cadena
case Stringer:
return cadena.String()
}
El primer caso encuentra un valor concreto; el segundo convierte la interfaz a otra interfaz. Es perfectamente legal mezclar tipos de este modo.
¿Qué pasa si solo nos interesa un tipo? ¿Si sabemos que el valor tiene una
cadena
y únicamente la queremos extraer? Un caso del switch de tipo lo
haría, pero también lo haría una aserción de tipo. Una aserción de tipo
toma un valor interfaz y extrae de él un valor del tipo especificado
explícitamente. La sintaxis toma prestado de la cláusula de apertura un
switch de tipo, pero con un tipo explícito en lugar de la palabra clave
type
:
valor.(nombreDeTipo)
y el resultado es un nuevo valor con el tipo estático nombreDeTipo
. Ese
tipo tampoco tiene que ser el tipo concreto soportado por la interfaz, o
un segundo tipo interfaz en que el valor se pueda convertir. Para
extraer la cadena sabemos que está en el valor, podríamos escribir:
cadena := valor.(string)
Pero, si resulta que el valor no contiene una cadena, el programa se detendrá con un error en tiempo de ejecución. Para resguardarte contra eso, usa el modismo "coma, ok" para probar, sin peligro, si el valor es una cadena:
cadena, ok := valor.(string)
if ok {
fmt.Printf("el valor de la cadena es: %q\n", cadena)
} else {
fmt.Printf("el valor no es una cadena\n")
}
Si la aserción de tipo falla, cadena
todavía existe y sera de tipo cadena,
pero esta tendrá el valor cero, una cadena vacía.
Como ilustración de su capacidad, aquí tienes una instrucción if
-else
equivalente al switch de tipo que abrió esta sección.
if cadena, ok := valor.(string); ok {
return cadena
} else if cadena, ok := valor.(Stringer); ok {
return cadena.String()
}
Generalidad
Si un tipo existe solo para implementar una interfaz y no tiene ningún método exportado además de la interfaz, no hay ninguna necesidad de exportar el tipo en sí mismo. Exportar solo la interfaz aclara que es el comportamiento el que importa, no la implementación y que otras implementaciones con diferentes propiedades pueden reflejar el comportamiento del tipo original. También evita la necesidad de repetir la documentación en cada copia de un método común.
En tales casos, el constructor tendría que devolver un valor de interfaz
en lugar del tipo implementado. Por ejemplo, en la bibliotecas hash
tanto crc32.NewIEEE
como adler32.New
devuelven el tipo 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 es afectado por el cambio en el algoritmo.
Un enfoque similar permite que los algoritmos de transmisión cifrada en
varios paquetes crypto
estén separados del bloque de cifradores
encadenados. La interfaz Block
en el paquete crypto/cipher
especifica
el comportamiento de un bloque cifrado, el cual proporciona encriptación
de un solo bloque de datos. Entonces, por analogía con el paquete
bufio
, los paquetes de cifrado que implementan esta interfaz suelen
poder construir cifradores de transmisión, representados por la interfaz
Stream
, sin conocer los detalles del bloque de encriptación.
Las interfaces crypto/cipher
tienen la siguiente apariencia:
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
Aquí está la definición del modo contador (CTR), el cual cambia un bloque cifrado a un flujo cifrado; observa que se abstraen los detalles del bloque cifrado:
// NewCTR devuelve un Stream que cifra/descifra usando el Bloque dado
// en el modo contador. La longitud de iv tiene que ser igual al
// tamaño del bloque.
func NewCTR(block Block, iv []byte) Stream
NewCTR
no solo aplica a un algoritmo de encriptación y fuente de datos
específicos sino a cualquier implementación de la interfaz Block
y a
cualquier Stream
. Debido a que regresa valores de la interfaz,
reemplazando la encriptación CTR con otros modos de encriptación es un
cambio localizado. Las llamadas al constructor se tienen que editar, pero
porque el código circundante tiene que tratar el resultado solo como un
Stream
, no se nota la diferencia.
Interfaces y métodos
Debido a que casi cualquier cosa puede tener métodos adjuntos, casi
cualquier cosa puede satisfacer una interfaz. Un ejemplo ilustrativo está
en el paquete http
, el cual define la interfaz Handler
. Cualquier
objeto que implemente Handler
puede servir peticiones HTTP.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter
en sí mismo es una interfaz que proporciona acceso a los
métodos necesarios para devolver la respuesta al cliente. Estos métodos
incluyen el método Write
estándar, por lo tanto http.ResponseWriter
puede utilizar cualquier io.Writer
. Request
es una estructura que
contiene una representación de la petición del cliente analizada
sintácticamente.
Por brevedad, ignoraremos las peticiones POST y asumiremos que las peticiones HTTP siempre son GET; esta simplificación no afecta la manera en que se configuran los controladores. Aquí tienes una trivial pero completa implementación de un controlador para contar las veces que la página es visitada.
// Sencillo contador del servidor.
type Contador struct {
n int
}
func (cnt *Contador) ServeHTTP(w http.ResponseWriter, req *http.Request) {
cnt.n++
fmt.Fprintf(w, "contador = %d\n", cnt.n)
}
(Manteniendo nuestro tema, observa cómo Fprintf
puede imprimir a un
http.ResponseWriter
). Para referencia, aquí tienes cómo asociar tal
servidor a un nodo en el árbol URL.
import "net/http"
...
cnt := new(Contador)
http.Handle("/contador", cnt)
Pero, ¿por qué hacer que Contador
sea una estructura? Todo lo que
necesitamos es un entero. (El receptor necesita ser un puntero para que
el incremento sea visible al llamador).
// Sencillo contador del servidor.
type Contador int
func (cnt *Contador) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*cnt++
fmt.Fprintf(w, "contador = %d\n", *cnt)
}
¿Qué pasa si tu programa tiene algún estado interno que sea necesario notificar cuándo se haya visitado una página? Vincula un canal a la página web.
// Un canal que envía una notificación en cada visita.
// (Probablemente quieras que el canal se mentenga en disco).
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notificación enviada")
}
Finalmente, digamos que deseas presentar los argumentos /args
utilizados
cuando invocaste el binario del servidor. Es fácil escribir una función
para imprimir los argumentos.
func ArgServer() {
fmt.Println(os.Args)
}
¿Cómo convertimos esto en un servidor HTTP? Podríamos hacer de
ArgServer
un método de algún tipo cuyo valor descartemos, pero hay una
manera más limpia. Puesto que 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:
// El tipo HandlerFunc es un adaptador para permitir el uso de
// funciones normales como controladores HTTP. Si f es una función
// con la firma apropiada, HandlerFunc(f) es un
// objeto controlador que llama a f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP llama a f(c, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
HandlerFunc
es un tipo con un método, ServeHTTP
, por lo tanto los
valores de ese tipo pueden servir peticiones 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 tan diferente, digamos
que, el receptor es un canal y enviamos el método en el canal.
Para hacer de ArgServer
un servidor HTTP, primero lo modificamos para que tenga la firma correcta.
// Argumento servidor.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
ArgServer
ahora tiene la misma firma que HandlerFunc
, por lo tanto se
puede convertir a ese tipo para acceder a sus métodos, tal como
convertimos Secuencia
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 el tipo HandlerFunc
. El servidor
HTTP invocará al método ServeHTTP
de ese tipo, con ArgServer
como el
receptor, el cual en su momento llama a ArgServer
(vía la invocación a
f(c, req)
dentro de HandlerFunc.ServeHTTP
). Entonces los argumentos
serán mostrados.
En esta sección hicimos un servidor HTTP a partir de una estructura, un entero, un canal y una función, todo porque las interfaces solo son conjuntos de métodos, los cuales se pueden definir para (casi) cualquier tipo.
El identificador blanco
Hasta ahora hemos mencionado un par de veces el identificador blanco, en
el contexto de los bucles for
range
y mapas. El
identificador blanco se puede asignar o declarar con cualquier valor de
cualquier tipo, desechando el valor inofensivamente. Es un poco como
escribir al archivo /dev/null
en Unix: este representa un valor de solo
escritura para utilizarlo como marcador de posición donde se necesita una
variable pero el valor real es irrelevante. Tiene usos más allá de lo que
ya hemos visto.
El identificador blanco en asignación múltiple
El uso de un identificador blanco en un bucle for
range
es un caso
especial de una situación general: asignación múltiple.
Si una asignación requiere múltiples valores en el lado izquierdo, pero uno de los valores no será utilizado por el programa, un identificador blanco en el lado izquierdo de la asignación evita la necesidad de crear una variable inútil y aclara el hecho de que el valor será descartado. Por ejemplo, cuándo llamas a una función que regresa un valor y un error, pero solo el error es importante, usa el identificador blanco para desechar el valor irrelevante.
if _, err := os.Stat(ruta); os.IsNotExist(err) {
fmt.Printf("%s no existe\n", ruta)
}
Ocasionalmente verás código que descarte el valor de error para ignorar el error; esta es una práctica terrible. Siempre revisa el error devuelto; este se proporciona por una razón.
// ¡Mal! Este código chocará si la ruta no existe.
ar, _ := os.Stat(ruta)
if ar.IsDir() {
fmt.Printf("%s es un directorio\n", path)
}
Importaciones y variables no utilizadas
Es un error importar un paquete o declarar una variable sin utilizarla. Las importaciones no utilizadas hinchan el programa y entorpecen la compilación, mientras que una variable iniciada pero no utilizada por lo menos es un ciclo del reloj malgastado y quizás un indicativo de un defecto más grave. Cuando un programa está bajo activo desarrollo, no obstante, a menudo surgen importaciones y variables no utilizadas y puede ser molesto eliminarlas solo para acelerar la compilación, únicamente para necesitarlas otra vez más tarde. El identificador blanco proporciona una solución alternativa.
Este programa medio escrito tiene dos importaciones sin usar (fmt
e
io
) y una variable inútil (fd
), por lo tanto no compila, pero sería
bueno ver qué tanto código es correcto.
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("prueba.go")
if err != nil {
log.Fatal(err)
}
// PENDIENE: usar fd.
}
Para silenciar las quejas sobre importaciones no utilizadas, usa un
identificador blanco para referir a un símbolo desde el paquete importado.
De igual modo, asignar la variable fd
no usada al identificador blanco
acalla el error de variable no utilizada. Esta versión del programa sí
compila.
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // Para depuración; eliminar cuando esté hecho.
var _ io.Reader // Para depuración; eliminar cuando esté hecho.
func main() {
fd, err := os.Open("prueba.go")
if err != nil {
log.Fatal(err)
}
// PENDIENE: usar fd.
_ = fd
}
Por convención, las declaraciones globales para silenciar errores de importación deberían venir justo después de las importaciones y estar comentadas, únicamente para facilitar su ubicación y como recordatorio para limpiar las cosas más tarde.
Efectos secundarios de importación
Una importación no utilizada tal como fmt
o io
en el ejemplo anterior
eventualmente se tendría que utilizar o remover: las asignaciones al
identificador blanco se asocian a código que todavía es un trabajo en
progreso. Pero a veces es útil importar un paquete solo por sus efectos
secundarios, sin ningún uso explícito. Por ejemplo, durante tu función
init
, el paquete [net/http/pprof](/pkg/net/http/pprof/)
registra los
controladores HTTP que proporcionan información de depuración. Esta tiene
una API exportada, pero la mayoría de los clientes únicamente necesitan
registrar el controlador y acceder a los datos a través de una página web.
Para importar el paquete solo por sus efectos secundarios, renombra el
paquete al identificador blanco:
import _ "net/http/pprof"
De esta forma aclaras que la importación del paquete se está haciendo solamente por sus efectos secundarios, porque no hay otro uso posible del paquete: en este archivo, este no tiene nombre. (Si lo tuviera y no utilizamos ese nombre, el compilador rechazaría el programa).
Revisando la interfaz
Como vimos en la explicación de las
interfaces arriba, un tipo no necesita
declarar explícitamente qué interfaz implementa. En cambio, un tipo
implementa la interfaz justo al implementar los métodos de la interfaz.
En la práctica, la mayoría de las conversiones de interfaz son estáticas y
por tanto comprobadas en tiempo de compilación. Por ejemplo, al pasar un
*os.File
a una función que espera un io.Reader
no compilará a no ser
que *os.File
implemente la interfaz io.Reader
.
No obstante, algunas revisiones de la interfaz ocurren en tiempo de
ejecución. Hay un ejemplo de esto en el paquete
[encoding/json](/pkg/encoding/json/)
, el cual define una interfaz
[Marshaler](/pkg/encoding/json/#Marshaler)
. Cuando el codificador JSON
recibe un valor que implementa esa interfaz, el codificador invoca los
valores del método marshaling
para convertirlo a JSON en vez de hacer la
conversión estándar. El codificador revisa esta propiedad en tiempo de
ejecución con una
aserción de tipo como esta:
m, ok := val.(json.Marshaler)
Si es necesario únicamente averigua si un tipo implementa cierta interfaz, sin de hecho utilizar la interfaz, quizás como parte de una comprobación de error, usa el identificador blanco para descartar el tipo/valor acertado:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("el valor %v de tipo %T implementa 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 de hecho satisface la
interfaz. Si un tipo —por ejemplo,
[json.RawMessage](/pkg/encoding/json/#RawMessage)
— necesita una
representación JSON personalizada, esta debe implementar json.Marshaler
,
pero no hay conversiones estáticas que provoquen que el compilador
verifique esto automáticamente. Si el tipo falla inadvertidamente para
satisfacer la interfaz, el codificador JSON todavía trabajará, pero no
utilizará la implementación personalizada. Para garantizar que la
implementación es correcta, se puede utilizar en el paquete una
declaración global utilizando el identificador blanco:
var _ json.Marshaler = (*RawMessage)(nil)
En esta declaración, la asignación implica una conversión de un
*RawMessage
a un Marshaler
la cual requiere que *RawMessage
implemente a Marshaler
y que la propiedad sea comprobada en tiempo de
compilación. json.Marshaler
debe cambiar la interfaz, este paquete ya
no compilará y serás notificado sobre qué necesitas actualizar.
El aspecto del identificador blanco en este constructor indica que la declaración existe solo para el tipo que está comprobando, no para crear una variable. No obstante, esto no es para cada tipo que satisface una interfaz. Por convención, tales declaraciones solo se utilizan cuando no hay conversiones estáticas presentes en el código, el cual es un acontecimiento raro.
Incrustando
Go no proporciona la típica, idea conocida de subclases de tipo, pero tiene la habilidad de “tomar piezas prestadas” de una implementación incrustando tipos dentro de una estructura o interfaz.
Una interfaz incrustada es muy sencilla. Ya hemos mencionado antes las
interfaces io.Reader
e io.Writer
; 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 muchas otras interfaces que especifican
objetos que pueden implementar muchos de esos métodos. Por ejemplo, está
io.ReadWriter
, una interfaz que contiene ambos métodos Read
y Write
.
Podríamos especificar io.ReadWriter
al enumerar los dos métodos
explícitamente, pero es más fácil y más sugerente incrustar las dos
interfaces para formar una nueva, de esta manera:
// ReadWriter es la interfaz que combina las interfaces Reader y Writer.
type ReadWriter interface {
Reader
Writer
}
Esto solo dice que: un ReadWriter
puede hacer lo que hace un Reader
y lo que hace un Writer
; es una unión de interfaces incrustadas (las
cuáles tienen que ser conjuntos de métodos disjuntos). Únicamente se
pueden incrustar interfaces en interfaces.
La misma idea básica aplica a estructuras, pero con más implicaciones de
ámbito. El paquete bufio
tiene dos tipos de estructuras, bufio.Reader
y bufio.Writer
, naturalmente, cada cual implementa las interfaces
análogas del paquete io
. Y bufio
también implementa un
escritor/lector con búfer, el cual trabaja combinando un lector y un
escritor en una estructura utilizando la incrustación: esta enumera los
tipos dentro de la estructura pero no les da nombre a los campos.
// ReadWriter almacena punteros a un Lector y a un Escritor.
// Este implementa io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
Los elementos incrustados son punteros a estructuras y naturalmente se
tienen que iniciar para apuntar a estructuras válidas antes de que se
puedan utilizar. La estructura ReadWriter
se podría haber escrito de la
siguiente manera
type ReadWriter struct {
reader *Reader
writer *Writer
}
Pero entonces para promover los métodos de los campos y para satisfacer
las interfaces de io
, también necesitaríamos proporcionar métodos de
reenvío, tal 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 los tipos incrustados vienen gratis, lo cual significa que
bufio.ReadWriter
no solo tiene los métodos bufio.Reader
y
bufio.Writer
, sino que también satisface otras tres interfaces:
io.Reader
, io.Writer
e io.ReadWriter
.
Hay una importante manera en que la incrustación difiere de las subclases.
Al incrustar un tipo, los métodos de ese tipo se vuelven métodos del tipo
externo, pero cuando se invoca el receptor del método es el tipo interno,
no el externo. En nuestro ejemplo, cuando se invoca al método Read
de
un bufio.ReadWriter
, este tiene exactamente el mismo efecto como el
método de reenvío escrito arriba; el receptor es el campo reader
del
ReadWriter
, no el ReadWriter
en sí mismo.
La incrustación también puede ser una simple comodidad. Este ejemplo muestra un campo incrustado junto a un campo nombrado regular.
type Empleo struct {
Orden string
*log.Logger
}
El tipo Empleo
ahora tiene los métodos Log
, Logf
y otros de *log.Logger
. Podríamos haberle dado el nombre de campo Logger
, por supuesto, pero no es necesario hacerlo. Y ahora, una vez iniciado, podemos registrar el Empleo
:
empleo.log("empezando ahora...")
El Logger
es un campo regular de la estructura Empleo
, por lo tanto lo
podemos iniciar en la manera habitual dentro del constructor de Empleo
,
de esta manera:
func NuevoEmpleo(orden string, notario *log.Logger) *Empleo {
return &Empleo{orden, notario}
}
o con un literal compuesto,
empleo := &Empleo{orden, log.New(os.Stderr, "Empleo: ", log.Ldate)}
Si necesitamos referirnos directamente a un campo incrustado, el nombre de
tipo del campo, ignorando el calificador de paquete, sirve como nombre de
campo, tal como lo hace el método Read
de nuestra estructura
ReaderWriter
. Aquí, si necesitamos acceder al *log.Logger
de una
variable empleo
de tipo Empleo
, escribiríamos empleo.Notario
, lo
cual sería útil si quisiéramos refinar los métodos del Logger
.
func (empleo *Empleo) Logf(format string, args ...interface{}) {
empleo.Notario.Logf("%q: %s", empleo.Orden, fmt.Sprintf(format, args...))
}
La incrustación de tipos introduce el problema de conflictos de nombre
pero las reglas para resolverlos son sencillas. Primero, un campo o
método X
oculta cualquier otro elemento X
en una parte anidada más
profunda del tipo. Si log.Logger
contuviera un campo o método llamado
Orden
, el campo Orden
del Empleo
predominaría.
Segundo, si el mismo nombre aparece al mismo nivel de anidamiento,
normalmente es un error; será erróneo incrustar log.Logger
si la
estructura Empleo
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 del tipo, está bien. Esta cualificación proporciona alguna
protección contra cambios hechos a tipos incrustados externos; no hay
ningún problema si se añade un campo que entre en conflicto con otro campo
en otro subtipo si nunca se utiliza ese campo.
Concurrencia
Compartiendo por comunicación
La programación concurrente es un gran tema y este únicamente es un espacio para resaltar algo específico de Go.
La programación concurrente en muchos entornos se dificulta por las sutilezas requeridas para implementar el correcto acceso a variables compartidas. Go estimula un enfoque diferente en el cual los valores compartidos se pasan en canales y, de hecho, nunca se comparten activamente entre hilos de ejecución separados. solo una rutinago tiene acceso al valor en cualquier momento dado. Las pugnas de datos no pueden ocurrir, por diseño. Para animar este modo de pensar lo hemos reducido a un eslogan:
No comunicar por medio de memoria compartida; en su lugar, compartir la memoria por comunicación.
Este enfoque se puede llevar demasiado lejos. La cantidad de referencia se puede hacer poniedo una exclusión mutua en torno a una variable entera, por ejemplo. Pero como enfoque de alto nivel, utilizar canales para controlar el acceso facilita la escritura de programas claros y correctos.
Una manera de pensar sobre este modelo es considerar un típico programa de un solo hilo corriendo en una CPU. Este no necesita primitivas de sincronización. Al correr otra instancia del mismo; tampoco necesita ninguna sincronización. Ahora, si permitimos que las dos se comuniquen; si la comunicación es el sincronizador, allí todavía no hay ninguna necesidad de otra sincronización. Las tuberías de Unix, por ejemplo, encajan en este modelo perfectamente. Aunque el enfoque de concurrencia en Go se origina en los procesos secuenciales de comunicación (PSC), que también se pueden ver como una generalización con seguridad de tipos de las tuberías Unix.
Rutinasgo
Se llaman rutinasgo porque los términos existentes como hilos, corutinas, procesos, etc., expresan connotaciones inexactas. Una rutinago tiene un modelo sencillo: es una función ejecutándose al mismo tiempo que otras rutinasgo en el mismo espacio. Es ligera, puesto que consta de un poco más de espacio que la asignación de la pila. Y la pila el inicio es pequeña, así que son baratas y crecen reservando (y liberando) almacenamiento en el montón cuando se requiere.
Las rutinasgo son multiplexadas en varios hilos del SO por lo que si una se debe bloquear, tal como cuándo espera E/S, las demás siguen funcionando. Su diseño esconde mucha de la complejidad de la creación y administración de hilos.
Prefija una función o llamada a método con la palabra clave go
para
ejecutar la llamada en una nueva rutinago. Cuando la llamada se completa,
la rutinago sale, silenciosamente. (El efecto es similar a la notación
&
del intérprete de ordenes de Unix para ejecutar una orden en segundo
plano).
go list.Sort() // ejecuta list.Sort simultáneamente; no espera a que termine.
Una función literal se puede manejar en una invocación a una rutinago.
func Anuncia(mensaje string, demora time.Duration) {
go func() {
time.Sleep(demora)
fmt.Println(mensaje)
}() // Observa los paréntesis - estos llaman a la función.
}
En Go, las funciones literales son cierres: la implementación se asegura de que las variables referidas por la función sobrevivan mientras están activas.
Estos ejemplos no son demasiado prácticos porque las funciones no tienen ninguna manera de señalar su conclusión. Para hacerlo, necesitamos canales.
Canales
Como los mapas, los canales 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, este establece el tamaño del
búfer para el canal. El predeterminado es cero, para un canal síncrono o
sin búfer.
ci := make(chan int) // canal de enteros sin búfer
cj := make(chan int, 0) // canal de enteros sin búfer
cs := make(chan *os.File, 100) // canal de punteros a Archivos con búfer
Los canales sin búfer combinan el intercambio de comunicación de un valor con sincronización garantizando que dos cálculos (rutinasgo) se encuentran en un estado conocido.
Hay muchos buenos modismos utilizando canales. Aquí está uno para que
podamos comenzar. En la sección anterior lanzamos una clase en segundo
plano. Un canal te puede permitir lanzar una rutinago para esperar que la
orden sort
termine.
c := make(chan int) // Reserva un canal.
// Inicia la orden sort en una rutinago; al completarse, lo señala en el canal.
go func() {
list.Sort()
c <- 1 // Envía una señal; el valor no importa.
}()
hazAlgoMientras()
<-c // Espera a que Sort termine; desecha el valor enviado.
Los receptores siempre se bloquean hasta que hay datos por recibir. Si el canal es sin búfer, el emisor se bloquea hasta que el receptor ha recibido el valor. Si el canal tiene un búfer, el emisor solo se bloquea hasta que el valor se ha copiado al búfer; si el búfer está lleno, significa que tiene que esperar hasta que algún receptor haya recuperado un valor.
Un canal con búfer se puede utilizar como un semáforo, por ejemplo para
limitar la transmisión de datos. En este ejemplo, las peticiones
entrantes se pasan al controlador
, el cual envía un valor al canal, este
procesa la petición y entonces recibe un valor desde el canal para
preparar el “semáforo” para el siguiente consumidor. La
capacidad del búfer del canal limita el número de llamadas simultáneas a
procesar
.
var sem = make(chan int, MaxOutstanding)
func controlador(r *Request) {
sem <- 1 // Espera hasta drenar la cola activa.
procesar(r) // Puede tomar mucho tiempo.
<-sem // Hecho; permite ejecutar la siguiente petición.
}
func Serve(cola chan *Request) {
for {
req := <-cola
go controlador(req) // no espera a que termine el controlador.
}
}
Una vez que los controladores MaxOutstanding
están ejecutando
procesar
, alguno más lo bloqueará tratando de enviar al canal con el
búfer lleno, hasta que uno de los controladores existentes termine y
reciba desde el búfer.
Este diseño tiene un problema, si bien: Serve
crea una nueva rutinago
para cada petición entrante, incluso aunque únicamente MaxOutstanding
de
ellos se puedan ejecutar en cualquier momento. Como resultado, el
programa puede consumir recursos ilimitados si las peticiones entran
demasiado rápido. Podemos tratar esa deficiencia cambiando Serve
a la
entrada de la creación de las rutinasgo. Aquí tienes una solución obvia,
pero ten cuidado esta tiene un fallo que arreglaremos posteriormente:
func Serve(cola chan *Request) {
for req := range cola {
sem <- 1
go func() {
procesar(req) // Lleno de errores; ve la explicación abajo.
<-sem
}()
}
}
El fallo es que en un bucle for
de Go, la variable del bucle se
reutiliza en cada iteración, así que la variable req
es compartida en
todas las rutinasgo. Eso no es lo que deseamos. Necesitamos asegurarnos
que req
es única en cada rutinago. Aquí está una manera de hacerlo,
pasando el valor de req
como un argumento para el cierre en la rutinago:
func Serve(cola chan *Request) {
for req := range cola {
sem <- 1
go func(req *Request) {
procesar(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 solo creando una nueva variable con el mismo nombre, como en este ejemplo:
func Serve(cola chan *Request) {
for req := range cola {
req := req // Crea una nueva instancia de req para la rutinago.
sem <- 1
go func() {
procesar(req)
<-sem
}()
}
}
Puede parecer extraño escribir
req := req
Pero hacerlo en Go es legal e idiomático. Consigues una versión fresca de la variable con el mismo nombre, ocultando deliberadamente la variable del bucle local pero únicamente para cada rutinago.
Volviendo al problema general de escribir el servidor, otro enfoque para
manejar bien los recursos es empezar a corregir una serie de rutinasgo
controladora
s que están leyendo desde el canal de la petición. La
cantidad de rutinasgo límitan el número de llamadas simultáneas a
procesar
. Esta función Serve
también acepta un canal en el cual se
producirá la salida; después de lanzar las rutinasgo estas bloquean la
recepción de ese canal.
func controlador(cola chan *Request) {
for r := range cola {
procesar(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Inicia los controladores
for i := 0; i < MaxOutstanding; i++ {
go controlador(clientRequests)
}
<-quit // Espera a que le digan que salga.
}
Canales de canales
Una de las propiedades más importantes de Go es que un canal es un valor de primera clase que se puede reservar y pasar como cualquier otro. Un uso común de esta propiedad es para implementar la demultiplexión segura y en paralelo.
En el ejemplo de la sección anterior, el controlador
era un controlador idealizado para una petición pero no definimos el tipo que controla. Si ese tipo incluye un canal en el cual responder, cada cliente puede proporcionar su propia ruta para la respuesta.
Aquí tienes 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 canal dentro del objeto petición en el cual recibir la respuesta.
func suma(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, suma, make(chan int)}
// envía la petición
clientRequests <- request
// Espera la respuesta.
fmt.Printf("respuesta: %d\n", <-request.resultChan)
En el lado del servidor, la función controladora es la única cosa que cambia.
func controlador(cola chan *Request) {
for req := range cola {
req.resultChan <- req.f(req.args)
}
}
Allí claramente hay mucho más por mejorar para hacerlo realista, pero este código es una plataforma para un índice limitado en paralelo, un sistema RPC no bloqueante y allí no hay una exclusión mutua a la vista.
Paralelización
Otra aplicación de estas ideas es el cálculo en paralelo en una CPU de múltiples núcleos. Si el cálculo se puede romper en piezas separadas que se puedan ejecutar independientemente, se puede paralelizar, con un canal para señalar cuando cada pieza esté completa.
Digamos que tienes que ejecutar una costosa operación 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
// Aplica la operación a v[i], v[i+1] ... hasta v[n-1].
func (v Vector) HazAlgo(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // señala que esta pieza está terminada
}
Lanzamos las piezas independientemente en un bucle, una por CPU. Estas se pueden completar en cualquier orden pero este no importa; justo contamos las señales de conclusión drenando el canal después de lanzar todas las rutinasgo.
const NCPU = 4 // número de núcleos del CPU
func (v Vector) HazTodo(u Vector) {
c := make(chan int, NCPU) // Con búfer opcional pero perceptible.
for i := 0; i < NCPU; i++ {
go v.HazAlgo(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
}
// Drena el canal.
for i := 0; i < NCPU; i++ {
<-c // espera a que se complete una tarea
}
// Todo terminado.
}
La implementación actual en tiempo de ejecución de Go por omisión no
paraleliza este código. Esta únicamente dedica un solo núcleo para
procesar a nivel de usuario. Una arbitraria cantidad de rutinasgo se
puede bloquear en llamadas al sistema, pero de manera predeterminada solo
una puede ejecutar código a nivel de usuario en un momento dado. Esta
debería ser inteligente pero un día será más lista y hasta entonces si
quieres paralelismo en tu CPU tienes que decirle al tiempo de ejecución
cuantas rutinasgo quieres que ejecuten código simultáneamente. Hay dos
maneras relacionadas de hacer esto. O bien, ejecutas tu trabajo con la
variable de entorno GOMAXPROCS
configurada al número de núcleos a
utilizar o importas el paquete runtime
y llamas a
runtime.GOMAXPROCS(NCPU)
. Un útil valor podría ser runtime.NumCPU()
,
el cual informa el número de CPUs lógicos en la máquina local. Una vez
más, este requisito se espera que sea retirado conforme a lo planificado y
el tiempo de ejecución mejore.
Asegúrate de no confundir las ideas de concurrencia—estructuración de un programa ejecutando componentes independientemente —y paralelismo— ejecutando cálculos en paralelo para eficiencia en múltiples CPUs. A pesar de que las características de concurrencia de Go pueden facilitar la resolución de algunos problemas al estructurar cálculos en paralelo, Go es un lenguaje concurrente, no en paralelo y no todos los problemas de paralelización encajan en el modelo de Go. Para una explicación de la distinción, ve la charla mencionada en esta publicación del blog.
Un búfer agujereado
Las herramientas de la programación concurrente incluso pueden hacer que
las ideas no concurrentes sean más fáciles de expresar. Aquí tienes un
ejemplo extraído de un paquete RPC. La rutinago cliente itera recibiendo
datos de alguna fuente, quizás una red. Para evitar alojar y liberar
búferes, mantiene una lista libre y utiliza un canal con búfer para
representarlos. Si el canal está vacío, se reserva un nuevo búfer. Una
vez que el búfer del mensaje está listo, se envía al servidor en
serverChan
.
var listaLibre = make(chan *Buffer, 100)
var servidorDelCanal = make(chan *Buffer)
func cliente() {
for {
var b *Buffer
// Graba un búfer si está disponible; si no lo reserva.
select {
case b = <-listaLibre:
// Consigue uno; nada más por hacer.
predefinido:
// Ninguno libre, así que reserva uno nuevo.
b = new(Buffer)
}
carga(b) // Lee el siguiente mensaje de la red.
servidorDelCanal <-b // Envía al servidor.
}
}
El bucle del servidor recibe cada mensaje del cliente, lo procesa y regresa el búfer a la lista libre.
func servidor() {
for {
b := <-servidorDelCanal // Espera el trabajo.
procesar(b)
// Reutiliza el búfer si hay espacio.
select {
case listaLibre <- b:
// Búfer en la lista libre; nada más que hacer.
predefinido:
// Lista libre llena, justo te lleva al principio.
}
}
}
El cliente intenta recuperar un búfer desde listaLibre
; si ninguno está
disponible, reserva uno fresco. El servidor lo envía a listaLibre
la
cual pone b
al final en la lista libre a no ser que la lista esté llena,
en cuyo caso se desecha el búfer para ser reclamado por el recolector de
basura. (Las cláusulas default
en las declaraciones select
se
ejecutan cuando ningún otro caso está listo, significa que el select
nunca se bloquea). Esta implementación construye un cubo agujereado de la
lista libre en solo unas cuantas líneas, confiando en que el canal con
búfer y el recolector de basura llevarán la contabilidad.
Errores
Las rutinas de la biblioteca a menudo tienen que regresar alguna clase de
indicación de error al llamador. Anteriormente, cuando mencionamos que,
el multivalor de retorno de Go facilita la devolución de una detallada
descripción de error junto al valor de retorno normal. Es un buen estilo
utilizar esta característica para proporcionar detallada información del
error. Por ejemplo, cuando vemos, os.Open
no solo regresa un puntero
nil
en caso de falla, también regresa un valor de error que describe qué
estuvo mal.
Por convención, los errores tienen el tipo error
, una sencilla interfaz
incorporada.
type error interface {
Error() string
}
Una biblioteca de escritura es libre de implementar esta interfaz con un
modelo más rico bajo la cubierta, haciéndo posible no solo ver el error
sino también para proporcionar algún contexto. Como mencionamos, junto al
habitual valor de retorno *os.File
, os.Open
también regresa un valor
de error. Si el archivo se abre exitosamente, el error será nil
, pero
cuando hay un problema, este tendrá un os.PathError
:
// PathError registra un error, la operación y
// la ruta al archivo que lo causó.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // El archivo asociado.
Err error // Devuelto por la llamada al sistema.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
Los errores
PathError
generan una cadena como esta:
open /etc/passwx: no existe el archivo o directorio
Tal error, incluye el nombre del archivo problemático, la operación y el error del sistema operativo provocado, es útil incluso si se imprime fuera de la llamada que lo causó; es mucho más informativo que el sencillo "no existe el archivo o directorio".
Cuando es factible, las cadenas de error deberían identificar su origen,
tal como cuando tienen un prefijo que nombra la operación o paquete que
generó el error. Por ejemplo, en el paquete image
, la representación de
la cadena para un error de decodificación debido a un formato desconocido
es "imagen: formato desconocido".
A los llamadores que les preocupan los detalles precisos del error pueden
utilizar un switch de tipo o una aserción de tipo para buscar errores
específicos y extraer los detalles. Para PathErrors
esto podría incluir
examinar el campo interno Err
para fallos recuperables.
for try := 0; try < 2; try++ {
archivo, err = os.Create(nombrearchivo)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Recupera algo de espacio.
continue
}
return
}
Aquí la segunda declaración if
es otra
aserción de tipo. Si esta
falla, ok
será falso y e
será nil
. Si tiene éxito, ok
será
cierto, lo cual significa que el error era de tipo *os.PathError
y
también e
lo es, el cual podemos examinar para obtener más información
sobre el error.
Pánico
La manera habitual de informar un error a un llamador es regresando un
error
como un valor de retorno extra. El método canónico Read
es un
caso bien conocido; este regresa un contador de byte y un error
. ¿Pero
qué pasa si el error es irrecuperable? A veces el programa sencillamente
no puede continuar.
Para este propósito, hay una función panic
incorporada que en efecto
crea un error en tiempo de ejecución que detendrá el programa (pero ve la
siguiente sección). La función toma un solo argumento de tipo arbitrario
—a menudo una cadena— para imprimirla cuando el programa
muere. También es una manera de indicar que algo imposible ha sucedido,
tal como salir de un bucle infinito.
// Una ingenua implementación de la raíz cúbica utilizando el método de
// Newton.
func RaízCubica(x float64) float64 {
z := x/3 // Valor inicial arbitrario
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if muyCerca(z, prevz) {
return z
}
}
// Un millón de iteraciones no han convergido; algo está mal.
panic(fmt.Sprintf("La RaízCubica(%g) no convergió", x))
}
Este solo es un ejemplo pero las funciones de una biblioteca real deberían
evitar `panic`. Si el problema se puede enmascarar o hacer funcionar de
otro modo, siempre es mejor dejar que las cosas continúen ejecutándose en
lugar de detener el programa entero. Un posible contraejemplo es durante
el inicio: si la biblioteca verdaderamente no puede arrancar, podría ser
razonable invocar a pánico.
```go
var usuario = os.Getenv("USER")
func init() {
if usuario == "" {
panic("no hay valor para $USER")
}
}
Recuperando
Cuándo se invoca a panic
, incluyendo implícitamente los errores en
tiempo de ejecución tal como la indexación fuera de límites de un sector o
una fallida aserción de tipo, inmediatamente detiene la ejecución de la
función actual y empieza a revertir la pila de la rutinago, ejecutando
cualquier función diferida. Si esa reversión alcanza la parte superior de
la pila en la rutinago, el programa muere. Sin embargo, es posible
utilizar la función incorporada recover
para recobrar el control de la
rutinago y reanudar la ejecución normal.
Una llamada a recover
detiene la reversión y regresa el argumento pasado
a panic
. Debido a que el único código que se ejecuta mientras la
reversión está dentro de las funciones diferidas, recover
solo es útil
dentro de las funciones diferidas.
Una aplicación de recover
es cerrar una rutinago que esté fallando
dentro de un servidor sin matar las otras rutinasgo que se estén
ejecutando.
func servidor(canalTrabajando <-chan *Trabajo) {
for trabajo := range canalTrabajando {
go hazloConSeguridad(trabajo)
}
}
func hazloConSeguridad(trabajo *Trabajo) {
defer func() {
if err := recover(); err != nil {
log.Println("trabajo fallido:", err)
}
}()
haz(trabajo)
}
En este ejemplo, si haz(trabajo)
entra en pánico, el resultado será
registrado y la rutinago saldrá limpiamente sin perturbar a las otras. No
hay necesidad de hacer nada más en el cierre diferido; al invocar a
recover
esta maneja la condición completamente.
Dado querecover
siempre regresa nil
a menos que la llamaras
directamente desde una función diferida, el código diferido puede llamar a
rutinas de la biblioteca en las que ellas mismas utilicen panic
y
recover
sin fallar. Por ejemplo, la función diferida en
hazloConSeguridad
podría llamar a una función de registro antes de
llamar a recover
y el código de registro se ejecutaría sin afectación
por el estado de panic.
Con nuestro patrón de recuperación en su sitio, la función haz
(y
cualquiera que la llame) puede salir limpiamente de cualquier mala
situación al llamar a panic
. Podemos utilizar esa idea para simplificar
el manejo del error en software complejo. Démos una mirada a una versión
idealizada de un paquete regexp
, el cual informa análisis de errores
llamando a panic
con un tipo de error local. Aquí está la definición de
Error
, un método error
y la función Compila
.
// Error es el tipo de un error de analisis; este satisface la interfaz
// error.
type Error string
func (e Error) Error() string {
return string(e)
}
// error es un método de *Regexp que informa los errores del análisis
// entrando en pánico con un Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compila regresa un representación procesada de la expresión regular.
func Compila(cadena string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// hazProceso invoca a panic si hay un error de análisis.
defer func() {
if e := recover(); e != nil {
regexp = nil // Limpia el valor de retorno.
err = e.(Error) // Reinvocará a panic si no un es un error
// de análisis.
}
}()
return regexp.hazProceso(cadena), nil
}
Si hazProceso
invoca a panic
, el bloque de recuperación pondrá el
valor de retorno a nil
—las funciones diferidas pueden modificar
los valores de retorno nombrados. Este entonces comprobará, en la
asignación a err
, que el problema era un error de análisis al asertar
que este tiene el tipo Error
local. Si no, la aserción de tipo fallará,
causando un error en tiempo de ejecución que continúa revirtiendo la pila
como si nada lo hubiera interrumpido. Esta comprobación significa que si
pasa algo inesperado, tal como un índice fuera de límites, el código
fallará incluso aunque estemos utilizando panic
y recover
para manejar
los errores de análisis.
Con el manejo de errores en su sitio, el método error
(debido a que es
un método vinculando al tipo, está bien, incluso es natural, que este
tenga el mismo nombre que el tipo error
incorporado) facilita el
análisis del informe de errores sin preocuparse de revertir a mano la pila
del análisis:
if pos == 0 {
re.error("'*' ilegal al inicio de la expresión")
}
Útil, aunque este patrón se debe utilizar solo dentro de un paquete.
Parse
cambia sus llamadas internas a panic
en valores de error
; no
expone el resultado de panic
a su cliente. Esta es una buena regla a
seguir.
Por cierto, este modismo de rellamar a panic cambia el valor de panic si
ocurre un error real. No obstante, ambas fallas la original y la nueva
serán presentadas en el informe de la colisión, así que la causa raíz del
problema todavía será visible. Por lo que este sencillo enfoque de
rellamar a panic normalmente es suficiente —es una colisión después
de todo— pero si solo quieres mostrar el valor original, puedes
escribir un poco más de código para filtrar problemas inesperados y
rellamar a panic
con el error original. Esto lo dejamos como ejercicio
para el lector.
Un servidor web
Terminaremos con un programa Go completo, un servidor web. Este, de hecho es una clase de reservidor web. Google proporciona un servicio en http://chart.apis.google.com que automáticamente da formato a datos en diagramas y gráficas. Aunque es difícil utilizarlo interactivamente, debido a que necesitas poner los datos en el URL como consulta. El programa proporciona una buena interfaz para un formulario de datos: dada una breve pieza de texto, este llama al servidor de gráficas para producir un código QR, un arreglo de cajas que codifican el texto. Puedes capturar esta imagen con la cámara de tu teléfono celular e interpretarla cómo, por ejemplo, una URL, ahorrándote el tener que escribir la URL en el minúsculo teclado del teléfono.
Aquí está el programa completo. Le sigue una explicación.
package main
import (
"flag"
"html/template"
"log"
"net/http"
)
var dir = flag.String("dir", ":1718", "dirección del servicio http") // Q=17, R=18
var plantilla = template.Must(template.New("qr").Parse(cadenaPlantilla))
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
err := http.ListenAndServe(*dir, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func QR(w http.ResponseWriter, req *http.Request) {
plantilla.Execute(w, req.FormValue("s"))
}
const cadenaPlantilla = `<!doctype html>
<html>
<head>
<title>Generador de enlaces QR</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="Texto QR a codificar">
<input type=submit value="Muestra QR" name=qr>
</form>
</body>
</html>
Te debería ser fácil seguir las piezas hasta main
. Una bandera
configura un puerto HTTP predefinido para nuestro servidor. La variable
plantilla
es dónde empieza la diversión. Esta construye una plantilla
HTML que será ejecutada por el servidor para mostrar la página; más sobre
esto en un momento.
La función main
procesa las banderas y, utilizando el mecanismo del que
hablamos anteriormente, vincula la función QR
a la ruta raíz del
servidor. Luego se invoca a http.ListenAndServe
para arrancar el
servidor; esta se bloquea mientras se ejecuta el servidor.
QR
solo recibe la petición, la cual contiene datos del formulario y
ejecuta la plantilla en los datos con el valor del formulario llamado s
.
El paquete de plantillas html/template
es potente; este programa solo
toca una minúscula parte de su capacidad. En esencia, este reescribe al
vuelo una pieza de texto HTML sustituyendo los elementos derivados desde
los datos pasados a plantilla.Execute
, en este caso el valor del
formulario. En el texto de la plantilla (cadenaPlantilla
), las piezas
delimitadas por dobles llaves denotan acciones de la plantilla. La pieza
desde "{{if .}}"
hasta "{{end}}"
solo se ejecuta si el valor del
elemento de datos actual, llamado .
(punto), no está vacío. Es decir,
cuándo la cadena está vacía, esta pieza de la plantilla se suprime.
Los dos fragmento {{html "{{.}}"}}
dicen que muestre el dato presentado
a la plantilla en la cadena de consulta de la página web. El paquete de
plantillas HTML automáticamente proporciona escape apropiado por lo tanto
es seguro mostrar el texto.
El resto de la cadena de texto de la plantilla solo es el HTML a mostrar
cuando se carga la página. Si esta es una explicación demasiado rápida,
ve la documentación del paquete template
para una
descripción más minuciosa.
Y aquí lo tienes: un útil servidor web en unas cuantas líneas de código más algún dato derivado del texto HTML. Go es lo suficientemente potente para hacer que suceda mucho más en unas cuantas líneas.
En su mayor parte este libro se reproduce a partir del trabajo creado y compartido por Google traducido al Español y se usa de acuerdo a los términos descritos en la Licencia Creative Commons 3.0 Attribution, el código se libera bajo una licencia BSD.