One place for hosting & domains

      sobre

      Información sobre tipos de datos en Go


      Introducción

      En los tipos de datos se especifica el tipo de valores que se almacenarán en determinadas variables cuando escriba un programa. En ellos también se determinan las operaciones que se pueden realizar en los datos.

      En este artículo, repasaremos los tipos de datos importantes nativos de Go. No se trata de una investigación exhaustiva sobre tipos de datos, pero le permitirá familiarizarse con las opciones que tiene a su disposición en Go. Al comprender algunos tipos de datos básicos podrá escribir código más preciso que funcione de forma eficiente.

      Antecendentes

      Una alternativa para pensar en los tipos de datos es considerar los diferentes tipos de información que usamos en el mundo real. Un ejemplo de datos en el mundo real son los números: por ejemplo, podemos usar números naturales (0, 1, 2, etc.), enteros (…, -1, 0, 1, etc.,) e irracionales (π).

      Normalmente, en matemática, podemos combinar números de diferentes tipos y obtener algún tipo de respuesta. Es posible que queramos sumar 5 y π, por ejemplo:

      5 + π
      

      Podemos mantener la ecuación como a respuesta para explicar el número irracional, o redondear π a un número con posiciones decimales abreviadas, y luego sumar los números:

      5 + π = 5 + 3.14 = 8.14
      

      Sin embargo, si intentamos evaluar los números con otro tipo de datos, como las palabras, el sentido comienza a perderse. ¿Qué solución aplicaríamos a la siguiente ecuación?

      shark + 8
      

      En el caso de las computadoras, cada tipo de datos es bastante diferente, como las palabras y los números. Por lo tanto, debemos tener cuidado cuando usamos los diferentes tipos de datos para asignar valores y la forma en que los manipulamos a través de operaciones.

      Enteros

      Como en la matemática, los enteros en la programación informática son números enteros que pueden ser positivos, negativos o neutros (…, -1, 0 y 1). En Go, un entero se identifica como un int. Al igual que en otros lenguajes de programación, no debe usar comas en números de cuatro o más dígitos. Por lo tanto, cuando escriba “1.000” en su programa, ingrese “1000”.

      Podemos imprimir un entero de manera sencilla, como se muestra a continuación:

      fmt.Println(-459)
      

      Output

      -459

      También podemos declarar una variable, que en este caso es un símbolo del número que usamos o manipulamos. De esta manera:

      var absoluteZero int = -459
      fmt.Println(absoluteZero)
      

      Output

      -459

      También podemos realizar cálculos con enteros en Go. En el siguiente bloque de código, usaremos el operador de asignación := para declarar la variable​​​​​​ sum​​ y crear una instancia de ella:

      sum := 116 - 68
      fmt.Println(sum)
      

      Output

      48

      Como se muestra en el resultado, el operador matemático - restó el entero 68 de 116, lo que dio como resultado 48. Aprenderá más sobre la declaración de variables a través de la sección Declarar tipos de datos para variables.

      Los enteros pueden utilizarse de muchas formas en programas de Go. A medida que aprenda más acerca de Go, tendrá muchas oportunidades de trabajar con enteros y aprovechar su conocimiento sobre este tipo de datos.

      Números de punto flotante

      Un número de punto flotante o flotante se utiliza para representar números reales que no se pueden expresar como enteros. Los números reales incluyen todos los números racionales e irracionales y a causa de esto, los números de punto flotante pueden contener una parte fraccionaria, como 9,0 o -116,42. Para que pueda entender un flotante en un programa de Go, se trata de un número que contiene un punto decimal.

      Como hicimos con los enteros, podemos imprimir un número de punto flotante de manera sencilla, como se muestra a continuación:

      fmt.Println(-459.67)
      

      Output

      -459.67

      También se puede declarar una variable que representa un flotante, como en este caso:

      absoluteZero := -459.67
      fmt.Println(absoluteZero)
      

      Output

      -459.67

      Como en el caso de los enteros, también podemos realizar cálculos con flotantes en Go:

      var sum = 564.0 + 365.24
      fmt.Println(sum)
      

      Output

      929.24

      Con los números enteros y de punto flotante, es importante tener en cuenta que 3 ≠ 3,0, ya que 3 hace referencia a un número entero mientras que 3,0 hace referencia a un flotante.

      Tamaños de tipos numéricos

      Además de la distinción entre enteros y flotantes, Go tiene dos tipos de datos numéricos que se distinguen por la naturaleza estática o dinámica de sus tamaños. El primer tipo es independiente de la arquitectura. Esto significa que el tamaño de los datos en bits no cambia, sin importar el equipo en el que se ejecuta el código.

      La mayoría de las arquitecturas de sistemas actuales son de 32 o 64 bits. Por ejemplo, podría realizar desarrollos para una computadora portátil moderna con Windows, en la cual el sistema operativo se ejecute en una arquitectura de 64 bits. Sin embargo, si realiza desarrollos para un dispositivo como un reloj con pulsómetro, podría trabajar con una arquitectura de 32 bits. Si utiliza un tipo independiente de la arquitectura, como int32, sin importar la arquitectura para la que realiza compilaciones, el tipo tendrá un tamaño constante.

      El segundo tipo es específico de la implementación. En este tipo, el tamaño de los bits puede variar, según la arquitectura en la que se construya el programa. Por ejemplo, si usamos el tipo int cuando se compila en Go para una arquitectura de 32 bits, el tamaño del tipo de datos será de 32 bits. Si en el programa se compila para una arquitectura de 64 bits, la variable será de 64 bits.

      Además de los tipos de datos que tienen diferentes tamaños, los tipos como los enteros también vienen en dos formatos básicos: con firma y sin firma. Un int8 es un entero con firma y puede tener un valor entre -128 y 127. Un uint8 es un entero sin firma y solo puede tener un valor positivo entre 0 y 255.

      Los rangos se basan en el tamaño de bits. Para los datos binarios, 8 bits pueden representar un total de 256 valores diferentes. Debido a que un tipo int debe admitir valores positivos y negativos, un número entero de 8 bits (int8) tendrá un rango de -128 a 127, para un total de 256 valores únicos posibles.

      Go tiene los siguientes tipos de enteros independientes de la arquitectura:

      uint8       unsigned  8-bit integers (0 to 255)
      uint16      unsigned 16-bit integers (0 to 65535)
      uint32      unsigned 32-bit integers (0 to 4294967295)
      uint64      unsigned 64-bit integers (0 to 18446744073709551615)
      int8        signed  8-bit integers (-128 to 127)
      int16       signed 16-bit integers (-32768 to 32767)
      int32       signed 32-bit integers (-2147483648 to 2147483647)
      int64       signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
      

      Los números flotantes y complejos también vienen en diferentes tamaños:

      float32     IEEE-754 32-bit floating-point numbers
      float64     IEEE-754 64-bit floating-point numbers
      complex64   complex numbers with float32 real and imaginary parts
      complex128  complex numbers with float64 real and imaginary parts
      

      También existen varios alias de tipos de números, que asignan nombres útiles a tipos de datos específicos:

      byte        alias for uint8
      rune        alias for int32
      

      El propósito del alias byte es dejar claro el momento en que el programa usa bytes a modo de medición informática común en elementos de cadenas de caracteres, en contraposición a enteros pequeños no relacionados con la medición de datos de byte. Aunque byte y uint8 son idénticos una vez que se compila el programa, byte se utiliza a menudo para representar datos de caracteres en formato numérico, mientras que uint8 está diseñado para ser un número en su programa.

      El alias rune es un poco diferente. Si bien byte y uint8 contienen exactamente los mismos datos, un rune puede ser de un solo byte o de cuatro, un rango determinado por int32. Un rune se utiliza para representar un carácter Unicode, mientras que solo los caracteres ASCII se pueden representar únicamente con un tipo de datos int32.

      Además, Go tiene los siguientes tipos específicos de la implementación:

      uint     unsigned, either 32 or 64 bits
      int      signed, either 32 or 64 bits
      uintptr  unsigned integer large enough to store the uninterpreted bits of a pointer value
      

      El tamaño de los tipos específicos de la implementación se definirá a través de la arquitectura para la que se compile el programa.

      Seleccionar tipos de datos numéricos

      La selección del tamaño correcto normalmente tiene que ver más con el rendimiento de la arquitectura de destino para la que realiza tareas de programación que con el tamaño de los datos con los que trabaja. Sin embargo, sin necesidad de conocer las ramificaciones específicas de rendimiento para su programa, puede seguir algunas de estas directrices básicas cuando dé los primeros pasos.

      Como se mencionó antes en este artículo, existen tipos independientes de la arquitectura y tipos específicos de la implementación. Para los datos enteros, en Go es común utilizar los tipos de implementación como int o uint en lugar de int64 o uint64. Normalmente, esto dará como resultado una velocidad de procesamiento más alta para su arquitectura de destino. Por ejemplo, si utiliza un int64 y realiza compilaciones en una arquitectura de 32 bits, necesitará al menos el doble de tiempo para procesar esos valores porque se requieren ciclos de CPU adicionales para mover los datos a través de la arquitectura. Si en su lugar utiliza un int, en el programa se definirá como uno de 32 bits de tamaño para una arquitectura de 32 bits y se podría procesar a una velocidad considerablemente mayor.

      Si sabe que no superará un rango de tamaño específico, la elección de un tipo independiente de la arquitectura puede aumentar la velocidad y disminuir el uso de memoria. Por ejemplo, si sabe que sus datos no superarán el valor de 100 y solo representarán un número positivo, la elección de uint8 hará que su programa sea más eficiente porque requerirá menos memoria.

      Ahora que examinamos algunos de los posibles rangos para los tipos de datos numéricos, veamos qué sucederá si superamos esos rangos en nuestro programa.

      Desbordamiento vs. ajuste

      Go tiene el potencial tanto de desbordar un número como de ajustarlo cuando intente almacenar un valor más grande que el tipo de datos que su diseño permite almacenar, según el valor se calcule en el tiempo de compilación o de ejecución. Un error de tiempo de compilación sucede cuando en el programa se encuentra un error a medida que se intenta compilar dicho programa. Un error de tiempo de ejecución se produce después de la compilación del programa, justo mientras se encuentra en ejecución.

      En el siguiente ejemplo, fijamos maxUint32 en su valor máximo:

      package main
      
      import "fmt"
      
      func main() {
          var maxUint32 uint32 = 4294967295 // Max uint32 size
          fmt.Println(maxUint32)
      }
      

      Se realizará la compilación y ejecución, y producirá el siguiente resultado:

      Output

      4294967295

      Si añadimos 1 al valor en tiempo de ejecución, se ajustará a 0:

      Output

      0

      Por otro lado, cambiaremos el programa para añadir 1 a la variable cuando lo asignemos, antes del tiempo de compilación:

      package main
      
      import "fmt"
      
      func main() {
          var maxUint32 uint32 = 4294967295 + 1
          fmt.Println(maxUint32)
      
      }
      

      En el tiempo de compilación, si en el compilador se determina que un valor será demasiado grande para contenerlo en el tipo de datos especificado, producirá un error overflow. Esto significa que el valor calculado es demasiado grande para el tipo de datos que especificó.

      Debido a que en el compilador se puede determinar que se desbordará el valor, ahora generará un error:

      Output

      prog.go:6:36: constant 4294967296 overflows uint32

      Comprender los límites de sus datos le permitirá evitar posibles errores en su programa en el futuro.

      Ahora que abarcamos los tipos numéricos, analizaremos la forma de almacenar valores booleanos.

      Booleanos

      El tipo de datos booleano puede ser uno de dos valores, ya sea true o false, y se define como bool al declararlo como un tipo de datos. Los booleanos se utilizan para representar los valores de verdad que se asocian con la rama lógica de la matemática, que informa algoritmos en el ámbito de la informática.

      Los valores true y false siempre aparecerán con t y f minúsculas respectivamente, ya que son identificadores declarados previamente en Go.

      Muchas operaciones matemáticas nos proporcionan respuestas que se evalúan en “true” o “false”:

      • mayor que
        • 500 > 100 true
        • 1 > 5 false
      • menor que
        • 200 < 400 verdadero
        • 4 < 2 falso
      • igual a
        • 5 = 5 verdadero
        • 500 = 400 false

      Al igual que con los números, podemos almacenar un valor booleano en una variable:

      myBool := 5 > 8
      

      Luego, podemos imprimir el valor booleano invocando la función fmt.Println():

      fmt.Println(myBool)
      

      Debido a que 5 no es mayor que 8, obtendremos el siguiente resultado:

      Output

      false

      A medida que escriba más programas en Go, se familiarizará más con el funcionamiento de los booleanos y con la forma en que las diferentes funciones y operaciones que se evalúan en true o false pueden cambiar el curso del programa.

      Cadenas

      Una cadena es una secuencia de uno o más caracteres (letras, números y símbolos) que puede ser una constante o una variable. Los cadenas existen dentro de comillas invertidas ”“o dobles”` en Go y tienen diferentes características según las que utilice.

      Si utiliza comillas invertidas, creará un literal de cadena sin formato. Si utiliza comillas dobles, creará un literal de cadena interpretado.

      Literales de cadena sin formato

      Los literales de cadena sin formato son secuencias de caracteres entre comillas inversas, a menudo conocidas como tildes inversas. Dentro de las comillas, cualquier carácter aparecerá como se muestra entre las comillas inversas, a excepció del propio carácter de comilla inversa.

      a := `Say "hello" to Go!`
      fmt.Println(a)
      

      Output

      Say "hello" to Go!

      Normalmente, las barras diagonales inversas se utilizan para representar caracteres especiales en cadenas. Por ejemplo, en una cadena interpretada, n representaría una nueva línea en una cadena. Sin embargo, las barras diagonales inversas no tienen un significado especial dentro de los literales de cadena sin formato:

      a := `Say "hello" to Go!n`
      fmt.Println(a)
      

      Debido a que las barras diagonales inversas no tienen un significado especial en un literal de cadena, en lugar de hacer una nueva línea en realidad imprimirá el valor de n:

      Output

      Say "hello" to Go!n

      Los literales de cadena sin formato también pueden utilizarse para crear cadenas de varias líneas:

      a := `This string is on
      multiple lines
      within a single back
      quote on either side.`
      fmt.Println(a)
      

      Output

      This string is on multiple lines within a single back quote on either side.

      En los bloques de código anteriores, las nuevas líneas fueron literalmente transferidas de la entrada al resultado.

      Literales de cadena interpretados

      Los literales de cadena interpretados son secuencias de caracteres entre comillas dobles, como en “bar”. Dentro de las comillas, cualquier carácter puede aparecer con excepción de las comillas de nueva línea y las comillas dobles sin escapes. Para mostrar las comillas dobles en una cadena interpretada, puede usar la barra diagonal inversa como un carácter de escape, como se muestra a continuación:

      a := "Say "hello" to Go!"
      fmt.Println(a)
      

      Output

      Say "hello" to Go!

      Casi siempre usará literales de cadena interpretados porque permiten caracteres de escape dentro de ellas. Para obtener más información sobre cómo trabajar con cadenas, consulte Introducción al uso de cadenas en Go.

      Cadenas con caracteres UTF-8

      UTF-8 es un esquema de codificación que se utiliza para codificar caracteres de ancho variable en uno a cuatro bytes. En Go se admiten caracteres UTF-8 desde el principio, sin ningún tipo de configuración, biblioteca o paquetes especiales. Los caracteres romanos como la letra A pueden representarse a través de un valor ASCII, como en el caso del número 65. Sin embargo, con caracteres especiales como el internacional , se requeriría UTF-8. Go utiliza el tipo de alias rune para los datos de UTF-8.

      a := "Hello, 世界"
      

      Puede usar la palabra clave range en un bucle for para realizar indexaciones a través de cualquier cadena en Go, incluso una cadena UTF-8. Los bucles for y range se abordarán en detalle más adelante en la serie; por ahora, es importante saber que podemos usar esto para contar los bytes en una cadena determinada:

      package main
      
      import "fmt"
      
      func main() {
          a := "Hello, 世界"
          for i, c := range a {
              fmt.Printf("%d: %sn", i, string(c))
          }
          fmt.Println("length of 'Hello, 世界': ", len(a))
      }
      

      En el bloque de código anterior, declaramos la variable a y le asignamos el valor de Hello, 世界. El texto asignado tiene caracteres UTF-8.

      Luego usamos un bucle for estándar y la palabra clave range. En Go, la palabra clave range se indexará a través de una cadena que muestra un carácter a la vez, así como el índice de bytes en el que se encuentra el carácter en la cadena.

      Usando la función fmt.Printf, proporcionamos una cadena de formato de %d: %sn. %d es el verbo de impresión para un dígito (en este caso, un entero) y %s es el verbo de impresión para una cadena. Luego proporcionamos los valores de i, que es el índice actual del bucle for, y c, que es el carácter actual en el bucle for.

      Por último, imprimimos la variable a en toda su extensión utilizando la función len integrada.

      Anteriormente, mencionamos que un rune es un alias para int32 y puede estar compuesto de uno a cuatro bytes. El carácter ocupa tres bytes para su definición y el índice se mueve en consecuencia cuando se desplaza a través de la cadena UTF-8. Esta es la razón por la que i no es secuencial cuando se imprime.

      Output

      0: H 1: e 2: l 3: l 4: o 5: , 6: 7: 世 10: 界 length of 'Hello, 世界': 13

      Como puede observar, la extensión supera el número de veces que tardó en recorrer la cadena.

      No siempre trabajará con cadenas UTF-8, pero cuando lo haga, comprenderá por qué son “runes” y no solo un int32.

      Declarar tipos de datos para variables

      Ahora que conoce los diferentes tipos de datos primitivos, repasaremos la forma de asignar estos tipos a variables en Go.

      En Go, podemos definir una variable con la palabra clave var seguida del nombre de la variable y el tipo de datos deseado.

      En el siguiente ejemplo, declararemos una variable con el nombre pi del tipo float64.

      La palabra clave var es lo primero que se declara:

      var pi float64
      

      A esta le sigue el nombre de nuestra variable, pi:

      var pi float64
      

      Por último, el tipo de datos float64:

      var pi float64
      

      Opcionalmente también se puede especificar un valor inicial, como 3.14:

      var pi float64 = 3.14
      

      Go es un lenguaje tipificado estáticamente. Esto significa que cada instrucción en el programa se verifica en el tiempo de compilación. También significa que el tipo de datos está ligado a la variable, mientras que en los lenguajes vinculados de forma dinámica este está ligado al valor.

      Por ejemplo, en Go el tipo se declara al declarar una variable:

      var pi float64 = 3.14
      var week int = 7
      

      Cada una de estas variables podría ser de un tipo de datos diferente si las declaró de forma diferente.

      Esto es diferente en comparación con un lenguaje como PHP, en el que se asocia el tipo de datos al valor:

      $s = "sammy";         // $s is automatically a string
      $s = 123;             // $s is automatically an integer
      

      En el bloque de código anterior, el primer $s es una cadena porque se le asigna el valor “sammy” y el segundo es un entero porque tiene el valor 123.

      A continuación, veremos tipos de datos más complejos como las matrices.

      Matrices

      Una matriz es una secuencia ordenada de elementos. La capacidad de una matriz se define en el momento de su creación. Una vez que se asigna el tamaño a una matriz, este ya no se puede cambiar. Debido a que el tamaño de una matriz es estático, significa que se asigna memoria solo una vez. Esto hace que el trabajo con matrices resulte un tanto rígido, pero aumenta el rendimiento de su programa. Debido a esto, las matrices suelen utilizarse al optimizar programas. Los segmentos, que veremos a continuación, son más flexibles y constituyen lo que se podría considerar como matrices en otros lenguajes.

      Las matrices se definen al declarar el tamaño de estas y luego el tipo de datos con los valores definidos entre llaves { }.

      Una matriz de cadenas tiene el siguiente aspecto:

      [3]string{"blue coral", "staghorn coral", "pillar coral"}
      

      Podemos almacenar una matriz en una variable e imprimirla:

      coral := [3]string{"blue coral", "staghorn coral", "pillar coral"}
      fmt.Println(coral)
      

      Output

      [blue coral staghorn coral pillar coral]

      Como ya se mencionó anteriormente, los segmentos son similares a las matrices, pero son mucho más flexibles. Veamos este tipo de datos mutable.

      Segmentos

      Un segmento es una secuencia ordenada de elementos cuya extensión puede cambiar. El tamaño de los segmentos puede aumentar de forma dinámica. Cuando añada nuevos elementos a un segmento, si este no cuenta con memoria suficiente para almacenar los nuevos elementos, se solicitará más memoria al sistema según sea necesario. Debido a que se pueden ampliar para añadir más elementos cuando sea necesario, los segmentos se utilizan con mayor frecuencia que las matrices.

      Los segmentos se definen declarando el tipo de datos precedidos por un corchete de apertura y cierre [], y disponiendo los valores entre llaves { }.

      Un segmento de enteros tiene el siguiente aspecto:

      []int{-3, -2, -1, 0, 1, 2, 3}
      

      Un segmento de flotantes tiene el siguiente aspecto:

      []float64{3.14, 9.23, 111.11, 312.12, 1.05}
      

      Un segmento de cadenas tiene el siguiente aspecto:

      []string{"shark", "cuttlefish", "squid", "mantis shrimp"}
      

      Definiremos nuestro segmento de cadenas como seaCreatures:

      seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp"}
      

      Podemos imprimirlos invocando a la variable:

      fmt.Println(seaCreatures)
      

      El resultado tendrá un aspecto exactamente igual a la lista que creamos:

      Output

      [shark cuttlefish squid mantis shrimp]

      Podemos usar la palabra clave append para añadir un elemento a nuestro segmento. Con el siguiente comando se agregará el valor de cadena seahorse al segmento:

      seaCreatures = append(seaCreatures, "seahorse")
      

      Puede verificar que se agregó imprimiéndolo:

      fmt.Println(seaCreatures)
      

      Output

      [shark cuttlefish squid mantis shrimp seahorse]

      Como puede observar, si necesita administrar un tamaño desconocido de elementos, un segmento será mucho más versátil que una matriz.

      Mapas

      El mapa es el tipo de hash o de diccionario incorporado de Go. Los mapas utilizan claves y valores como un par para almacenar datos. Esto es útil en la programación para buscar rápidamente valores a través de un índice o, en este caso, una clave. Por ejemplo, podría querer mantener un mapa de usuarios, indexados por su ID de usuario. La clave sería el ID del usuario y el objeto del usuario sería el valor. Un mapa se construye usando la palabra clave map seguida por el tipo de datos de clave entre corchetes [ ] y luego por el tipo de datos de valor y los pares clave-valor entre llaves.

      map[key]value{}
      

      Un mapa normalmente se usa para guardar datos relacionados, como la información contenida en un ID, y tiene el siguiente aspecto:

      map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      

      Observará que, además de las llaves, hay también dos puntos en varias partes del mapa. Las palabras que se hallan a la izquierda de los dos puntos son las claves. Las claves pueden ser de cualquier tipo comparable en Go. Los tipos comparables son primitivos, como strings e ints, entre otros. Un tipo primitivo se define por el lenguaje y no se construye a partir de la combinación de cualquier otros tipos. Aunque puede haber tipos definidos por el usuario, se considera que una buena práctica es procurar que sean sencillos para evitar errores de programación. Las claves en el diccionario anterior son: name, animal, color y location.

      Las palabras a la derecha de los dos puntos representan los valores. Los valores pueden comprender cualquier tipo de datos. Los valores en el diccionario anterior son: Sammy, shark, blue y ocean.

      Almacenaremos el mapa dentro de una variable y lo imprimiremos:

      sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      fmt.Println(sammy)
      

      Output

      map[animal:shark color:blue location:ocean name:Sammy]

      Si queremos aislar el color de Sammy, podemos hacerlo invocando a sammy["color"]. Lo imprimiremos:

      fmt.Println(sammy["color"])
      

      Output

      blue

      Debido a que los mapas ofrecen pares clave-valor para almacenar datos, pueden ser elementos importantes en su programa de Go.

      Conclusión

      En este punto, comprenderá mejor algunos de los tipos de datos principales que están disponibles para que utilizarlos en Go. Cada uno de estos tipos de datos cobrará importancia a medida que usted desarrolle proyectos de programación en el lenguaje Go.

      Una vez que conozca bien los tipos de datos disponibles en Go, podrá aprender a convertir tipos de datos para tener la posibilidad de cambiarlos según la situación.



      Source link

      Información sobre defer en Go


      Introducción

      Go tiene muchas de las palabras claves de flujo de control comunes que se encuentran en otros lenguajes de programación, como if, switch y for, entre otras. Una palabra clave que no tienen la mayoría de los otros lenguajes de programación es defer, y aunque es menos común, pronto verá la utilidad de esta palabra en sus programas.

      Uno de los principales usos de una instrucción defer es el de limpiar recursos como archivos abiertos, conexiones de red y controladores de bases de datos. Cuando su programa termine con estos recursos, es importante cerrarlos para evitar agotar los límites del programa y permitir que otros programas accedan a esos recursos. defer aporta más claridad a nuestro código y reduce su propensión a experimentar errores mediante la conservación de las invocaciones para cerrar archivos y recursos cerca de las invocaciones abiertas.

      En este articulo, aprenderá a usar de forma adecuada la instrucción defer para limpiar recursos y también verá algunos errores comunes que se cometen cuando se utiliza defer.

      Qué es una instrucción defer

      Una instrucción defer añade la invocación de la función después de la palabra clave defer en una pila. Todas las invocaciones de la pila en cuestión se invocan cuando regresa la función en la que se añadieron. Debido a que las invocaciones se disponen en una pila, se llaman en el orden “último en entrar” y “primero en salir”.

      Veremos la forma en que defer funciona imprimiendo un texto:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("Bye")
          fmt.Println("Hi")
      }
      

      En la función main hay dos instrucciones. La primera comienza con la palabra clave defer y le sigue una afirmación print que imprime Bye. La siguiente línea imprime Hi.

      Si ejecutamos el programa, veremos el siguiente resultado:

      Output

      Hi Bye

      Observe que Hi se imprimió primero. Esto es porque cualquier instrucción precedida por la palabra clave defer no se invoca hasta el final de la función en la cual se utilizó defer.

      Echaremos otro vistazo al programa y esta vez añadiremos algunos comentarios para ayudar a ilustrar lo que está sucediendo:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          // defer statement is executed, and places
          // fmt.Println("Bye") on a list to be executed prior to the function returning
          defer fmt.Println("Bye")
      
          // The next line is executed immediately
          fmt.Println("Hi")
      
          // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
      }
      

      La clave para comprender defer es que cuando se ejecuta la instrucción defer, los argumentos para la función diferida se evalúan de inmediato. Cuando defer se ejecuta, dispone la instrucción después de sí en una lista para que se invoque antes del regreso de la función.

      Aunque este código ilustra el orden en el cual se ejecutaría defer, no es una alternativa habitual para usarla cuando se escribe un programa de Go. Es más probable que utilicemos defer para limpiar un recurso, como el controlador de un archivo. Veremos la forma de hacer eso a continuación.

      Utilizar defer para limpiar recursos

      En Go, es muy común usar defer para limpiar recursos. Primero, veremos un programa que escribe una cadena en un archivo, pero no utiliza defer para gestionar la limpieza del recurso:

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
          file.Close()
          return nil
      }
      

      En este programa, existe una función llamada write que primero intentará crear un archivo. Si tiene un error, lo mostrará y cerrará la función. A continuación, intenta escribir la cadena This is a readme file en el archivo especificado. Si recibe un error, lo mostrará y cerrará la función. A continuación, la función intentará cerrar el archivo y liberar el recurso de vuelta para el sistema. Finalmente, la función muestra nil para indicar que se ejecutó sin errores.

      Aunque este código funciona, hay un error sutil. Si falla la invocación de io.WriteString, la función volverá sin cerrar el archivo ni liberar el recurso de vuelta para el sistema.

      Podríamos solucionar el problema añadiendo otra instrucción file.Close(), método con el cual probablemente resolvería esto en un lenguaje sin defer:

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          _, err = io.WriteString(file, text)
          if err != nil {
              file.Close()
              return err
          }
          file.Close()
          return nil
      }
      

      Ahora, incluso si la invocación de io.WriteString falla, cerraremos el archivo de todos modos. Aunque este era un error relativamente fácil de detectar y solucionar, con una función más complicada, es posible que se haya pasado por alto.

      En vez de añadir la segunda invocación a file.Close(), podemos usar una instrucción defer para garantizar que independientemente de las secciones que se tomen durante la ejecución, siempre invoquemos Close().

      Aquí está la versión que utiliza la palabra clave defer:

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          defer file.Close()
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
          return nil
      }
      

      Esta vez, añadimos la línea de código: defer file.Close(). Esto indica al compilador que debería ejecutar file.Close antes de cerrar la función write.

      Ahora, nos hemos asegurado de que, incluso si añadimos más código y creamos otra ramificación que cierre la función en el futuro, siempre limpiaremos y cerraremos el archivo.

      Sin embargo, hemos introducido un error más al añadir defer. Ya no comprobaremos el error potencial que puede mostrarse desde el método Close. Esto se debe a que cuando usamos defer no hay forma de comunicar valores de retorno a nuestra función.

      En Go, se considera una práctica segura y aceptada invocar Close() más de una vez sin que esto afecte al comportamiento de su programa. Si Close() muestra un error, lo hará la primera vez que se invoque. Esto nos permite invocarlo explícitamente en la ruta de ejecución correcta de nuestra función.

      Veamos cómo podemos aplicar defer a la invocación para Close y, de todas formas, notificar un error si encontramos uno.

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          defer file.Close()
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
      
          return file.Close()
      }
      

      El único cambio en este programa es la última línea en la que mostramos file.Close(). Si la invocación a Close genera un error, este ahora se mostrará como previsto para la función de invocación. Tenga en cuenta que nuestra instrucción defer file.Close() también se ejecutará después de la instrucción return. Esto significa que file.Close() posiblemente se invoque dos veces. Aunque esto no es lo ideal, es una práctica aceptable porque no debería tener efectos colaterales en su programa.

      Si, sin embargo, vemos un error previamente en la función, como cuando invocamos WriteString; la función mostrará ese error y también intentará invocar a file.Close porque se difirió. Aunque file.Close puede mostrar un error (y probablemente lo haga) también, esto ya no nos importa porque vemos un error que probablemente nos indique el problema.

      Hasta ahora, vimos la forma en que podemos usar un único defer para asegurarnos de limpiar nuestros recursos correctamente. A continuación, veremos la manera en que podemos usar varias instrucciones defer para limpiar más de un recurso.

      Varias instrucciones defer

      Es normal que haya más de una instrucción defer en una función. Crearemos un programa que solo tenga instrucciones defer para ver qué sucede cuando introducimos varias instrucciones defer:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("one")
          defer fmt.Println("two")
          defer fmt.Println("three")
      }
      

      Si ejecutamos el programa, veremos el siguiente resultado:

      Output

      three two one

      Observe que el orden es el opuesto al que empleamos para invocar las instrucciones defer. Esto se debe a que cada instrucción diferida que se invoca se apila sobre la anterior y luego se invoca a la inversa cuando la función sale del ámbito (Last In, First Out).

      Puede tener tantas invocaciones diferidas como sea necesario en una función, pero es importante recordar que todas se invocarán en el orden opuesto en el que se ejecutaron.

      Ahora que comprendemos el orden en el cual se ejecutarán varias instrucciones defer, veremos la forma de usar varias instrucciones defer para limpiar varios recursos. Crearemos un programa que abra un archivo, realice tareas de escritura en él y luego lo abra de nuevo para copiar el contenido a otro archivo.

      main.go

      package main
      
      import (
          "fmt"
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("sample.txt", "This file contains some sample text."); err != nil {
              log.Fatal("failed to create file")
          }
      
          if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
              log.Fatal("failed to copy file: %s")
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          defer file.Close()
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
      
          return file.Close()
      }
      
      func fileCopy(source string, destination string) error {
          src, err := os.Open(source)
          if err != nil {
              return err
          }
          defer src.Close()
      
          dst, err := os.Create(destination)
          if err != nil {
              return err
          }
          defer dst.Close()
      
          n, err := io.Copy(dst, src)
          if err != nil {
              return err
          }
          fmt.Printf("Copied %d bytes from %s to %sn", n, source, destination)
      
          if err := src.Close(); err != nil {
              return err
          }
      
          return dst.Close()
      }
      

      Añadimos una nueva función llamada fileCopy. En esta función, primero abrimos nuestro archivo de origen desde el que realizaremos la copia. Comprobaremos si se mostró un error al abrir el archivo. Si es así, aplicaremos return al error y cerraremos la función. De lo contrario, aplicaremos defer al cierre del archivo de origen que acabamos de abrir.

      A continuación, crearemos un archivo de destino. De nuevo, comprobaremos si aparece un error al crear el archivo. Si esto sucede, aplicaremos return a ese error y cerraremos la función. De lo contrario, también aplicaremos defer a Close() para el archivo de destino. Ahora tenemos dos funciones defer que se invocarán cuando la función cierre su ámbito.

      Ahora que ambos archivos están abiertos, aplicaremos Copy() a los datos del archivo de origen al de destino. Si esto se realiza correctamente, intentaremos cerrar ambos archivos. Si observamos un error al intentar cerrar cualquiera de los archivos, aplicaremos return al error y cerraremos el ámbito de la función.

      Observe que invocamos de forma explícita a Close() para cada archivo, aunque defer también invoque a Close(). Esto es para garantizar que notifiquemos el error si hay un error al cerrar un archivo. También garantiza que si, por cualquier motivo, la función se cierra antes de tiempo con un error, por ejemplo, si no pudimos realizar una copia entre los dos archivos, cada uno de ellos intentará cerrarse de forma adecuada a partir de las invocaciones diferidas.

      Conclusión

      En este artículo, incorporó conocimientos sobre la instrucción defer y la forma en que puede usarse para verificar que se hayan limpiado correctamente los recursos del sistema en nuestro programa. Limpiar correctamente los recursos del sistema hará que su programa utilice menos memoria y funcione mejor. Para obtener más información acerca de las aplicaciones de defer, lea el artículo sobre el manejo de Panics o consulte nuestra serie Cómo realizar codifcaciones en Go.



      Source link

      Información sobre init en Go


      Introducción

      En Go, la función init() predeterminada establece una porción de código que debe ejecutarse antes que cualquier otra parte de su paquete. Este código se ejecutará tan pronto como se importe el paquete y puede usarse cuando necesite que su aplicación se inicie en un estado específico; por ejemplo, cuando requiera que la aplicación se inicie con una configuración o un conjunto de recursos específicos. También se utiliza al importar un efecto secundario, una técnica que se utiliza para establecer el estado de un programa al importar un paquete específico. Esto se suele utilizar para registrar un paquete con otro a fin de garantizar que el programa considere el código correcto para la tarea.

      Si bien init() es una herramienta útil, a veces puede dificultar la lectura del código, dado que una instancia init() difícil de encontrar afectará en gran medida el orden en el que se ejecuta el código. Debido a esto, es importante que los desarrolladores que comienzan a usar Go comprendan las facetas de esta función, para poder asegurarse de utilizar init() de forma legible al escribir código.

      A través de este tutorial, aprenderá a usar init() para configurar e inicializar variables de paquetes específicas, cálculos por única vez y registros de un paquete para su uso con otro.

      Requisitos previos

      Para algunos de los ejemplos que se incluyen en este artículo, necesitará lo siguiente:

      .
      ├── bin
      │
      └── src
          └── github.com
              └── gopherguides
      

      Declarar init()

      Siempre que declare una función init(), Go la cargará antes que a cualquier otra cosa de ese paquete. Para demostrarlo, en esta sección se explicará la manera de definir una función init() y se mostrarán los efectos sobre la forma en que se ejecuta el paquete.

      Primero, tomemos lo siguiente como ejemplo de código sin la función init():

      main.go

      package main
      
      import "fmt"
      
      var weekday string
      
      func main() {
          fmt.Printf("Today is %s", weekday)
      }
      

      En este programa, declaramos una variable global llamada weekday. De forma predeterminada, el valor de weekday es una cadena vacía.

      Ejecutaremos este código:

      Debido a que el valor de weekday está vacío, al ejecutar el programa, obtendremos el siguiente resultado:

      Output

      Today is

      Podemos completar la variable en blanco introduciendo una función init() que inicialice el valor de weekday en el día actual. Añada las siguientes líneas resaltadas a main.go:

      main.go

      package main
      
      import (
          "fmt"
          "time"
      )
      
      var weekday string
      
      func init() {
          weekday = time.Now().Weekday().String()
      }
      
      func main() {
          fmt.Printf("Today is %s", weekday)
      }
      

      En este código, importamos y usamos el paquete time para obtener el día actual de la semana (Now(). Weekday(). String()) y, luego, utilizamos init() para inicializar weekday con ese valor.

      Ahora, cuando ejecutemos el programa, imprimirá el día actual de la semana:

      Output

      Today is Monday

      Aunque esto ilustra la forma en que init() funciona, es mucho más común usar init() al importar un paquete. Esto puede ser útil cuando necesita realizar tareas de configuración específicas en un paquete antes de que se utilice. Para demostrarlo, crearemos un programa que requerirá una inicialización específica a fin de que el paquete funcione como se indica.

      Inicializar paquetes en la importación

      Primero, escribiremos código para que se seleccione e imprima un animal al azar de un segmento, pero no usaremos init() en nuestro programa inicial. Esto indicará mejor el problema que tenemos y la manera en que init() lo resolverá.

      Desde su directorio src/github.com/gopherguides/, cree una carpeta llamada creatrue con el siguiente comando:

      Dentro de la carpeta creature, cree un archivo llamado creature:

      • nano creature/creature.go

      En este archivo, añada el siguiente contenido:

      creature.go

      package creature
      
      import (
          "math/rand"
      )
      
      var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
      
      func Random() string {
          i := rand.Intn(len(creatures))
          return creatures[i]
      }
      

      Este archivo define una variable llamada creatures que tiene un conjunto de animales marinos que se inicializan como valores. También tiene una función Random exportada que mostrará un valor al azar de la variable creatures.

      Guarde y cierre este archivo.

      A continuación, crearemos un paquete cmd que usaremos para escribir nuestra función main() e invocar el paquete creature.

      En el mismo nivel de archivo desde el que creamos la carpeta creature, cree una carpeta cmd con el siguiente comando:

      Dentro de la carpeta cmd, cree un archivo llamado main.go:

      Añada el siguiente contenido al archivo:

      cmd/main.go

      package main
      
      import (
          "fmt"
      
          "github.com/gopherguides/creature"
      )
      
      func main() {
          fmt.Println(creature.Random())
          fmt.Println(creature.Random())
          fmt.Println(creature.Random())
          fmt.Println(creature.Random())
      }
      

      Aquí, importamos el paquete creature y luego, en la función main(), usamos la función creature.Random() para obtener un animal al azar e imprimirlo cuatro veces.

      Guarde y cierre main.go.

      Ahora, tenemos todo nuestro programa escrito. Sin embargo, para poder ejecutar este programa, también debemos crear algunos archivos de configuración a fin de que nuestro código funcione correctamente. Go utiliza Go Modules para configurar las dependencias de paquetes e importar recursos. Estos módulos son archivos de configuración que se disponen en su directorio de paquetes e indican al compilador el punto desde el cual se deben importar los paquetes. Si bien en este artículo no obtendrá información sobre los módulos, podemos escribir algunas líneas de configuración para que este ejemplo funcione a nivel local.

      En el directorio cmd, cree un archivo llamado go.mod:

      Una vez que el archivo esté abierto, disponga el siguiente contenido:

      cmd/go.mod

      module github.com/gopherguides/cmd
       replace github.com/gopherguides/creature => ../creature
      

      La primera línea de este archivo indica al compilador que el paquete cmd que creamos es, de hecho, github.com/gopherguides/cmd. La segunda línea indica al compilador que github.com/gopherguides/creature se encuentra a nivel local en el disco, en el directorio ../creature.

      Guarde y cierre el archivo. A continuación, cree un archivo go.mod en el directorio creature:

      Añada la siguiente línea de código al archivo:

      creature/go.mod

       module github.com/gopherguides/creature
      

      Esto indica al compilador que el paquete creature que creamos, en realidad, es el paquete github.com/gopherguides/creature. Sin esto, el paquete cmd no tendría registro del punto desde el cual debería importar este paquete.

      Guarde y cierre el archivo.

      Ahora, debería contar con esta estructura de directorios y distribución de archivos:

      ├── cmd
      │   ├── go.mod
      │   └── main.go
      └── creature
          ├── go.mod
          └── creature.go
      

      Ahora que completamos toda la configuración, podemos ejecutar el programa main con el siguiente comando:

      Esto proporcionará lo siguiente:

      Output

      jellyfish squid squid dolphin

      Cuando ejecutamos este programa, recibimos cuatro valores y los imprimimos. Si ejecutamos el programa varias veces, observaremos que siempre obtenemos el mismo resultado, en vez de un resultado al azar como se espera. Esto se debe a que el paquete rand crea números pseudoaleatorios que generarán de forma sistemática el mismo resultado para un único estado inicial. Para lograr un número más aleatorio, podemos propagar el paquete o establecer un origen cambiante para que el estado inicial sea diferente cada vez que ejecutemos el programa. En Go, es habitual usar la hora actual para propagar el paquete rand.

      Dado que queremos que el paquete creature maneje la funcionalidad aleatoria, abra este archivo:

      • nano creature/creature.go

      Añada las siguientes líneas al archivo creature.go:

      creature/creature.go

      package creature
      
      import (
          "math/rand"
          "time"
      )
      
      var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
      
      func Random() string {
          rand.Seed(time.Now().UnixNano())
          i := rand.Intn(len(creatures))
          return creatures[i]
      }
      

      En este código, importamos el paquete time y usamos Seed() para propagar la hora actual. Guarde el archivo y ciérrelo.

      Ahora, cuando ejecutemos el programa, obtendremos un resultado aleatorio:

      Output

      jellyfish octopus shark jellyfish

      Si continúa ejecutando el programa una y otra vez, seguirá obteniendo resultados aleatorios. Sin embargo, esta todavía no es una implementación ideal de nuestro código, porque cada vez qye se invoca creature.Random() también se vuelve a propagar el paquete rand invocando rand.Seed(time.Now(). UnixNano()) de nuevo. La repetición de la propagación aumentará la probabilidad de realizar la propagación con el mismo valor inicial si el reloj interno no se ha modificado, lo cual posiblemente generará repeticiones del patrón aleatorio o aumentará el tiempo de procesamiento de la CPU al hacer que el programa espere el cambio del reloj.

      Para solucionar esto, podemos usar una función init(). Actualizaremos el archivo creature.go:

      • nano creature/creature.go

      Añada las siguientes líneas de código:

      creature/creature.go

      package creature
      
      import (
          "math/rand"
          "time"
      )
      
      var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
      
      func init() {
          rand.Seed(time.Now().UnixNano())
      }
      
      func Random() string {
          i := rand.Intn(len(creatures))
          return creatures[i]
      }
      

      La adición de la función init() indica al compilador que, al importar el paquete creature, debe ejecutar la función init() una vez con una sola propagación para la generación de un número al azar. Esto garantiza que no ejecutemos código más de lo necesario. Ahora, si ejecutamos el programa, continuaremos obteniendo resultados aleatorios:

      Output

      dolphin squid dolphin octopus

      En esta sección, vimos que el uso de init() puede garantizar que se realicen inicializaciones o cálculos adecuados antes de usar un paquete. A continuación, veremos la forma de usar varias instrucciones init() en un paquete.

      Varias instancias de init()

      A diferencia de la función main(), que solo se puede declarar una vez, la función init() puede declararse varias veces en un paquete. Sin embargo, varias funciones init() pueden hacer que resulte difícil determinar la que tiene prioridad sobre las demás. En esta sección, se mostrará la manera de mantener el control sobre varias instrucciones init().

      En la mayoría de los casos, las funciones init() se ejecutarán en el orden en el que las encuentre. Tomemos el siguiente código como ejemplo:

      main.go

      package main
      
      import "fmt"
      
      func init() {
          fmt.Println("First init")
      }
      
      func init() {
          fmt.Println("Second init")
      }
      
      func init() {
          fmt.Println("Third init")
      }
      
      func init() {
          fmt.Println("Fourth init")
      }
      
      func main() {}
      

      Si ejecutamos el programa con el siguiente comando:

      Obtendremos el siguiente resultado:

      Output

      First init Second init Third init Fourth init

      Observe que cada función init() se ejecuta en el orden en el que el compilador la encuentra. Sin embargo, es posible que no siempre sea tan fácil determinar el orden de invocación de las funciones init().

      Veamos una estructura de paquetes más complicada en la que tenemos varios archivos, cada uno con su propia función init() declarada en su interior. Para ilustrar esto, crearemos un programa que comparta una variable llamada message y la imprima.

      Elimine los directorios creature y cmd y su contenido de la sección anterior, y sustitúyalos por los directorios y la estructura de archivos que se indican a continuación:

      ├── cmd
      │   ├── a.go
      │   ├── b.go
      │   └── main.go
      └── message
          └── message.go
      

      Ahora, agregaremos el contenido de cada archivo. En a.go, añada las siguientes líneas:

      cmd/a.go

      package main
      
      import (
          "fmt"
      
          "github.com/gopherguides/message"
      )
      
      func init() {
          fmt.Println("a ->", message.Message)
      }
      

      Este archivo contiene una función init() única que imprime el valor de message.Message del paquete message.

      A continuación, añada el siguiente contenido a b.go:

      cmd/b.go

      package main
      
      import (
          "fmt"
      
          "github.com/gopherguides/message"
      )
      
      func init() {
          message.Message = "Hello"
          fmt.Println("b ->", message.Message)
      }
      

      En b.go, hay una función init() única que fija el valor de message.Message en Hello y lo imprime.

      A continuación, cree main.go para que tenga el siguiente aspecto:

      cmd/main.go

      package main
      
      func main() {}
      

      Este archivo no hace más que simplemente ofrecer un punto de entrada para que se ejecute el programa.

      Por último, cree su archivo message.go de la siguiente manera:

      message/message.go

      package message
      
      var Message string
      

      Nuestro paquete messages declara la variable Message exportada.

      Para iniciar el programa, ejecute el siguiente comando desde el directorio cmd:

      Debido a que hay varios archivos de Go en la carpeta cmd que conforman el paquete main, debemos indicar al compilador que todos los archivos .go de la carpeta cmd deben compilarse. Usar *.go indica al compilador que cargue todos los archivos que terminan en .go de la carpeta cmd. Si emitiéramos el comando go main.go, el programa no se compilaría porque no detectaría el código en los archivos a.go y b.go.

      Esto generará el siguiente resultado:

      Output

      a -> b -> Hello

      De acuerdo con la especificación del lenguaje de Go para Inicialización de paquetes, cuando se encuentran varios archivos en un paquete se procesan en orden alfabético. Es por esto que la primera vez que imprimimos message.Message desde a.go, el valor estaba vacío. El valor no se inicializó hasta que se ejecutó la función init() desde b.go.

      Si cambiáramos el nombre del archivo de a.go a c.go, obtendríamos un resultado diferente:

      Output

      b -> Hello a -> Hello

      Ahora, el compilador encuentra b.go primero y, por lo tanto, el valor de message.Message ya está inicializado con Hello cuando se encuentra la función init() en c.go.

      Este comportamiento podría generar un problema en su código. En el ámbito del desarrollo de software, es habitual cambiar los nombres de los archivos y, por la forma en que se procesa init(), hacer este cambio puede modificar el orden en el que se procesa init(). Esto podría tener el efecto no deseado de cambiar el resultado de su programa. Para garantizar un comportamiento de inicialización reproducible, se recomienda que los sistemas de compilación presenten a un compilador varios archivos pertenecientes al mismo paquete en orden de nombre de archivo léxico. Una forma de garantizar que se carguen todas las funciones init() en orden es declararlas en su totalidad en un único archivo. Esto impedirá que el orden cambie, incluso si se cambian los nombres de los archivos.

      Además de garantizar que el orden de sus funciones init() no cambie, también debe intentar evitar la administración del estado en su paquete usando variables globales; es decir, variables accesibles desde cualquier punto del paquete. En el programa anterior, la variable message.Message estaba disponible para todo el paquete y mantuvo el estado del programa. Debido a este acceso, las instrucciones init() pudieron cambiar la variable y desestabilizar la previsibilidad de su programa. Para evitar esto, intente trabajar con variables en espacios controlados que tengan el menor nivel de acceso posible y, al mismo tiempo, permitan que el programa funcione.

      Vimos que puede tener varias declaraciones init() en un único paquete. Sin embargo, esto puede crear efectos no deseados y hacer que su programa sea difícil de leer o predecir. Evitar tener varias instrucciones init() o mantenerlas en un único archivo garantizará que el comportamiento de su programa no cambie al mover los archivos o modificar su nombre.

      A continuación, veremos cómo se utiliza init() para la importación con efectos secundarios.

      Usar init() para efectos secundarios

      En Go, a veces es conveniente importar un paquete no por su contenido, sino por los efectos secundarios que se producen al importarlo. Esto suele significar que hay una instrucción init() en el código importado que se ejecuta antes de cualquier otro código, lo cual permite que el desarrollador manipule el estado en el que se inicia el programa. La técnica se denomina importación para efectos secundarios.

      Un caso de uso común para realizar una importación para obtener efectos secundarios tiene que ver con registrar la funcionalidad en su código, lo cual permite que un paquete registre la parte del código que necesita usar su programa. En el paquete image, por ejemplo, la función image.Decode debe registrar el formato de la imagen que intenta decodificar (jpg, png y gif, entre otros) para poder ejecutarse. Puede realizar esto importando, primero, un programa específico que tenga un efecto secundario de instrucción init().

      Supongamos que intenta usar image.Decode en un archivo .png con el siguiente fragmento de código:

      Sample Decoding Snippet

      . . .
      func decode(reader io.Reader) image.Rectangle {
          m, _, err := image.Decode(reader)
          if err != nil {
              log.Fatal(err)
          }
          return m.Bounds()
      }
      . . .
      

      De todos modos, se compilará un programa con este código, pero cada vez que intentemos decodificar una imagen png, obtendremos un error.

      Para solucionar esto, primero, debemos registrar un formato de imagen para image.Decode. Afortunadamente, el paquete image/png contiene la siguiente instrucción init():

      image/png/reader.go

      func init() {
          image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
      }
      

      Por lo tanto, si importamos image/png en nuestro fragmento de decodificación, la función image.RegisterFormat() en image/png se ejecutará antes que cualquier parte de nuestro código:

      Sample Decoding Snippet

      . . .
      import _ "image/png"
      . . .
      
      func decode(reader io.Reader) image.Rectangle {
          m, _, err := image.Decode(reader)
          if err != nil {
              log.Fatal(err)
          }
          return m.Bounds()
      }
      

      Con esto, se establecerá el estado y se registrará que necesitamos la versión png de image.Decode(). El registro se realizará como efecto secundario de la importación de image/png.

      Posiblemente, haya observado el identificador en blanco (_) antes de "image/png". Esto es necesario porque Go no le permite importar paquetes que no se utilicen en todo el programa. Cuando se incluye el identificador en blanco, el valor de la importación se descarta para que solo se produzca el efecto secundario de la importación. Esto significa que, aunque nunca invoquemos el paquete image/png en nuestro código, podemos importarlo para obtener el efecto secundario.

      Es importante conocer el momento en que se debe importar un paquete debido a su efecto secundario. Sin el registro adecuado, es probable que su programa se compile y no funcione correctamente cuando se ejecute. Los paquetes de la biblioteca estándar declararán la necesidad de este tipo de importación en su documentación. Si escribe un paquete que requiere una importación para obtener efectos secundarios, también debe asegurarse de que la instrucción init() que esté usando se documente para que los usuarios que importen su paquete puedan utilizarlo correctamente.

      Conclusión

      A través de este tutorial, aprendió que la función init() se carga antes que el resto del código de su paquete y que puede realizar tareas específicas para un paquete, como inicializar un estado deseado. También aprendió que el orden en el que el compilador ejecuta varias instrucciones init() depende del orden en el que carga los archivos de origen. Si desea obtener más información sobre init(), consulte la documentación oficial de Golang o lea los comentarios acerca de la función en la comunidad de Go.

      Puede leer más sobre funciones en nuestro artículo Cómo definir e invocar funciones en Go o consultar toda la serie Cómo programar en Go.



      Source link