Introducción
Al crear un paquete en Go, el objetivo final suele ser hacer que el paquete esté disponible para que otros desarrolladores lo utilicen, ya sea en paquetes de orden superior o en programas completos. Al importar el paquete, tu fragmento de código puede servir como bloque de construcción para otras herramientas más complejas. Sin embargo, solo ciertos paquetes están disponibles para su importación. Esto se determina por la visibilidad del paquete.
Visibilidad en este contexto se refiere al espacio de archivo desde el cual se puede hacer referencia a un paquete u otro constructo. Por ejemplo, si definimos una variable dentro de una función, la visibilidad (alcance) de esa variable es solo dentro de la función en la que se definió. Del mismo modo, si defines una variable en un paquete, puedes hacerla visible solo para ese paquete, o permitir que sea visible fuera del paquete también.
Controlar cuidadosamente la visibilidad de los paquetes es importante al escribir código ergonómico, especialmente al considerar cambios futuros que desees realizar en tu paquete. Si necesitas corregir un error, mejorar el rendimiento o cambiar la funcionalidad, querrás hacer el cambio de una manera que no rompa el código de nadie que use tu paquete. Una forma de minimizar los cambios drásticos es permitir el acceso solo a las partes de tu paquete que se necesitan para su uso adecuado. Al limitar el acceso, puedes hacer cambios internos en tu paquete con menos posibilidades de afectar cómo otros desarrolladores están utilizando tu paquete.
En este artículo, aprenderás cómo controlar la visibilidad de los paquetes, así como cómo proteger partes de tu código que solo deberían ser utilizadas dentro de tu paquete. Para ello, crearemos un logger básico para registrar y depurar mensajes, utilizando paquetes con diferentes grados de visibilidad de elementos.
Requisitos previos
Para seguir los ejemplos de este artículo, necesitarás:
- Un espacio de trabajo de Go configurado siguiendo Cómo Instalar Go y Configurar un Entorno de Programación Local. Este tutorial utilizará la siguiente estructura de archivos:
.
├── bin
│
└── src
└── github.com
└── gopherguides
Elementos Exportados y No Exportados
A diferencia de otros lenguajes de programación como Java y Python que utilizan modificadores de acceso como public
, private
, o protected
para especificar el alcance, Go determina si un elemento es exportado
y no exportado
a través de cómo se declara. Exportar un elemento en este caso lo hace visible
fuera del paquete actual. Si no se exporta, solo es visible y utilizable desde dentro del paquete en el que se definió.
Esta visibilidad externa se controla mediante el uso de mayúsculas en la primera letra del elemento declarado. Todas las declaraciones, como Tipos
, Variables
, Constantes
, Funciones
, etc., que comienzan con una letra mayúscula son visibles fuera del paquete actual.
Veamos el siguiente código, prestando especial atención a la capitalización:
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
Este código declara que está en el paquete greet
. Luego declara dos símbolos, una variable llamada Greeting
y una función llamada Hello
. Debido a que ambos comienzan con una letra mayúscula, ambos son exportados
y disponibles para cualquier programa externo. Como se mencionó anteriormente, crear un paquete que limite el acceso permitirá un mejor diseño de API y facilitará la actualización de su paquete internamente sin romper el código de nadie que dependa de su paquete.
Definiendo la Visibilidad del Paquete
Para dar un vistazo más cercano a cómo funciona la visibilidad del paquete en un programa, creemos un paquete logging
, teniendo en cuenta lo que queremos hacer visible fuera de nuestro paquete y lo que no haremos visible. Este paquete de logging será responsable de registrar cualquier mensaje de nuestro programa en la consola. También examinará en qué nivel estamos registrando. Un nivel describe el tipo de registro y será uno de tres estados: info
, warning
o error
.
Primero, dentro de tu directorio src
, creemos un directorio llamado logging
para colocar nuestros archivos de logging:
Muévete a ese directorio a continuación:
Luego, usando un editor como nano, crea un archivo llamado logging.go
:
Coloca el siguiente código en el archivo logging.go
que acabamos de crear:
La primera línea de este código declara un paquete llamado logging
. En este paquete, hay dos funciones exportadas
: Debug
y Log
. Estas funciones pueden ser llamadas por cualquier otro paquete que importe el paquete logging
. También hay una variable privada llamada debug
. Esta variable solo es accesible desde dentro del paquete logging
. Es importante notar que aunque la función Debug
y la variable debug
tienen la misma ortografía, la función está en mayúscula y la variable no. Esto las hace declaraciones distintas con diferentes ámbitos.
Guarda y sal del archivo.
Para usar este paquete en otras áreas de nuestro código, podemos importarlo
en un nuevo paquete. Crearemos este nuevo paquete, pero primero necesitaremos un nuevo directorio para almacenar esos archivos fuente.
Salgamos del directorio logging
, creemos un nuevo directorio llamado cmd
y entremos en ese nuevo directorio:
Crea un archivo llamado main.go
en el directorio cmd
que acabamos de crear:
Ahora podemos agregar el siguiente código:
Ahora tenemos nuestro programa completo escrito. Sin embargo, antes de poder ejecutar este programa, necesitaremos crear un par de archivos de configuración para que nuestro código funcione correctamente. Go utiliza Go Modules para configurar las dependencias de paquetes para importar recursos. Los módulos de Go son archivos de configuración colocados en el directorio de tu paquete que indican al compilador dónde importar los paquetes. Aunque aprender sobre módulos está fuera del alcance de este artículo, podemos escribir solo un par de líneas de configuración para hacer que este ejemplo funcione localmente.
Abre el siguiente archivo go.mod
en el directorio cmd
:
Luego coloca el siguiente contenido en el archivo:
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
La primera línea de este archivo le indica al compilador que el paquete cmd
tiene una ruta de archivo de github.com/gopherguides/cmd
. La segunda línea le indica al compilador que el paquete github.com/gopherguides/logging
puede encontrarse localmente en el disco en el directorio ../logging
.
También necesitaremos un archivo go.mod
para nuestro paquete logging
. Volvamos al directorio logging
y creemos un archivo go.mod
:
Añade el siguiente contenido al archivo:
module github.com/gopherguides/logging
Esto le indica al compilador que el paquete logging
que creamos es en realidad el paquete github.com/gopherguides/logging
. Esto hace posible importar el paquete en nuestro paquete main
con la siguiente línea que escribimos anteriormente:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Ahora deberías tener la siguiente estructura de directorios y diseño de archivos:
├── cmd
│ ├── go.mod
│ └── main.go
└── logging
├── go.mod
└── logging.go
Ahora que hemos completado toda la configuración, podemos ejecutar el programa main
desde el paquete cmd
con los siguientes comandos:
Obtendrás una salida similar a la siguiente:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
El programa imprimirá la hora actual en formato RFC 3339 seguida de cualquier declaración que hayamos enviado al logger. RFC 3339 es un formato de tiempo diseñado para representar la hora en internet y se utiliza comúnmente en archivos de registro.
Debido a que las funciones Debug
y Log
se exportan desde el paquete de logging, podemos usarlas en nuestro paquete main
. Sin embargo, la variable debug
en el paquete logging
no se exporta. Intentar referenciar una declaración no exportada resultará en un error en tiempo de compilación.
Agrega la siguiente línea resaltada a main.go
:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
fmt.Println(logging.debug)
}
Guarda y ejecuta el archivo. Recibirás un error similar al siguiente:
Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug
Ahora que hemos visto cómo se comportan los elementos exported
y unexported
en los paquetes, a continuación veremos cómo se pueden exportar fields
y methods
de structs
.
Visibilidad Dentro de Estructuras
Si bien el esquema de visibilidad en el registrador que construimos en la última sección puede funcionar para programas simples, comparte demasiado estado para ser útil desde múltiples paquetes. Esto se debe a que las variables exportadas son accesibles para varios paquetes que podrían modificar las variables en estados contradictorios. Permitir que el estado de tu paquete se cambie de esta manera hace difícil predecir cómo se comportará tu programa. Con el diseño actual, por ejemplo, un paquete podría establecer la variable Debug
en true
, y otro podría establecerla en false
en la misma instancia. Esto crearía un problema ya que ambos paquetes que están importando el paquete logging
se verían afectados.
Podemos hacer que el registrador esté aislado creando una estructura y luego colgando métodos en ella. Esto nos permitirá crear una instancia
de un registrador para ser utilizada de manera independiente en cada paquete que la consuma.
Cambia el paquete logging
a lo siguiente para refactorizar el código y aislar el registrador:
En este código, creamos una estructura Logger
. Esta estructura albergará nuestro estado no exportado, incluyendo el formato de tiempo para imprimir y la variable debug
configurada como true
o false
. La función New
establece el estado inicial para crear el logger, como el formato de tiempo y el estado de depuración. Luego almacena los valores que le proporcionamos internamente en las variables no exportadas timeFormat
y debug
. También creamos un método llamado Log
en el tipo Logger
que toma una declaración que queremos imprimir. Dentro del método Log
hay una referencia a su variable de método local l
para acceder a sus campos internos como l.timeFormat
y l.debug
.
Este enfoque nos permitirá crear un Logger
en muchos paquetes diferentes y usarlo de manera independiente de cómo lo estén utilizando los otros paquetes.
Para usarlo en otro paquete, modifiquemos cmd/main.go
para que se vea de la siguiente manera:
Al ejecutar este programa, obtendrás la siguiente salida:
Output2019-08-28T11:56:49-05:00 This is a debug statement...
En este código, creamos una instancia del logger llamando a la función exportada New
. Almacenamos la referencia a esta instancia en la variable logger
. Ahora podemos llamar a logging.Log
para imprimir declaraciones.
Si intentamos referenciar un campo no exportado del Logger
como el campo timeFormat
, recibiremos un error en tiempo de compilación. Intenta agregar la siguiente línea resaltada y ejecuta cmd/main.go
:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("This is a debug statement...")
fmt.Println(logger.timeFormat)
}
Esto producirá el siguiente error:
Output. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
El compilador reconoce que logger.timeFormat
no está exportado y, por lo tanto, no puede ser recuperado del paquete logging
.
Visibilidad Dentro de Métodos
De la misma manera que los campos de estructuras, los métodos también pueden ser exportados o no exportados.
Para ilustrar esto, agreguemos registro por niveles a nuestro registrador. El registro por niveles es un medio de categorizar tus registros para que puedas buscar en tus registros tipos específicos de eventos. Los niveles que incorporaremos en nuestro registrador son:
-
El nivel
info
, que representa eventos de tipo información que informan al usuario de una acción, comoPrograma iniciado
oCorreo electrónico enviado
. Estos nos ayudan a depurar y rastrear partes de nuestro programa para ver si el comportamiento esperado está ocurriendo. -
El nivel
warning
. Estos tipos de eventos identifican cuándo algo inesperado está ocurriendo que no es un error, comoFallo al enviar correo electrónico, reintentando
. Nos ayudan a ver partes de nuestro programa que no están funcionando tan suavemente como esperábamos. -
El nivel
error
, lo que significa que el programa encontró un problema, comoFile not found
. Esto a menudo resultará en el fallo de la operación del programa.
También podrías desear activar y desactivar ciertos niveles de registro, especialmente si tu programa no está funcionando como se esperaba y deseas depurarlo. Agregaremos esta funcionalidad cambiando el programa para que cuando debug
se establezca en true
, imprimirá todos los niveles de mensajes. De lo contrario, si es false
, solo imprimirá mensajes de error.
Agrega el registro por niveles haciendo los siguientes cambios en logging/logging.go
:
En este ejemplo, introdujimos un nuevo argumento al método Log
. Ahora podemos pasar el level
del mensaje de registro. El método Log
determina qué nivel de mensaje es. Si es un mensaje info
o warning
, y el campo debug
es true
, entonces escribe el mensaje. De lo contrario, ignora el mensaje. Si es cualquier otro nivel, como error
, escribirá el mensaje sin importar nada.
La mayor parte de la lógica para determinar si el mensaje se imprime se encuentra en el método Log
. También introdujimos un método no exportado llamado write
. El método write
es el que realmente genera el mensaje de log.
Ahora podemos utilizar este registro por niveles en nuestro otro paquete cambiando cmd/main.go
para que se vea como lo siguiente:
Al ejecutar esto obtendrás:
Output[info] 2019-09-23T20:53:38Z starting up service
[warning] 2019-09-23T20:53:38Z no tasks found
[error] 2019-09-23T20:53:38Z exiting: no work performed
En este ejemplo, cmd/main.go
utilizó con éxito el método exportado Log
.
Ahora podemos pasar el level
de cada mensaje cambiando debug
a false
:
Ahora veremos que solo se imprimen los mensajes de nivel error
:
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
Si intentamos llamar al método write
desde fuera del paquete logging
, recibiremos un error en tiempo de compilación:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("info", "starting up service")
logger.Log("warning", "no tasks found")
logger.Log("error", "exiting: no work performed")
logger.write("error", "log this message...")
}
Outputcmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)
Cuando el compilador detecta que estás intentando referenciar algo de otro paquete que comienza con una letra minúscula, sabe que no está exportado y, por lo tanto, arroja un error de compilación.
El registrador en este tutorial muestra cómo podemos escribir código que solo expone las partes que queremos que otros paquetes consuman. Debido a que controlamos qué partes del paquete son visibles fuera del paquete, ahora somos capaces de realizar cambios futuros sin afectar ningún código que dependa de nuestro paquete. Por ejemplo, si quisiéramos apagar únicamente los mensajes de nivel info
cuando debug
es falso, podrías hacer este cambio sin afectar ninguna otra parte de tu API. También podríamos hacer cambios de manera segura en el mensaje de registro para incluir más información, como el directorio desde el cual se estaba ejecutando el programa.
Conclusión
Este artículo mostró cómo compartir código entre paquetes al mismo tiempo que proteges los detalles de implementación de tu paquete. Esto te permite exportar una API simple que cambiará poco por compatibilidad hacia atrás, pero permitirá cambios privados en tu paquete según sea necesario para mejorar su funcionamiento en el futuro. Esto se considera una práctica recomendada al crear paquetes y sus correspondientes APIs.
Para obtener más información sobre paquetes en Go, consulta nuestros artículos Importando Paquetes en Go y Cómo Escribir Paquetes en Go, o explora toda nuestra serie Cómo Programar en Go.
Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go