Esta guía es la versión traducida al español de la versión original creada por Uber. Intentaremos mantener esta versión actualizada casi al mismo tiempo que la original. Si queréis contribuir podéis dejar vuestros PRs, pero recordar que siempre tendrán que ser sobre correcciones sobre la traducción o añadir nuevos cambios que no hayamos actualizados.
Además iremos dejando todos los cambios que se vayan realizando en el fichero CHANGELOG.md
- Introducción
- Directrices
- Punteros a interfaces
- Receptores e interfaces
- No es necesario inicializar los Mutexs
- Limitaciones copiando Slices y Maps
- Defer para limpiar
- El tamaño de los Canales es Uno o Ninguno
- Empezando Enums en Uno
- Error Types
- Error Wrapping
- Manejando los errores de Type Assertion
- No Panic
- Usa go.uber.org/atomic
- Evite globales mutables
- Rendimiento
- Estilo
- Se consistente
- Agrupar declaraciones similares
- Agrupaciones y orden en los imports
- Nombre de paquetes
- Nombres de funciones
- Alias en los imports
- Agrupación de funciones y orden
- Reducir el código anidado
- Else innecesario
- Declaración de variables globales
- Prefijo _ para las globales privadas
- Composición en Structs
- Usa el nombre de los campos al inicializar Structs
- Declaración de variables locales
- nil es un slice válido
- Reducir el scope de las variables
- Evitar parámetros planos
- Evita declarar strings con escapados
- Inicializando referencias a Struct
- Inicializando Maps
- Formatos de Strings fuera del Printf
- Nombra funciones al estilo Printf
- Patrones
Los estilos son las convenciones que gobiernan nuestro código. El término estilo aquí es un poco inapropiado, ya que estas convenciones cubren algo más que el formato de nuestros ficheros, del cual ya se encarga sobradamente gofmt
.
La finalidad de esta guía es la de tratar de describir en detalle como se escribe código Go en Uber. Estas reglas han sido creadas para mantener un código manejable, haciendo que los desarrolladores puedan realizar nuevas tareas o funcionalidades de manera más productiva.
Esta guía fue creada originalmente por Prashant Varanasi y Simon Newton como una forma de poner al día a algunos compañeros en Go. Con los años se ha ido mejorando con el feedback que ha ido recibiendo.
Este documento cubre las convenciones idiomáticas en Go que se siguen en Uber. Muchas de éstas son pautas generales para Go, mientras que otras se extienden a recursos externos:
Todo el código debe estar libre de errores cuando se ejecute tanto golint
como go vet
. Recomendamos configurar tu editor para ejecutar:
goimports
al guardargolint
ygo vet
para comprobar errores.
Puedes encontrar más información, en la sección de soporte de editores aquí: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
Casi nunca necesitarás un puntero a una interfaz. Deberías pasar las interfaces como valor, el dato subyacente puede seguir siendo un puntero.
Una interfaz se compone de dos campos:
- Un puntero a información específica del tipo. Puedes pensar en ello como un
type
. - Puntero a datos. Si el dato almacenado es un puntero, será almacenado directamente. Si el dato almacenado es un valor, entonces se almacenará un puntero a su valor.
Si quieres que tu interfaz contenga métodos que puedan modificar los datos subyacentes, tendrás que utilizar un puntero.
Métodos que reciben un receptor por valor, pueden ser llamados tanto como punteros como por valor.
Por ejemplo,
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
sVals := map[int]S{1: {"A"}}
// Puedes llamar a Read usando un valor
sVals[1].Read()
// Producirá un error de compilación:
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// Puedes llamar a ambos Read y Write usando un puntero
sPtrs[1].Read()
sPtrs[1].Write("test")
Del mismo modo, un puntero puede satisfacer una interfaz, incluso si el método tiene un receptor por valor.
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}
var i F
i = s1Val
i = s1Ptr
i = s2Ptr
// El siguiente código no compilará, ya que s2Val es un valor, y no hay método f() que esperen ser llamados por un valor.
// i = s2Val
Effective Go tiene muy buena documentación sobre Punteros vs. Valor.
El valor por defecto de sync.Mutex
y sync.RWMutex
es suficiente, no necesitarás en la mayoría de los casos crear un puntero hacia un mutex
.
Incorrecto | Correcto |
---|---|
mu := new(sync.Mutex)
mu.Lock() |
var mu sync.Mutex
mu.Lock() |
Si usas un struct
como puntero, entonces el mutex
no necesita ser un puntero.
Los structs
privados que usen mutex
para proteger campos del struct
deberían estar compuestos por mutex
.
type smap struct {
sync.Mutex // sólo para tipos privados
data map[string]string
}
func newSMap() *smap {
return &smap{
data: make(map[string]string),
}
}
func (m *smap) Get(k string) string {
m.Lock()
defer m.Unlock()
return m.data[k]
} |
type SMap struct {
mu sync.Mutex
data map[string]string
}
func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
} |
Embed mutex para tipos privados o tipos que necesitan implementar la interfaz Mutex. | Para tipos públicos, usa un campo privado. |
Slices y maps contienen punteros que apuntan a datos subyacentes, así que hay tener cuidado cuando tengamos que copiar sus datos.
Ten presente que un usuario puede modificar un map
o un slice
que recibas como argumento si guardas su referencia.
Incorrecto | Correcto |
---|---|
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// ¿Querías modificar d1.trips?
trips[0] = ... |
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// Ahora podemos modificar trips[0] sin que d1.trips se vea afectado.
trips[0] = ... |
Del mismo modo, ten cuidado con las modificaciones que se realicen a los maps
o slice
que exponen su estado interno.
Incorrecto | Correcto |
---|---|
type Stats struct {
mu sync.Mutex
counters map[string]int
}
// Snapshot devuelve el valor actual de s.counters.
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters
}
// snapshot ya no está protegido por el mutex, por eso
// cualquier acceso al snapshot está sujeto a condiciones de carrera.
snapshot := stats.Snapshot() |
type Stats struct {
mu sync.Mutex
counters map[string]int
}
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
// Snapshot es ahora una copia.
snapshot := stats.Snapshot() |
Utiliza defer
para limpiar y cerrar recursos como ficheros y locks
.
Incorrecto | Correcto |
---|---|
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// es sencillo olvidar realizar unlocks cuando tienes múltiples returns |
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// más legible |
El sobrecoste que tiene defer
es extremadamente pequeño y sólo debe evitarse si puedes
asegurar que el tiempo de ejecución de tu función es de nanosegundos. Se gana mucho más con la legibilidad obtenida utilizando defer
que el minúsculo coste que tiene utilizarlos. Esto se puede observar claramente en métodos largos que tienen más que simples accesos a memoria, dónde los otros cálculos son mucho más significantes que el defer
.
Los canales normalmente deberían ser de tamaño uno o ser unbuffered
. Por defecto, los canales son
unbuffered
y tienen un tamaño de cero. Cualquier otro tamaño debería de ser analizado con mucho detalle.
Incorrecto | Correcto |
---|---|
// ¡Esta inicialización debería ser suficiente para cualquiera!
c := make(chan int, 64) |
// Tamaño de uno
c := make(chan int, 1) // o
// Canal unbuffered, tamaño de cero
c := make(chan int) |
La manera estándar de introducir enums
en Go es declarar un tipo personalizado y un grupo de const
empezando con el valor iota
. Como las variables tienen un valor predeterminado de 0, deberías de empezar tus enums
por un valor que sea distinto de 0.
Incorrecto | Correcto |
---|---|
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2 |
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3 |
Hay casos donde usar el valor cero tiene sentido, como por ejemplo cuando el valor cero es el comportamiento deseado.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Hay varias formas de declarar errores en Go:
errors.New
para errores con un simplestring
estáticofmt.Errorf
para errores con unstring
formateado- Tipos personalizados que implementan la interfaz
error
es decir tienen el métodoError()
- Errores envueltos (aka "wrapeados") usando
"pkg/errors".Wrap
A la hora de devolver errores, hay que considerar ciertos aspecto para tomar la mejor decisión:
- ¿Es un simple error que no necesita información adicional? Entonces,
errors.New
debería ser suficiente. - ¿Los clientes necesitan detectar y manejar este error? Entonces, deberías de utilizar un tipo personalizado que implemente la interfaz
error
. - ¿Estás propagando un error que te ha devuelto otra función?Are you propagating an error returned by a downstream function? Entonces, salta a la sección de error wrapping.
- En otros caso utilizaremos,
fmt.Errorf
.
Si el cliente necesita detectar el error pero no necesita información adicional, utiliza errores sentinelas o sentinel errors
en lugar de devolver la función con errors.New
, como se puede ver a continuación:
Incorrecto | Correcto |
---|---|
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
func use() {
if err := foo.Open(); err != nil {
if err.Error() == "could not open" {
// handle
} else {
panic("unknown error")
}
}
} |
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if err == foo.ErrCouldNotOpen {
// handle
} else {
panic("unknown error")
}
} |
Si tienes un error que los clientes necesitan detectar, y además quieres proporcionar más información, entonces deberías utilizar un tipo personalizado.
Bad | Good |
---|---|
func open(file string) error {
return fmt.Errorf("file %q not found", file)
}
func use() {
if err := open(); err != nil {
if strings.Contains(err.Error(), "not found") {
// handle
} else {
panic("unknown error")
}
}
} |
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func open(file string) error {
return errNotFound{file: file}
}
func use() {
if err := open(); err != nil {
if _, ok := err.(errNotFound); ok {
// handle
} else {
panic("unknown error")
}
}
} |
Ten cuidado con el scope de tus errores de tipos personalizados, ya que si los haces públicos pasaran a ser parte de la API pública del paquete. Es preferible solo exponer una función que compruebe si el error es del tipo deseado, en lugar de exponer tu struct
.
// package foo
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
}
func Open(file string) error {
return errNotFound{file: file}
}
// package bar
if err := foo.Open("foo"); err != nil {
if foo.IsNotFoundError(err) {
// handle
} else {
panic("unknown error")
}
}
Hay tres formas para propagar los errores si una llamada falla:
- Devolver el error original si no necesitas añadir un contexto adicional y quieres mantener el tipo del error original.
- Añadir contexto usando
"pkg/errors".Wrap
para que el mensaje de error proporcione más contexto y"pkg/errors".Cause
pueda ser usado para extraer el error original. - Usa
fmt.Errorf
si los métodos/funciones no necesitan detectar o manejar este caso específico de error.
Se recomienda añadir contexto donde sea posible para que en lugar de tener errores como "connection refused", tengamos algo más útil como "call service foo: connection refused".
Cuando añadimos contexto a los errores devueltos, intentemos mantener el contexto conciso evitando frases como "failed to", que indican lo obvio y se nos acumularán en la pila de mensajes:
Incorrecto | Correcto |
---|---|
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %s", err)
} |
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %s", err)
} |
|
|
Sin embargo es enviado a otro sistema, deberá de dejar claro que el mensaje es un error (ej. un tag err
o un prefijo como "Failed" en los logs).
Un artículo muy interesante sobre el tratamiento de errores en Go: Don't just check errors, handle them gracefully.
Para claridad de esta parte mantendremos el término original en inglés, Type Assertion
en lugar de validación de tipos, ya que está más extendido.
Devolver en una sola linea un type assertion provocará panic
en tipos incorrectos. Por lo tanto, siempre usaremos la anotación "comma ok".
Incorrecto | Correcto |
---|---|
t := i.(string) |
t, ok := i.(string)
if !ok {
// handle the error gracefully
} |
El código que funciona en producción no puede provocar panics
. Los panics
son la mayor fuente de fallos en cascada. Si un ocurre un error, la función tiene que devolver un error y permitir al que esta usando dicha función como manejarlo.
Incorrecto | Correcto |
---|---|
func foo(bar string) {
if len(bar) == 0 {
panic("bar must not be empty")
}
// ...
}
func main() {
if len(os.Args) != 2 {
fmt.Println("USAGE: foo <bar>")
os.Exit(1)
}
foo(os.Args[1])
} |
func foo(bar string) error {
if len(bar) == 0 {
return errors.New("bar must not be empty")
}
// ...
return nil
}
func main() {
if len(os.Args) != 2 {
fmt.Println("USAGE: foo <bar>")
os.Exit(1)
}
if err := foo(os.Args[1]); err != nil {
panic(err)
}
} |
Panic/recover no es una estrategia para controlar errores. Un programa tiene que devolver panic solo cuando sucede algo de lo que no puede recuperarse, como puede ser acceder aun método de un valor nil
. La excepción a esta regla sólo se aplica cuando inicializamos el programa: aquello que haga que nuestro programa no pueda funcionar correctamente en el momento de inicialización deberá abortar la ejecución debido a un panic
.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Incluso en los tests, es preferible usar t.Fatal
o t.FailNow
en lugar de panics
para marcar el test como fallido.
Incorrecto | Correcto |
---|---|
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
} |
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
} |
Las operaciones atómicas con el paquete sync/atomic operan en los tipos sin procesar
(int32
, int64
, etc.) por lo que es fácil olvidar usar operaciones atómicas para leer o modificar las variables.
go.uber.org/atomic añade seguridad de tipo a estas operaciones ocultando el tipo subyacente. Además, incluye un tipo conveniente, el atomic.Bool
.
Incorrecto | Correcto |
---|---|
type foo struct {
running int32 // atomic
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// already running…
return
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // race!
} |
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// already running…
return
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
} |
Evite la mutación de variables globales, en su lugar opte por la inyección de dependencias. Esto se aplica tanto a los punteros de función como a otros tipos de valores.
Bad | Good |
---|---|
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
} |
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
} |
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
} |
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
} |
Aqui se recogen las directrices específicas de rendimiento aplicadas a como realizar ciertas acciones o utilizar ciertas funciones.
Cuando convertimos primitivos a/o strings, strconv
es más rápido que fmt
.
Incorrecto | Correcto |
---|---|
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
} |
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
} |
|
|
No se recomienda crear slice
de byte
a partir de un string de manera repetitiva. En su lugar,
realiza la conversión una vez y captura el resultado.
Incorrecto | Correcto |
---|---|
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
} |
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
} |
|
|
Cuando sea posible, inicializacermos el map
indicando una capacidad aproximada, utilizando la función make()
.
make(map[T1]T2, hint)
Intenta siempre que puedas, proporcionar una capacidad aproximada utilizando make()
,
ajustando su tamaño a la hora de ser inicializado, esto reduce la necesidad de aumentar
el tamaño y las asignaciones del map
a medida que se vayan agregando nuevos elementos.
Ten en cuenta que la capacidad aproximada no impedirá en ningún caso que se puedan asignar
elementos de más aunque ésta haya sido proporcionada.
Incorrecto | Correcto |
---|---|
m := make(map[string]os.FileInfo)
files, _ := ioutil.ReadDir("./files")
for _, f := range files {
m[f.Name()] = f
} |
files, _ := ioutil.ReadDir("./files")
m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
m[f.Name()] = f
} |
|
|
Alguna de las directrices recogidas en este documente pueden ser tratadas de manera objetiva; sin embargo otras son situacionales, contextuales o subjetivas.
Por encima de todo, se consistente.
El código consistente es sencillo de mantener, sencillo de comprender, requiere menos esfuerzo cognitivo y es más sencillo de migrar o actualizar si aparecen nuevas convenciones o funcionalidades o bugs que solucionar.
Por el contrario, tener estilos dispares o conflictivos entre si en un solo repositorio de código, causará sin duda alguna un sobrecoste de mantenimiento, que afectará de manera significativa a la velocidad, code reviews y creación de nuevas funcionalidades y corrección de errores.
Cuando apliques estas directrices en tu código, es recomendable que los cambios sean hechos a nivel de paquete (o en todo el código), de otro modo la aplicación a nivel de sub paquetes violará la preocupación anterior al introducir múltiples estilos en el mismo código.
Go soporta la agrupación de declaraciones similares.
Incorrecto | Correcto |
---|---|
import "a"
import "b" |
import (
"a"
"b"
) |
Esto también aplica a variables, constantes y declaraciones de tipo.
Incorrecto | Correcto |
---|---|
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64 |
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
) |
Sólo agruparemos aquellas declaraciones que estén estrechamente cohesionadas.
Incorrecto | Correcto |
---|---|
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
ENV_VAR = "MY_ENV"
) |
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const ENV_VAR = "MY_ENV" |
Las agrupaciones no tienen limitaciones en cuanto a donde pueden ser usadas. Podemos utilizarlas incluso dentro de una función.
Incorrecto | Correcto |
---|---|
func f() string {
var red = color.New(0xff0000)
var green = color.New(0x00ff00)
var blue = color.New(0x0000ff)
...
} |
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
} |
Debería haber siempre sólo dos grupos de imports
:
- Paquete estándar
- Todo lo demás
Esta es la forma que aplica goimports
por defecto.
Incorrecto | Correcto |
---|---|
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
Cuando nombramos un paquete, debemos seguir las siguientes normas:
- Será completamente en minúscula. No habrá ninguna mayúscula para separar palabras ni guiones bajo.
- No debería de necesitar el uso de alias cuando sea importado.
- Debe ser corto y conciso. Recuerda que ese nombre será el identificador del paquete en cada llamada.
- No usar pluares. Por ejemplo,
net\url
, nonet\urls
. - No utilizar nombres de paquetes como,
common
,util
,shared
, olib
. Son nombres malos y que no dicen nada.
Ver también Package Names y Style guideline for Go packages.
Seguimos las pautas de la comunidad de Go MixedCaps for function names. La única excepción es para las funciones de los test, las cuales pueden contener guiones bajos con el propósito agrupar los casos de los tests. ej., TestMyFunction_WhatIsBeingTested
.
Los alias para los imports sólo deben ser usados si el nombre no coincide con el último elemento de la ruta importada.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
En todos los demás escenarios, los alias para los imports deben ser evitados a no ser que haya un conflicto directo entre los paquetes.
Incorrecto | Correcto |
---|---|
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
) |
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
) |
- Las funciones deben agruparse por proximidad de llamada.
- Las funciones de un fichero deben agruparse por su receptor.
Por lo tanto, las funciones públicas aparecerán al principio del fichero, después de las definiciones de struct
, const
, var
.
El constructor newXYZ()
/NewXYZ()
debe aparecer después de que el tipo sea definido, pero antes del resto de métodos del receptor.
Como las funciones están agrupadas por receptor, las funciones de tipo helper deberían aparecer al final del fichero.
Incorrecto | Correcto |
---|---|
func (s *something) Cost() {
return calcCost(s.weights)
}
type something struct{ ... }
func calcCost(n []int) int {...}
func (s *something) Stop() {...}
func newSomething() *something {
return &something{}
} |
type something struct{ ... }
func newSomething() *something {
return &something{}
}
func (s *something) Cost() {
return calcCost(s.weights)
}
func (s *something) Stop() {...}
func calcCost(n []int) int {...} |
Tenemos que tener un código con poca anidación, donde pondremos el control de errores y condiciones de validación al principio provocando un return temprano o continuar un bucle. Reduciendo el número de código anidado a varios niveles.
Incorrecto | Correcto |
---|---|
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
} |
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
} |
Si una variable es asignada tanto en el if
como en el else
, esto podrá ser remplazado por un único if
.
Incorrecto | Correcto |
---|---|
var a int
if b {
a = 100
} else {
a = 10
} |
a := 10
if b {
a = 100
} |
Las variables globales usan la palabra reservada var
. No es necesario especificar el tipo, a no ser que sea una declaración de un tipo diferente que la expresión.
Incorrecto | Correcto |
---|---|
var _s string = F()
func F() string { return "A" } |
var _s = F()
// Como F ya indica que devuelve una cadena, no hace falta especificar
// el tipo de nuevo.
func F() string { return "A" } |
Especifica el tipo si la expresión no es exactamente el tipo que buscas.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F returns an object of type myError but we want error.
Todas las variables, var
y constantes, const
globales irán acompañadas del prefijo _
para clarificar cuando se usan que son globales.
Excepción: los errores privados, deben ir acompañados del prefijo err
.
Justificación: Las variables y constantes globales tienen el scope del paquete. Usando un nombre muy genérico se pueden provocar accidentes muy fácilmente.
Incorrecto | Correcto |
---|---|
// foo.go
const (
defaultPort = 8080
defaultUser = "user"
)
// bar.go
func Bar() {
defaultPort := 9090
...
fmt.Println("Default port", defaultPort)
// No veremos error de compilación si la primera línea de
// Bar() es borrada.
} |
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
) |
Embedded types (como los mutexs) deben estar al principio de la lista del struct
, y deben añadir un salto de línea para separarlos de los campos regulares del struct
.
Incorrecto | Correcto |
---|---|
type Client struct {
version int
http.Client
} |
type Client struct {
http.Client
version int
} |
Debes especificar los campos del struct
cuando vayas a inicializarlo. Además esto es ahora de cumplimiento obligatorio utilizando go vet
.
Incorrecto | Correcto |
---|---|
k := User{"John", "Doe", true} |
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
} |
Excepción: Puedes omitir inicializar un struct
con el nombre de sus campos en los test tables cuando tengan 3 o menos campos.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
La forma corta de declaración de variables (:=
) debe ser usada si una variable se le va a asignar un valor explícito.
Incorrecto | Correcto |
---|---|
var s = "foo" |
s := "foo" |
Sin embargo, hay algunos casos donde el valor por defecto queda más claro utilizando var
. Declarando Slices vacíos, por ejemplo.
Incorrecto | Correcto |
---|---|
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
} |
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
} |
nil
es un slice
válido de tamaño 0. Eso quiere decir que,
-
No debes devolver un
slice
de tamaño 0 explícitamente. Devuelvenil
en su lugar.Incorrecto Correcto if x == "" { return []int{} }
if x == "" { return nil }
-
Para comprobar si un
slice
está vacío, siempre usaremoslen(s) == 0
. No comprobaremos si esnil
.Incorrecto Correcto func isEmpty(s []string) bool { return s == nil }
func isEmpty(s []string) bool { return len(s) == 0 }
-
Cuando declaramos un slice con
var
, es decir declararlo a su valor inicial, éste es usable inmediatamente sin necesidad demake()
.Incorrecto Correcto nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
Cuando sea posible, reducir el scope de las variables. No hay que reducir el scope si entramos en conflicto con la regla Reducir el código anidado.
Incorrecto | Correcto |
---|---|
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
} |
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
} |
Si necesitas un resultado de una función fuera del if
, entonces no debes de intentar reducir el scope.
Incorrecto | Correcto |
---|---|
if data, err := ioutil.ReadFile(name); err == nil {
err = cfg.Decode(data)
if err != nil {
return err
}
fmt.Println(cfg)
return nil
} else {
return err
} |
data, err := ioutil.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil |
Los parámetros planos en una función pueden dificultar la legibilidad. Para solucionar esto añadiremos los comentarios al estilo C
, (/* ... */
), para añadir el nombre de los parámetros cuando estos no sean obvios.
Incorrecto | Correcto |
---|---|
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true) |
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */) |
Todavía mejor, si remplazamos el bool
por un tipo personalizado más legible y seguro. Además esto nos permitirá añadir nuevos estados que sólo (true
/false
) en el futuro.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady = iota + 1
StatusDone
// Maybe we will have a StatusInProgress in the future.
)
func printInfo(name string, region Region, status Status)
Go soporta los llamados raw string literals,
los cuales permiten múltiples lineas y comillas, usa este tipo de string
para evitar tener que escapar tu cadena y que la haga más complicada de leer.
Incorrecto | Correcto |
---|---|
wantError := "unknown name:\"test\"" |
wantError := `unknown error:"test"` |
Usa siempre &T{}
en lugar de new(T)
cuando realices una inicialización por referencia de un struct
a modo de ser consistente con la inicialización del struct
.
Incorrecto | Correcto |
---|---|
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar" |
sval := T{Name: "foo"}
sptr := &T{Name: "bar"} |
Elige make(..)
para maps
vacíos, y maps
que sean llenados programáticamente.
Esto hace que la inicialización del map
sea distinta de la declaración visualmente, haciendo que sea más fácil agregar capacidad en un futuro si fuera necesario.
Incorrecto | Correcto |
---|---|
var (
// m1 is safe to read and write;
// m2 will panic on writes.
m1 = map[T1]T2{}
m2 map[T1]T2
) |
var (
// m1 is safe to read and write;
// m2 will panic on writes.
m1 = make(map[T1]T2)
m2 map[T1]T2
) |
La declaración y la inicialización es visualmente similar. |
La declaración y la inicialización es visualmente distinta. |
Donde sea posible, añade la capacidad cuando inicialices los maps
con make()
. Más información: Especificar una capacidad aproximada al Map.
Por otro lado, si el map
tiene un tamaño fijo de elementos, declara literalmente el map
al inicializarlo.
Incorrecto | Correcto |
---|---|
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3 |
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
} |
La regla de oro es utilizar la declaración literal de los maps
, cuando añadas un grupo fijo de elementos en el momento de inicialización, de otro modo usa make
(y especifica la capacidad si es posible).
Si declaras un formato de string
con el estilo del Printf
fuera de una cadena, entonces debes hacerlo con una constante.
Esto ayudará a go vet
a realizar un análisis estático del formato del string
.
Incorrecto | Correcto |
---|---|
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2) |
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2) |
Cuando declares una función de estilo Printf
, asegúrate de que go vet
pueda detectarla y comprobar el formato del string
.
Esto quiere decir que debes usar los nombres al estilo Printf
si es posible. go vet
comprobará esto por defecto. Más información: Printf family
Si no puedes utilizar los nombres predifinidos, acaba tus funciones con f
: Wrapf
, no Wrap
. A go vet
se le puede especificar que compruebe como funciones de estilo Printf
, aquellas funciones que su nombre acabe por f
.
$ go vet -printfuncs=wrapf,statusf
Más información: go vet: Printf family check.
Usa table-driven tests con subtests para evitar la duplicación de código cuando la lógica del test es repetitiva.
Incorrecto | Correcto |
---|---|
// func TestSplitHostPort(t *testing.T)
host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port)
host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port)
host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port)
host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port) |
// func TestSplitHostPort(t *testing.T)
tests := []struct{
give string
wantHost string
wantPort string
}{
{
give: "192.0.2.0:8000",
wantHost: "192.0.2.0",
wantPort: "8000",
},
{
give: "192.0.2.0:http",
wantHost: "192.0.2.0",
wantPort: "http",
},
{
give: ":8000",
wantHost: "",
wantPort: "8000",
},
{
give: "1:8",
wantHost: "1",
wantPort: "8",
},
}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
host, port, err := net.SplitHostPort(tt.give)
require.NoError(t, err)
assert.Equal(t, tt.wantHost, host)
assert.Equal(t, tt.wantPort, port)
})
} |
Test tables hace mucho más sencillo añadir contexto a mensajes de errores, reduce la lógica duplicada y añade nuevos casos de test.s.
Seguimos la convención de el slice
de structs
se le llamará tests
y cada caso de test tt
. Además explicitamos los valores de input(entrada) y output(salida) de cada test con los prefijos give
y want
.
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
Functional options es un patrón donde declaras un tipo Option
que almacena la información en algún struct
interno. Puedes aceptar un número variable de estas opciones y actuar según la información almacenada por las options
en el struct
interno.
Es recomendable utilizar este patrón para argumentos opcionales en los constructores y otras funciones públicas de tu API cuando prevés la necesidad de expandirlos, especialmente si ya tienes tres o más argumentos en dicha función.
Bad | Good |
---|---|
// package db
func Open(
addr string,
cache bool,
logger *zap.Logger
) (*Connection, error) {
// ...
} |
// package db
type Option interface {
// ...
}
func WithCache(c bool) Option {
// ...
}
func WithLogger(log *zap.Logger) Option {
// ...
}
// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
// ...
} |
Los parámetros de db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log) |
Las opciones son proporcionadas solo si son necesarias. db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
addr,
db.WithCache(false),
db.WithLogger(log),
) |
La forma que nosotros sugerimos de implementar este patrón es con una interfaz Option
que contiene un método no exportado, grabando las opciones en un struct no exportado
de nombre options
.
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
Tenga en cuenta que existe un método para implementar este patrón con closures
, pero
creemos que el patrón anterior proporciona más flexibilidad para los autores y es
más fácil de depurar y probar para los usuarios. En particular, permite que las opciones sean
comparadas entre sí en test
y mocks
, versus closures
donde esto es
imposible. Además, permite que las opciones implementen otras interfaces, incluidas
fmt.Stringer
que permite representaciones de string
legibles por el usuario.
Recomendamos mirar también,