Directrices básicas de estándares de codificación Golang
Utilizar defer para liberar recursos
Utilice defer para liberar recursos como archivos y bloqueos.
No recomendado:
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// Es fácil olvidar desbloquear cuando hay múltiples ramas de retorno
Recomendado:
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// Más legible
El costo de utilizar defer es extremadamente bajo, por lo que solo debería evitarse cuando se pueda demostrar que el tiempo de ejecución de la función está en el nivel de nanosegundos. Utilizar defer para mejorar la legibilidad vale la pena porque el costo de usarlo es insignificante. Esto es especialmente aplicable a métodos más grandes que involucran más que simples accesos a memoria, donde el consumo de recursos de otros cálculos supera con creces el de defer
.
El tamaño del canal debe ser 1 o sin búfer
Los canales típicamente deben tener un tamaño de 1 o ser sin búfer. Por defecto, los canales son sin búfer con un tamaño de cero. Cualquier otro tamaño debe ser estrictamente revisado. Necesitamos considerar cómo determinar el tamaño, considerar qué evita que el canal escriba bajo cargas altas y cuando está bloqueado, y considerar qué cambios ocurren en la lógica del sistema cuando esto sucede.
No recomendado:
// ¡Debería ser suficiente para manejar cualquier situación!
c := make(chan int, 64)
Recomendado:
// Tamaño: 1
c := make(chan int, 1) // o
// Canal sin búfer, tamaño es 0
c := make(chan int)
Los enums comienzan desde 1
El método estándar de introducir enums en Go es declarar un tipo personalizado y un grupo constante que usa iota. Dado que el valor predeterminado de las variables es 0, los enums típicamente deben comenzar con un valor distinto de cero.
No recomendado:
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
Recomendado:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
En algunos casos, tiene sentido usar el valor cero (enums que comienzan desde cero), por ejemplo, cuando el valor cero es el comportamiento predeterminado ideal.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Utilizar atomic
Utilice operaciones atómicas del paquete sync/atomic para operar en tipos primitivos (int32
, int64
, etc.) porque es fácil olvidar usar operaciones atómicas para leer o modificar variables.
go.uber.org/atomic agrega seguridad de tipos a estas operaciones al ocultar el tipo subyacente. Además, incluye un conveniente tipo atomic.Bool
.
Enfoque no recomendado:
type foo struct {
running int32 // atómico
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// ¡ya en ejecución!
return
}
// comenzar el Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // ¡carrera!
}
Enfoque recomendado:
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// ¡ya en ejecución!
return
}
// comenzar el Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
}
Evitar variables globales mutables
Utilice el enfoque de inyección de dependencias para evitar cambiar variables globales. Esto es aplicable tanto para punteros a funciones como para otros tipos de valores.
Enfoque no recomendado 1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
Enfoque recomendado 1:
// 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)
}
Enfoque no recomendado 2:
// 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))
}
Enfoque recomendado 2:
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
Evitar el uso de identificadores predeclarados
La Especificación del lenguaje Go describe varios identificadores predeclarados que no deben usarse en proyectos Go. Estos identificadores predeclarados no deben reutilizarse como nombres en diferentes contextos, ya que hacerlo ocultará los identificadores originales en el ámbito actual (o cualquier ámbito anidado), lo que potencialmente conducirá a la confusión del código. En el mejor de los casos, el compilador generará un error; en el peor de los casos, dicho código puede introducir errores potenciales difíciles de recuperar.
Práctica no recomendada 1:
var error string
// `error` sombrea implícitamente el identificador incorporado
// o
func handleErrorMessage(error string) {
// `error` sombrea implícitamente el identificador incorporado
}
Práctica recomendada 1:
var errorMessage string
// `error` ahora apunta al identificador incorporado no sombreado
// o
func handleErrorMessage(msg string) {
// `error` ahora apunta al identificador incorporado no sombreado
}
Práctica no recomendada 2:
type Foo struct {
// Aunque estos campos técnicamente no hacen sombra, redefinir las cadenas `error` o `string` ahora se vuelve ambiguo.
error error
string string
}
func (f Foo) Error() error {
// `error` y `f.error` son visualmente similares
return f.error
}
func (f Foo) String() string {
// `string` y `f.string` son visualmente similares
return f.string
}
Práctica recomendada 2:
type Foo struct {
// `error` y `string` ahora son explícitos.
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
Tenga en cuenta que el compilador no generará errores al usar los identificadores predeclarados, pero herramientas como go vet
señalarán correctamente estos y otros problemas relacionados implícitamente.
Evite usar init()
Trate de evitar usar init()
tanto como sea posible. Cuando init()
sea inevitable o preferible, el código debe tratar de:
- Asegurar la completitud independientemente del entorno del programa o la llamada.
- Evitar depender del orden o efectos secundarios de otras funciones
init()
. Aunque el orden deinit()
es explícito, el código puede cambiar, por lo que la relación entre las funcionesinit()
puede hacer que el código sea frágil y propenso a errores. - Evitar acceder o manipular estados globales o ambientales, como información de la máquina, variables de entorno, directorios de trabajo, parámetros/entradas del programa, etc.
- Evitar I/O, incluidos sistemas de archivos, redes y llamadas al sistema.
El código que no cumpla con estos requisitos puede pertenecer a parte de la llamada main()
(o en otro lugar del ciclo de vida del programa) o puede escribirse como parte de main()
en sí. En particular, las bibliotecas destinadas a ser utilizadas por otros programas deben prestar especial atención a la completitud en lugar de realizar "magia de init".
Enfoque no recomendado 1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
Enfoque recomendado 1:
var _defaultFoo = Foo{
// ...
}
// o, para una mejor testabilidad:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
Enfoque no recomendado 2:
type Config struct {
// ...
}
var _config Config
func init() {
// Incorrecto: basado en el directorio actual
cwd, _ := os.Getwd()
// Incorrecto: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
Enfoque recomendado 2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// manejar error
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// manejar error
var config Config
yaml.Unmarshal(raw, &config)
return config
}
Dadas las consideraciones anteriores, en algunos casos, init()
puede ser más preferible o necesario, incluyendo:
- No puede ser representado como una sola asignación de una expresión compleja.
- Hooks insertables, como
database/sql
, registros de tipos, etc.
Prefiera especificar la capacidad de la lista al agregar
Siempre priorice especificar un valor de capacidad para make()
al inicializar una lista que se va a agregar.
Enfoque no recomendado:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Enfoque recomendado:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Uso de etiquetas de campo en la serialización de estructuras
Cuando se serializa a JSON, YAML, u otro formato que soporta el nombramiento de campos basado en etiquetas, se deben utilizar etiquetas relevantes para la anotación.
No recomendado:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
Recomendado:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Seguro cambiar Name a Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
En teoría, el formato de serialización de una estructura es un contrato entre diferentes sistemas. Realizar cambios en la forma de serialización de la estructura (incluidos los nombres de los campos) romperá este contrato. Especificar nombres de campos en etiquetas hace que el contrato sea explícito y también ayuda a prevenir violaciones accidentales del contrato a través de refactorizaciones o renombramientos de campos.