Las estructuras en Go proporcionando una forma de agrupar datos relacionados bajo un mismo tipo. A diferencia de los lenguajes orientados a objetos tradicionales, Go no tiene clases, pero las estructuras permiten una funcionalidad similar al permitir la creación de tipos complejos que pueden contener múltiples campos de diferentes tipos.
Este artículo es bastante extenso así que lo he organizado de la siguiente manera:
- Estructura básica.
- Estructuras anónimas.
- Métodos asociados a estructuras.
- Composition.
- Encapsulación de estructuras.
- Comparar estructuras
Si eres nuevo en el mundo de Go te recomiendo los siguientes artículos anteriores:
Estructura básica:
Imaginemos que queremos gestionar personas desde nuestro programa y los datos de las personas constan de varios datos, como el nombre, la edad y el sexo. Podríamos crearnos un map para la edad y otro para el sexo ambos utilizando el nombre como índice, pero resulta mucho mas sencillo una estructura.
En el siguiente ejemplo creamos dos objetos de tipo persona con distintos datos cada uno.
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func main() {
person1 := person {
name: "John",
age: 25,
sex: "Male",
}
person2 := person {
name: "Sarah",
age: 45,
sex: "Female",
}
fmt.Println(person1)
fmt.Println("Name: ", person1.name)
fmt.Println("Age: ", person1.age)
fmt.Println("Sex: ", person1.sex)
fmt.Println(person2)
fmt.Println("Name: ", person2.name)
fmt.Println("Age: ", person2.age)
fmt.Println("Sex: ", person2.sex)
}
{John 25 Male}
Name: John
Age: 25
Sex: Male
{Sarah 45 Female}
Name: Sarah
Age: 45
Sex: Female
Estructuras anónimas:
Cuando una estructura incorpora otra, no hace falta definir el campo como de tipo estructura2 en estructura1, este tipo de campos son llamados anónimos.
Veamos primero un ejemplo en el que si se define el tipo.
package main
import (
"fmt"
)
func main() {
type personalData struct {
name string
age int
sex string
}
type client struct {
person personalData
address string
telephone int
}
client1 := client{
person: personalData{
name: "John",
age: 25,
sex: "Male",
},
address: "Paper street",
telephone: 666334455,
}
fmt.Println("client1 name: ", client1.person.name)
fmt.Println("client1 age: ", client1.person.age)
fmt.Println("client1 sex: ", client1.person.sex)
fmt.Println("client1 address: ", client1.address)
fmt.Println("client1 telephone: ", client1.telephone)
}
client1 name: John
client1 age: 25
client1 sex: Male
client1 address: Paper street
client1 telephone: 666334455
La versión con campos anónimos quedaría del siguiente modo.
package main
import (
"fmt"
)
func main() {
type personalData struct {
name string
age int
sex string
}
type client struct {
// Anonymous field
personalData
address string
telephone int
}
personalDataClient1 := personalData{
name: "John",
age: 25,
sex: "Male",
}
client1 := client{
// Set structure values omitting field names
personalDataClient1,
"Paper street",
666334455,
}
// Direct personalData fields access using client1
fmt.Println("client1 name: ", client1.name)
fmt.Println("client1 age: ", client1.age)
fmt.Println("client1 sex: ", client1.sex)
fmt.Println("client1 address: ", client1.address)
fmt.Println("client1 telephone: ", client1.telephone)
}
client1 name: John
client1 age: 25
client1 sex: Male
client1 address: Paper street
client1 telephone: 666334455
Es posible incluso crear estructuras sin crear un tipo de antemano pudiendo crearlas “al vuelo” y de un solo uso.
package main
import (
"fmt"
)
func main() {
client1 := struct {
name string
age int
sex string
address string
telephone int
}{
name: "John",
age: 25,
sex: "Male",
address: "Paper street",
telephone: 666334455,
}
fmt.Println("client1 name: ", client1.name)
fmt.Println("client1 age: ", client1.age)
fmt.Println("client1 sex: ", client1.sex)
fmt.Println("client1 address: ", client1.address)
fmt.Println("client1 telephone: ", client1.telephone)
}
client1 name: John
client1 age: 25
client1 sex: Male
client1 address: Paper street
client1 telephone: 666334455
La única limitación de los campos anónimos es que no puede tratarse de un slice.
package main
import (
"fmt"
)
func main() {
type client struct {
name string
address string
telephone int
}
type clientList struct {
// Anonymous slice Not allowed
[]client
}
client1 := client{
name: "John",
address: "Paper street",
telephone: 666334455,
}
client2 := client{
name: "Sarah",
address: "Inexistent avenue",
telephone: 555667788,
}
var clientSlice []client
clientSlice = append(clientSlice, client1, client2)
clientList1 := clientList{
clientSlice,
}
for _, v := range clientList1 {
fmt.Println(v.name)
fmt.Println(v.address)
fmt.Println(v.telephone)
}
}
./test.go:15:9: syntax error: unexpected [, expected field name or embedded type
Para que funcione debemos de dejar de utilizar campos anónimos.
package main
import (
"fmt"
)
func main() {
type client struct {
name string
address string
telephone int
}
type clientList struct {
clients []client
}
client1 := client{
name: "John",
address: "Paper street",
telephone: 666334455,
}
client2 := client{
name: "Sarah",
address: "Inexistent avenue",
telephone: 555667788,
}
var clientSlice []client
clientSlice = append(clientSlice, client1, client2)
clientList1 := clientList{
clientSlice,
}
for _, v := range clientList1.clients {
fmt.Println(v.name)
fmt.Println(v.address)
fmt.Println(v.telephone)
}
}
John
Paper street
666334455
Sarah
Inexistent avenue
555667788
Métodos asociados a estructuras:
Como vincular métodos a tipo ya se explicó en un artículo anterior sobre funciones , pero nunca viene mal refrescar la memoria.
En este ejemplo vincularemos un tipo de tipo estructura al método area(), en dicho método obtendremos una copia del objeto en si mismo, Go lo llama receiver
en algunos lenguajes de programación lo llaman this
, la forma idiomática de nombrar a dicho receiver es utilizar la primera letra del tipo del objeto.
package main
import "fmt"
type rectangle struct {
Base int
Height int
}
func (r rectangle) area() int {
return r.Base * r.Height
}
func main() {
// Assign structure variable values
rect := rectangle{Base: 3, Height: 4}
// rect structure moreover has area method available
// Call structure method
area := rect.area()
fmt.Println(area)
}
12
Los métodos presentan varias ventajas respecto a utilizar funciones:
- Los métodos quedan vinculados de forma lógica a los tipos.
- Los nombres de los métodos pueden ser reutilizados en los distintos tipos.
Composition:
En Go, el composition se refiere a la capacidad de incrustar un tipo en otro heredando sus comportamientos, extensión de funcionalidad de un tipo existente incorporando otros tipos que proporcionan funcionalidades adicionales, además de una mayor simplicidad y claridad en el diseño al permitir una estructura más modular y fácil de entender.
En este sencillo ejemplo tenemos tres estructuras:
- person: Estructura común con un método
personMethod
. - student: Incluye un campo de tipo
person
, añade el stringgrade
y tiene un método propiostudentMethod
. - teacher: Incluye un campo de tipo
person
, añade el intteaching_years
y tiene un método propioteacherMethod
.
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) personMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
personData person
grade string
}
func (s student) studentMethod() {
fmt.Println("studentMethod accessed")
}
type teacher struct {
personData person
teachingYears int
}
func (t teacher) teacherMethod() {
fmt.Println("teacherMethod accessed")
}
func main() {
student := student {
personData: person {
name: "Ana",
age: 20,
sex: "Female",
},
grade: "engineering",
}
teacher := teacher {
personData: person {
name: "Joe",
age: 30,
sex: "Male",
},
teachingYears: 10,
}
fmt.Println(student)
fmt.Println("Name: ", student.personData.name)
fmt.Println("Age: ", student.personData.age)
fmt.Println("Sex: ", student.personData.sex)
fmt.Println("Grade: ", student.grade)
student.personData.personMethod()
student.studentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.personData.name)
fmt.Println("Age: ", teacher.personData.age)
fmt.Println("Sex: ", teacher.personData.sex)
fmt.Println("TeachingYears: ", teacher.teachingYears)
teacher.personData.personMethod()
teacher.teacherMethod()
}
Ejecutamos el código:
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
Las estructuras de Go soportan campos anónimos, esto quiere decir que podemos definir campos en las estructuras solo por el tipo, no hace falta indicar un nombre, de este modo el tipo quedará incluido. Utilizando campos anónimos al acceder a los campos de las estructuras o los métodos asociados obtendremos un código mas claro y sencillo.
El ejemplo anterior utilizando campos anónimos quedaría de la siguiente manera.
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) personMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
grade string
}
func (s student) studentMethod() {
fmt.Println("studentMethod accessed")
}
type teacher struct {
person
teachingYears int
}
func (t teacher) teacherMethod() {
fmt.Println("teacherMethod accessed")
}
func main() {
student := student {
person: person {
name: "Ana",
age: 20,
sex: "Female",
},
grade: "engineering",
}
teacher := teacher {
person: person {
name: "Joe",
age: 30,
sex: "Male",
},
teachingYears: 10,
}
fmt.Println(student)
fmt.Println("Name: ", student.name)
fmt.Println("Age: ", student.age)
fmt.Println("Sex: ", student.sex)
fmt.Println("Grade: ", student.grade)
student.personMethod()
student.studentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.name)
fmt.Println("Age: ", teacher.age)
fmt.Println("Sex: ", teacher.sex)
fmt.Println("TeachingYears: ", teacher.teachingYears)
teacher.personMethod()
teacher.teacherMethod()
}
Ejecutamos el código:
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
Encapsulación de estructuras:
Hay ocasiones en las que necesitamos estar seguros de que los campos de una estructura tiene valores asignados, en el ejemplo anterior podríamos querer asegurarnos de que una persona siempre tenga un nombre o que su edad no sea negativa por ejemplo.
Una forma de asegurarse de que un objeto de un tipo cumpla con los datos requeridos es utilizar la encapsulación, esta consiste en forzar la instanciación del tipo a través de ciertos métodos.
Con el código actual podríamos instanciar una persona sin nombre:
package main
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) personMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
grade string
}
func (s student) studentMethod() {
fmt.Println("studentMethod accessed")
}
type teacher struct {
person
teachingYears int
}
func (t teacher) teacherMethod() {
fmt.Println("teacherMethod accessed")
}
func main() {
student := student {
person: person {
age: -20,
sex: "Female",
},
grade: "engineering",
}
teacher := teacher {
person: person {
age: -30,
sex: "Male",
},
teachingYears: 10,
}
fmt.Println(student)
fmt.Println("Name: ", student.name)
fmt.Println("Age: ", student.age)
fmt.Println("Sex: ", student.sex)
fmt.Println("Grade: ", student.grade)
student.personMethod()
student.studentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.name)
fmt.Println("Age: ", teacher.age)
fmt.Println("Sex: ", teacher.sex)
fmt.Println("TeachingYears: ", teacher.teachingYears)
teacher.personMethod()
teacher.teacherMethod()
}
Dando como resultado, el campo Name vacío:
{{ -20 Female} engineering}
Name:
Age: -20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{ -30 Male} 10}
Name:
Age: -30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
Mediante encapsulación vamos a resolver dicho problema, el primer paso es hacer que las estructuras no sean accesibles de forma directa, esto se consigue moviéndolas a otro package. Cuando un tipo, variable o función se encuentra en otro package y sus nombres empiezan por minúsculas, esos tipos, variables o funciones no serán expuestos a otros packages.
La idea de la encapsulación es mover las estructuras a otro package y crear métodos que accedan a ellas, donde estos métodos si que serán exportados ya que sus nombres empezarán con mayúsculas quedando de este modo visibles desde otros packages.
Como detalle final debemos tener en cuenta que los nombres de los campos y los métodos también deben empezar por mayúscula si queremos acceder a dichos campos o métodos, en caso contrario tendremos una estructura de la que no podremos leer datos ni ejecutar métodos.
Para que el módulo siga la estructura de un proyecto en Go, inicializamos un módulo.
cd structureTest
go mod init structureTest
El primer paso será mover las estructuras a otro package y crear los métodos.
vi structures/structures.go
package structures
import "fmt"
type person struct {
Name string
Age int
Sex string
}
func (p person) PersonMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
Grade string
}
func (s student) StudentMethod() {
fmt.Println("studentMethod accessed")
}
// New code, uppercase to make it available from outside current package
func NewStudent(name string, age int, sex, grade string) (student, error) {
newStudent := student{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || sex == "Female") && len(grade) > 0 {
newStudent = student {
person: person{
Name: name,
Age: age,
Sex: sex,
},
Grade: grade,
}
return newStudent, nil
} else {
return newStudent, fmt.Errorf("Error: Incorrect student data input.")
}
}
type teacher struct {
person
TeachingYears int
}
func (t teacher) TeacherMethod() {
fmt.Println("teacherMethod accessed")
}
// New code, uppercase to make it available from outside current package
func NewTeacher(name string, age int, sex string, teachingYears int) (teacher, error) {
newTeacher := teacher{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || sex == "Female") && teachingYears > 0 {
newTeacher = teacher {
person: person{
Name: name,
Age: age,
Sex: sex,
},
TeachingYears: teachingYears,
}
return newTeacher, nil
} else {
return newTeacher, fmt.Errorf("Error: Incorrect teacher data input.")
}
}
Ahora desde el fichero principal debemos importar el package y llamar al método para instanciar el estudiante y el maestro.
package main
import (
"fmt"
"structureTest/structures"
)
func main() {
student, err := structures.NewStudent("Ana", 20, "Female", "engineering")
if err != nil {
fmt.Println("Error obtaining structures.NewStudent:", err)
return
}
teacher, err := structures.NewTeacher("Joe", 30, "Male", 10)
if err != nil {
fmt.Println("Error obtaining structures.NewTeacher:", err)
return
}
fmt.Println(student)
fmt.Println("Name: ", student.Name)
fmt.Println("Age: ", student.Age)
fmt.Println("Sex: ", student.Sex)
fmt.Println("Grade: ", student.Grade)
student.PersonMethod()
student.StudentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.Name)
fmt.Println("Age: ", teacher.Age)
fmt.Println("Sex: ", teacher.Sex)
fmt.Println("TeachingYears: ", teacher.TeachingYears)
teacher.PersonMethod()
teacher.TeacherMethod()
fmt.Println("-----")
student2, err := structures.NewStudent("", 20, "Female", "engineering")
if err != nil {
fmt.Println("Error obtaining structures.NewStudent:", err)
return
}
fmt.Println(student2)
}
Ejecutando el código podemos ver que funciona sin problemas, incluso no permite crear un alumno con el nombre vacío.
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
TeachingYears: 10
personMethod accessed
teacherMethod accessed
-----
Error obtaining structures.NewStudent: Error: Incorrect student data input.
La encapsulación no se limita al momento de instanciación de objetos, también puede utilizarse para realizar lecturas(getters) o escrituras(setters) de los campos de una estructura, esto nos permitirá programar una lógica de lecturas y escrituras de campos totalmente personalizada.
Para ello los campos de la estructura deben estar en minúsculas para que sean privados y debemos escribir un métodos para leer/setear valores. Los nombres idiomáticos de los getters/setters es el nombre del campo para los getters y SetNOMBRECAMPO para los setters.
package structures
import "fmt"
type person struct {
name string
age int
sex string
}
func (p person) PersonMethod() {
fmt.Println("personMethod accessed")
}
type student struct {
person
grade string
}
func (s student) StudentMethod() {
fmt.Println("studentMethod accessed")
}
func (s student) Name() string {
return s.name
}
func (s student) Age() int {
return s.age
}
func (s student) Sex() string {
return s.sex
}
func (s student) Grade() string {
return s.grade
}
func (s *student) SetName(newName string) {
s.name = newName
}
func (s *student) SetAge(newAge int) {
s.age = newAge
}
func (s *student) SetSex(newSex string) {
s.sex = newSex
}
func (s *student) SetGrade(newGrade string) {
s.grade = newGrade
}
func NewStudent(name string, age int, sex, grade string) (student, error) {
newStudent := student{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || sex == "Female") && len(grade) > 0 {
newStudent = student{
person: person{
name: name,
age: age,
sex: sex,
},
grade: grade,
}
return newStudent, nil
} else {
return newStudent, fmt.Errorf("Error: Incorrect student data input.")
}
}
type teacher struct {
person
teachingYears int
}
func (t teacher) TeacherMethod() {
fmt.Println("teacherMethod accessed")
}
func (t teacher) Name() string {
return t.name
}
func (t teacher) Age() int {
return t.age
}
func (t teacher) Sex() string {
return t.sex
}
func (t teacher) TeachingYears() int {
return t.teachingYears
}
func (t *teacher) SetName(newName string) {
t.name = newName
}
func (t *teacher) SetAge(newAge int) {
t.age = newAge
}
func (t *teacher) SetSex(newSex string) {
t.sex = newSex
}
func (t *teacher) SetTeachingYears(newteachingYears int) {
t.teachingYears = newteachingYears
}
func NewTeacher(name string, age int, sex string, teachingYears int) (teacher, error) {
newTeacher := teacher{}
if len(name) > 0 && age > 18 && age < 120 && (sex == "Male" || sex == "Female") && teachingYears > 0 {
newTeacher = teacher{
person: person{
name: name,
age: age,
sex: sex,
},
teachingYears: teachingYears,
}
return newTeacher, nil
} else {
return newTeacher, fmt.Errorf("Error: Incorrect teacher data input.")
}
}
package main
import (
"fmt"
"structureTest/structures"
)
func main() {
student, err := structures.NewStudent("Ana", 20, "Female", "engineering")
if err != nil {
fmt.Println("Error obtaining structures.NewStudent:", err)
return
}
teacher, err := structures.NewTeacher("Joe", 30, "Male", 10)
if err != nil {
fmt.Println("Error obtaining structures.NewTeacher:", err)
return
}
fmt.Println(student)
fmt.Println("Name: ", student.Name())
fmt.Println("Age: ", student.Age())
fmt.Println("Sex: ", student.Sex())
fmt.Println("Grade: ", student.Grade())
student.PersonMethod()
student.StudentMethod()
fmt.Println("-----")
fmt.Println(teacher)
fmt.Println("Name: ", teacher.Name())
fmt.Println("Age: ", teacher.Age())
fmt.Println("Sex: ", teacher.Sex())
fmt.Println("Grade: ", teacher.TeachingYears())
teacher.PersonMethod()
teacher.TeacherMethod()
fmt.Println("--- Resetting student --")
student.SetName("Kevin Mitnick")
student.SetAge(66)
student.SetSex("Male")
student.SetGrade("Hacker")
fmt.Println(student)
fmt.Println("Name: ", student.Name())
fmt.Println("Age: ", student.Age())
fmt.Println("Sex: ", student.Sex())
fmt.Println("Grade: ", student.Grade())
student.PersonMethod()
student.StudentMethod()
fmt.Println("--- Resetting teacher --")
teacher.SetName("Richard Stallman")
teacher.SetAge(99)
teacher.SetSex("Male")
teacher.SetTeachingYears(20)
fmt.Println(teacher)
fmt.Println("Name: ", teacher.Name())
fmt.Println("Age: ", teacher.Age())
fmt.Println("Sex: ", teacher.Sex())
fmt.Println("Grade: ", teacher.TeachingYears())
teacher.PersonMethod()
teacher.TeacherMethod()
}
Ejecutando el código podemos ver que sigue funcionando, pero esta vez se accede a los valores a través de loe getters y se cambian los valores a través de los setters:
{{Ana 20 Female} engineering}
Name: Ana
Age: 20
Sex: Female
Grade: engineering
personMethod accessed
studentMethod accessed
-----
{{Joe 30 Male} 10}
Name: Joe
Age: 30
Sex: Male
Grade: 10
personMethod accessed
teacherMethod accessed
--- Resetting student --
{{Kevin Mitnick 66 Male} Hacker}
Name: Kevin Mitnick
Age: 66
Sex: Male
Grade: Hacker
personMethod accessed
studentMethod accessed
--- Resetting teacher --
{{Richard Stallman 99 Male} 20}
Name: Richard Stallman
Age: 99
Sex: Male
Grade: 20
personMethod accessed
teacherMethod accessed
Comparar estructuras:
Las estructuras se podrán comparar siempre y cuando tengan el mismo número y tipo de campos.
package main
import (
"fmt"
)
type name struct {
firstName string
lastName string
}
func main() {
name1 := name{
firstName: "Steve",
lastName: "Jobs",
}
name2 := name{
firstName: "Steve",
lastName: "Jobs",
}
if name1 == name2 {
fmt.Println("name1 and name2 are equal")
} else {
fmt.Println("name1 and name2 are not equal")
}
name3 := name{
firstName: "Steve",
lastName: "Jobs",
}
name4 := name{
firstName: "Steve",
}
if name3 == name4 {
fmt.Println("name3 and name4 are equal")
} else {
fmt.Println("name3 and name4 are not equal")
}
}
name1 and name2 are equal
name3 and name4 are not equal
Pero si algún campo no es comparable, como ocurre con los maps
que no es posible compararlos, obtendremos un error.
package main
import (
"fmt"
)
type image struct {
data map[int]int
}
func main() {
image1 := image{
data: map[int]int{
0: 155,
}}
image2 := image{
data: map[int]int{
0: 155,
}}
if image1 == image2 {
fmt.Println("image1 and image2 are equal")
}
}
./06-structures.go:20:8: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)