1. Code
  2. Coding Fundamentals

Buceo Profundo En el Go Type System

Scroll to top

Spanish (Español) translation by Jean Perez (you can also view the original English article)

Go tiene un sistema de tipo muy interesante. Evita las clases y la herencia a favor de interfaces y composición, pero por otro lado no tiene plantillas o genéricos. La manera que maneja colecciones también es única.

En este tutorial usted aprenderá sobre los entresijos de la Go type system y cómo utilizarlo eficazmente para escribir código Go claro e idiomático.

El Panorama del Go type system

El sistema de tipo Go soporta los paradigmas procesales, orientado a objetos y funcionales. Tiene muy limitado soporte para programación genérica. Mientras que el Go es un lenguaje decididamente estático, proporciona suficiente flexibilidad para técnicas de dinámica a través de interfaces, funciones de primera clase y la reflexión. Sistema de tipo de ir carece de capacidades que son comunes en las lenguas más modernas:

  • No hay ningún tipo de excepción desde el error de ir manejo se basa en la interfaz de error y códigos de retorno.
  • No hay ninguna sobrecarga de operadores.
  • No hay ninguna función de sobrecarga (mismo nombre de función con parámetros diferentes).
  • Hay no opcional o por defecto de parámetros de la función.

Esas omisiones son todos de diseño con el fin de hacer más sencillo posible.

Tipos de Alias

Usted puede tipos de alias en ir y crear tipos distintos. Se puede asignar un valor del tipo subyacente a un tipo de alias sin conversión. Por ejemplo, la asignación var b int = a el siguiente programa produce un error de compilación porque el tipo de Age es un alias de int, pero no es un int:

1
package main
2
3
4
type Age int
5
6
func main() {
7
    var a Age = 5
8
    var b int = a
9
}
10
11
12
Output:
13
14
tmp/sandbox547268737/main.go:8: cannot use a (type Age) as
15
type int in assignment

Puede agrupar las declaraciones de tipo o utilizar una declaración por línea:

1
type IntIntMap map[int]int
2
StringSlice []string
3
4
type (
5
    Size   uint64
6
    Text  string
7
    CoolFunc func(a int, b bool)(int, error)
8
)

Tipos Básicos

Todos los sospechosos de siempre están aquí: bool, cadenas, enteros y enteros sin signo con tamaños poco explícita, complejo números (64-bit y 128-bit) y números de coma flotante (32 bits y 64 bits).

1
bool
2
string
3
int  int8  int16  int32  int64
4
uint uint8 uint16 uint32 uint64 uintptr
5
byte // alias for uint8
6
rune // alias for int32, represents a Unicode code point
7
float32 float64
8
complex64 complex128

Cadenas

Cadenas en GO son UTF8 codificada y por lo tanto pueden representar cualquier carácter Unicode. El paquete de cadenas proporciona una serie de operaciones de cadena. Aquí está un ejemplo tomando un conjunto de palabras, convertir al caso correcto y junto a ellos a una oración.

1
package main
2
3
import (
4
    "fmt"
5
    "strings"
6
)
7
8
func main() {
9
    words := []string{"i", "LikE", "the ColORS:", "RED,", 
10
                      "bLuE,", "AnD", "GrEEn"}
11
    properCase := []string{}
12
    
13
    for i, w := range words {
14
        if i == 0 {
15
            properCase = append(properCase, strings.Title(w))
16
        } else {
17
            properCase = append(properCase, strings.ToLower(w))
18
        }
19
    }
20
    
21
    sentence := strings.Join(properCase, " ") + "."
22
    fmt.Println(sentence)
23
}

Punteros

Go tiene punteros. El puntero nulo (ver más adelante valores cero) es nulo. Usted puede obtener un puntero a un valor con el & operador y obtener de nuevo usando el * operador. Puede tener punteros a punteros también.

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
8
type S struct {
9
    a float64
10
    b string
11
}
12
13
func main() {
14
    x := 5
15
    px := &x
16
    *px = 6
17
    fmt.Println(x)
18
    ppx := &px
19
    **ppx = 7
20
    fmt.Println(x)
21
}

Programación Orientada a Objetos

Go soporta programación orientada a objetos a través de interfaces y estructuras. Hay no hay clases y no hay jerarquía de clases, aunque puede integrar estructuras anónimas dentro de estructuras, que ofrecen a una especie de herencia simple.

Para una exploración detallada de la programación orientada a objetos en ir, echa un vistazo Let's Go: Golang Programación Orientado a Objetos.

Interfaces

Las interfaces son la piedra angular del sistema de tipo de Go. Una interfaz es simplemente una colección de firmas de método. Cada tipo que implementa todos los métodos es compatible con la interfaz. Aquí está un ejemplo rápido. La interfaz de Shape define dos métodos: GetPerimeter() y GetArea(). El objeto Square implementa la interfaz.

1
type Shape interface {
2
    GetPerimeter() uint
3
    GetArea() uint
4
}
5
6
type Square struct {
7
   side  uint
8
}
9
10
func (s *Square) GetPerimeter() uint {
11
    return s.side * 4
12
}
13
14
func (s *Square) GetArea() uint {
15
    return s.side * s.side
16
}

La interfaz vacío interface{} es compatible con cualquier tipo, porque no existen métodos que se requieren. La interfaz vacíela entonces puede apuntar a cualquier objeto (similar a Java del objeto o puntero void C y C++) y a menudo se utiliza para mecanografiar dinámico. Interfaces siempre son punteros y apuntan a un objeto concreto.

Para un artículo entero sobre interfaces ir, echa un vistazo: Cómo Definir e Implementar una Interfaz de GO.

Estructuras

Las estructuras son tipos definidos por el usuario de Go. Una estructura contiene los campos nombre, que pueden ser tipos básicos, los tipos de puntero u otros tipos de estructura. También puede integrar estructuras de forma anónima en otras estructuras como una forma de herencia de implementación.

En el ejemplo siguiente, las estructuras de S1 y S2 están incrustadas en la estructura S3, que también tiene su propio campo de int y un puntero a su tipo:

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
8
type S1 struct {
9
    f1 int
10
}
11
12
type S2 struct {
13
    f2 int
14
}
15
16
type S3 struct {
17
    S1
18
    S2
19
    f3 int
20
    f4 *S3
21
}
22
23
24
func main() {
25
    s := &S3{S1{5}, S2{6}, 7, nil}
26
    
27
    fmt.Println(s)
28
}
29
30
Output:
31
32
&{{5} {6} 7 <nil>}

Tipo de Afirmaciones

Tipo de afirmaciones le permiten convertir una interfaz a su tipo concreto. Si ya sabes el tipo subyacente, sólo puede afirmarla. Si no está seguro, puede tratar de varias afirmaciones de tipo hasta que descubres el tipo correcto.

En el ejemplo siguiente, hay una lista de cosas que contiene cadenas y valores de cadena no representados como un segmento de interfaces vacías. El código recorre en iteración todas las cosas, tratando de convertir cada elemento en una cadena y almacenar todas las cadenas en un segmento independiente que finalmente se imprime.

1
package main
2
3
import "fmt"
4
5
func main() {
6
    things := []interface{}{"hi", 5, 3.8, "there", nil, "!"}
7
    strings := []string{}
8
    
9
    for _, t := range things {
10
        s, ok := t.(string)
11
        if ok {
12
            strings = append(strings, s)
13
        }
14
    }
15
    
16
    fmt.Println(strings)
17
    
18
}
19
20
Output:
21
22
[hi there !]

Reflexión

El paquete de reflect Go le permite comprobar directamente el tipo de una interfaz sin afirmaciones de tipo. También puede extraer el valor de una interfaz y convertirlo en una interfaz si desea (no tan útil).

Aquí hay un ejemplo similar al ejemplo anterior, pero en lugar de las cadenas de impresión sólo cuenta, por lo que no es necesario convertir de interfaz{} a cadena. La clave es llamar a reflect.Type() para obtener un objeto de tipo, que tiene un método Kind() que nos permite detectar si tratamos con una cadena o no.

1
package main
2
3
import (
4
    "fmt"
5
    "reflect"
6
)
7
8
9
func main() {
10
    things := []interface{}{"hi", 5, 3.8, "there", nil, "!"}
11
    stringCount := 0
12
    
13
    for _, t := range things {
14
        tt := reflect.TypeOf(t)
15
        if tt != nil && tt.Kind() == reflect.String {
16
            stringCount++
17
        }
18
    }
19
    
20
    fmt.Println("String count:", stringCount)
21
}

Funciones

Funciones son ciudadanos de primera clase en Go. Eso significa que puede asignar funciones a variables, pasar funciones como argumentos a otras funciones o devolver como resultados. Le permite utilizar el estilo de programación funcional con ir.

En el ejemplo siguiente se muestra un par de funciones, GetUnaryOp() y GetBinaryOp(), que devuelven funciones anónimas seleccionadas al azar. El programa principal decide si necesita una operación unaria o una operación binaria basada en el número de argumentos. Almacena la función seleccionada en una variable local llamada "op" y entonces invoca con el número correcto de argumentos.

1
package main
2
3
import (
4
    "fmt"
5
    "math/rand"
6
)
7
8
type UnaryOp func(a int) int
9
type BinaryOp func(a, b int) int
10
11
12
func GetBinaryOp() BinaryOp {
13
    if rand.Intn(2) == 0 {
14
        return func(a, b int) int { return a + b }
15
    } else {
16
        return func(a, b int) int { return a - b }
17
    }
18
}
19
20
func GetUnaryOp() UnaryOp {
21
    if rand.Intn(2) == 0 {
22
        return func(a int) int { return -a }
23
    } else {
24
        return func(a int) int { return a * a }
25
    }
26
}
27
28
29
func main() {
30
    arguments := [][]int{{4,5},{6},{9},{7,18},{33}}
31
    var result int
32
    for _, a := range arguments {
33
        if len(a) == 1 {
34
            op := GetUnaryOp()
35
            result = op(a[0])            
36
        } else {
37
            op := GetBinaryOp()
38
            result = op(a[0], a[1])                    
39
        }
40
        fmt.Println(result)                
41
    }
42
}

Canales

Los canales son un tipo de datos inusuales. Usted puede pensar en ellos como mensaje colas utilizadas para pasar mensajes entre goroutines. Los canales son fuertemente tipados. Se sincronizan y han dedicado soporte de sintaxis para enviar y recibir mensajes. Cada canal puede recibir sólo, sólo enviar o bidireccional.

Canales también pueden ser opcionalmente protegidos. Se puede iterar sobre los mensajes en un canal con variedad e ir rutinas pueden bloquear en varios canales al mismo tiempo con la operación de selección versátil.

Aquí está un ejemplo típico donde se calcula la suma de los cuadrados de una lista de enteros en paralelo por las rutinas de ir dos, cada uno responsable de la mitad de la lista. La función principal espera por el resultado de ambas rutinas irás y luego se suma las sumas parciales para el total. Nota cómo el canal c se crea utilizando la función incorporada de make() y cómo el código Lee y escribe en el canal mediante el especial <- operador.

1
package main
2
3
import "fmt"
4
5
func sum_of_squares(s []int, c chan int) {
6
    sum := 0
7
    for _, v := range s {
8
        sum += v * v
9
    }
10
    c <- sum // send sum to c
11
}
12
13
func main() {
14
    s := []int{11, 32, 81, -9, -14}
15
16
    c := make(chan int)
17
    go sum_of_squares(s[:len(s)/2], c)
18
    go sum_of_squares(s[len(s)/2:], c)
19
    sum1, sum2 := <-c, <-c // receive from c
20
    total := sum1 + sum2
21
22
    fmt.Println(sum1, sum2, total)
23
}

Esto es apenas raspando la superficie. Para una revisión detallada de los canales, echa un vistazo:

Colecciones

Go tiene varias colecciones genéricas incorporados que pueden almacenar cualquier tipo. Estas colecciones son especiales, y no se pueden definir sus propio colecciones genéricas. Las colecciones son matrices, cortes y mapas. Canales también son genéricos y pueden ser considerados colecciones demasiado, pero tienen algunas propiedades bastante únicas, por lo que prefiero discutir por separado.

Matrices

Las matrices son colecciones de tamaño fijo de elementos del mismo tipo. Estos son algunos arreglos:

1
package main
2
import "fmt"
3
4
5
func main() {
6
    a1 := [3]int{1, 2, 3}
7
    var a2 [3]int
8
    a2 = a1 
9
10
    fmt.Println(a1)
11
    fmt.Println(a2)
12
    
13
    a1[1] = 7
14
15
    fmt.Println(a1)
16
    fmt.Println(a2)
17
    
18
    a3 := [2]interface{}{3, "hello"}
19
    fmt.Println(a3)
20
}

Tamaño de la matriz es parte de su tipo. Puede copiar matrices del mismo tipo y tamaño. La copia es por valor. Si desea almacenar elementos de tipo diferente, puede utilizar la escotilla de escape de una gran variedad de interfaces de vacías.

Rebanadas

Las matrices son muy limitadas debido a su tamaño fijo. Rebanadas son mucho más interesantes. Usted puede pensar de rebanadas como matrices dinámicas. Bajo las sábanas, rebanadas de utilizan una matriz para almacenar sus elementos. Usted puede comprobar la longitud de un segmento, añadir elementos y otros sectores, y más divertido de todos que se puede extraer las rodajas similar a Python rebanar:

1
package main
2
3
import "fmt"
4
5
6
7
func main() {
8
    s1 := []int{1, 2, 3}
9
    var s2 []int
10
    s2 = s1 
11
12
    fmt.Println(s1)
13
    fmt.Println(s2)
14
15
    // Modify s1    
16
    s1[1] = 7
17
18
    // Both s1 and s2 point to the same underlying array
19
    fmt.Println(s1)
20
    fmt.Println(s2)
21
    
22
    fmt.Println(len(s1))
23
    
24
    // Slice s1
25
    s3 := s1[1:len(s1)]
26
    
27
    fmt.Println(s3)
28
}

Cuando copie rebanadas, solo copie la referencia a la misma matriz subyacente. Cuando rebana, el sub-segmento todavía señala el mismo arreglo de discos. Pero cuando usted agregar, un sector que apunta a una nueva matriz.

Puede iterar sobre matrices o rebanadas utilizando un lazo regular con índices o gamas. También puede crear divisiones en una capacidad determinada que se inicializará con el valor cero de su tipo de datos mediante la función make():

1
package main
2
3
import "fmt"
4
5
6
7
func main() {
8
    // Create a slice of 5 booleans initialized to false    
9
    s1 := make([]bool, 5)
10
    fmt.Println(s1)
11
    
12
    s1[3] = true
13
    s1[4] = true
14
15
    fmt.Println("Iterate using standard for loop with index")
16
    for i := 0; i < len(s1); i++ {
17
        fmt.Println(i, s1[i])
18
    }
19
    
20
    fmt.Println("Iterate using range")
21
    for i, x := range(s1) {
22
        fmt.Println(i, x)
23
    }
24
}
25
26
Output:
27
28
[false false false false false]
29
Iterate using standard for loop with index
30
0 false
31
1 false
32
2 false
33
3 true
34
4 true
35
Iterate using range
36
0 false
37
1 false
38
2 false
39
3 true
40
4 true

Mapas

Los mapas son colecciones de pares de clave y valor. Se pueden asignar literales de mapa u otros mapas. También puede crear mapas de vacíos utilizando la función incorporada de make. Usted tener acceso a elementos mediante corchetes. Mapas de apoyan iteración usando range, y puede comprobar si una clave existe tratando de acceder a él y comprobando el valor devuelto booleano opcional segunda.

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
func main() {
8
    // Create map using a map literal
9
    m := map[int]string{1: "one", 2: "two", 3:"three"}
10
    
11
    // Assign to item by key
12
    m[5] = "five"
13
    // Access item by key
14
    fmt.Println(m[2])
15
    
16
    v, ok := m[4]
17
    if ok {
18
        fmt.Println(v)
19
    } else {
20
        fmt.Println("Missing key: 4")
21
    }
22
    
23
    
24
    for k, v := range m {
25
        fmt.Println(k, ":", v)
26
    }
27
}
28
29
Output:
30
31
two
32
Missing key: 4
33
5 : five
34
1 : one
35
2 : two
36
3 : three

Tenga en cuenta que la iteración no está en orden de creación o inserción.

Valores Cero

No hay ningún tipo sin inicializar en ir. Cada tipo tiene un predefinido cero valor. Si se declara una variable de un tipo sin asignarle un valor, que contiene su valor cero. Esta es una característica de seguridad importante.

Para cualquier tipo T, *new(T) devolverá un cero valor de T.

De tipo booleano, el valor cero es "falso". Para tipos numéricos, la cero valor es... cero. Láminas, mapas y punteros, es nulo. Para estructuras, es una estructura donde todos los campos se inicializan con su valor de cero.

1
package main
2
3
import (
4
    "fmt"
5
)
6
7
8
type S struct {
9
    a float64
10
    b string
11
}
12
13
func main() {
14
    fmt.Println(*new(bool))
15
    fmt.Println(*new(int))
16
    fmt.Println(*new([]string))
17
    fmt.Println(*new(map[int]string))
18
    x := *new([]string)
19
    if x == nil {
20
        fmt.Println("Uninitialized slices are nil")
21
    }
22
23
    y := *new(map[int]string)
24
    if y == nil {
25
        fmt.Println("Uninitialized maps are nil too")
26
    }
27
    fmt.Println(*new(S))
28
}

¿Plantillas o Genéricos?

Go no tiene ninguno. Esta es probablemente la queja más común sobre el sistema de ir. Los diseñadores van están abiertos a la idea, pero no sé todavía cómo implementar sin violar los principios de diseño subyacentes a la lengua. ¿Qué puede hacer si usted necesita gravemente algunos tipos de datos genéricos? Aquí están algunas sugerencias:

  • Si sólo tienes unas pocas instancias, considere a crear objetos concretos.
  • Utilice una interfaz vacía (usted necesitará tipo afirmar a su tipo concreto en algún momento).
  • Usar generación de código.

Conclusión

Go tiene un interesante sistema de tipo. Los diseñadores de Go decisiones explícitas para quedarse en el simple lado del espectro. Si eres serio sobre Go programación, debe invertir el tiempo y conocer su sistema y su idiosincrasia. Será bien vale la pena su tiempo.