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) 
Output48 

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) 
Output929.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:

Output4294967295 

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

Output0 

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:

Outputprog.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:

Outputfalse 

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) 
OutputSay "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:

OutputSay "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) 
OutputThis 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) 
OutputSay "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.

Output0: 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) 
Outputmap[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"]) 
Outputblue 

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.