Skip to content

Commit e3a04a9

Browse files
committed
feat: add comprehensive comparison of structs in Rust and Go
1 parent 387252b commit e3a04a9

File tree

1 file changed

+369
-0
lines changed

1 file changed

+369
-0
lines changed
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
# Estructuras (`struct`)
2+
3+
Las estructuras en Rust y Go comparten algunas similitudes superficiales:
4+
5+
- Ambas se definen con la palabra clave `struct`.
6+
- Ambas se usan para modelar tipos compuestos.
7+
- Ambas pueden tener métodos asociados (aunque se definen de forma diferente).
8+
- Pueden implementar múltiples `traits` en Rust de la misma manera que pueden
9+
implementar múltiples interfaces en Go.
10+
- Se almacenan en la pila (stack) por defecto.
11+
12+
Más adelante ahondaremos en algunas de las cosas mencionadas.
13+
14+
Sin embargo, las diferencias semánticas y de uso son profundas y reflejan los
15+
distintos modelos de diseño de cada lenguaje.
16+
17+
## Estructura y comportamiento separados (Métodos/Funciones Asociadas)
18+
19+
En Rust, struct define solo los datos. El comportamiento se define por separado
20+
en bloques `impl`.
21+
22+
En Go, los métodos se asocian mediante receivers, y no están encapsulados dentro
23+
de un bloque como en Rust.
24+
25+
Veamos este ejemplo hecho en Go:
26+
27+
```go
28+
package main
29+
30+
import (
31+
"fmt"
32+
"math"
33+
)
34+
35+
type Punto struct {
36+
X float64
37+
Y float64
38+
}
39+
40+
func CalcularDistanciaEntreDosPuntos(p1 Punto, p2 Punto) float64 {
41+
dx := p2.X - p1.X
42+
dy := p2.Y - p1.Y
43+
return math.Sqrt(dx*dx + dy*dy)
44+
}
45+
46+
func main() {
47+
puntoA := Punto{X: 1.0, Y: 2.0}
48+
puntoB := Punto{X: 4.0, Y: 6.0}
49+
distancia := CalcularDistanciaEntreDosPuntos(puntoA, puntoB)
50+
fmt.Printf("La distancia entre los puntos es: %f\n", distancia)
51+
}
52+
53+
```
54+
55+
Definimos una estructura `Punto` con dos campos `X` e `Y`, y una función
56+
`CalcularDistanciaEntreDosPuntos` que toma dos puntos y calcula la distancia
57+
entre ellos utilizando la fórmula de distancia euclidiana.
58+
59+
En Rust, el mismo concepto se implementaría de la siguiente manera:
60+
61+
```rust
62+
struct Punto {
63+
x: f64,
64+
y: f64,
65+
}
66+
67+
fn calcular_distancia_entre_dos_puntos(p1: &Punto, p2: &Punto) -> f64 {
68+
let dx = p2.x - p1.x;
69+
let dy = p2.y - p1.y;
70+
(dx.powi(2) + dy.powi(2)).sqrt()
71+
}
72+
73+
fn main() {
74+
let punto_a = Punto { x: 1.0, y: 2.0 };
75+
let punto_b = Punto { x: 4.0, y: 6.0 };
76+
let distancia = calcular_distancia_entre_dos_puntos(&punto_a, &punto_b);
77+
println!("La distancia entre los puntos es: {distancia}");
78+
}
79+
```
80+
81+
Para ejemplo practico ocultaremos la declaración de la estructura
82+
en ambos casos pero igualmente el ejemplo seguirá siendo ejecutable y
83+
podrás ver el código oculto.
84+
85+
Este ejemplo si lo pensamos tendría sentido que estuviera contenido como un
86+
método asociado a la estructura `Punto`, ya que está directamente relacionado
87+
con los datos que contiene.
88+
89+
Nosotros si deseamos definir un método o función asociada a la estructura
90+
podríamos hacerlo de la siguiente manera:
91+
92+
```rust
93+
#struct Punto {
94+
# x: f64,
95+
# y: f64,
96+
#}
97+
#
98+
impl Punto {
99+
fn calcular_distancia_a_(&self, otro: &Punto) -> f64 {
100+
let dx = otro.x - self.x;
101+
let dy = otro.y - self.y;
102+
(dx.powi(2) + dy.powi(2)).sqrt()
103+
}
104+
}
105+
106+
fn main() {
107+
let punto_a = Punto { x: 1.0, y: 2.0 };
108+
let punto_b = Punto { x: 4.0, y: 6.0 };
109+
let distancia = punto_a.calcular_distancia_a_(&punto_b);
110+
println!("La distancia entre los puntos es: {distancia}");
111+
}
112+
```
113+
114+
En donde los métodos asociados se definen dentro de un bloque `impl`
115+
que se refiere al tipo de dato `Punto`. Aquí, `&self` es una
116+
referencia al objeto actual, similar a `this` en otros lenguajes orientados a
117+
objetos.
118+
119+
En Go, podríamos definir un método asociado a la estructura de la siguiente manera:
120+
121+
```go
122+
#package main
123+
#
124+
#import (
125+
# "fmt"
126+
# "math"
127+
#)
128+
#
129+
#type Punto struct {
130+
# X float64
131+
# Y float64
132+
#}
133+
#
134+
func (p *Punto) CalcularDistanciaA(otro *Punto) float64 {
135+
dx := otro.X - p.X
136+
dy := otro.Y - p.Y
137+
return math.Sqrt(dx*dx + dy*dy)
138+
}
139+
140+
func main() {
141+
puntoA := Punto{X: 1.0, Y: 2.0}
142+
puntoB := Punto{X: 4.0, Y: 6.0}
143+
distancia := puntoA.CalcularDistanciaA(&puntoB)
144+
fmt.Printf("La distancia entre los puntos es: %f\n", distancia)
145+
}
146+
```
147+
148+
Además debemos hacer algunas aclaraciones quizás un poco obvias, tanto en Rust
149+
como en Go:
150+
151+
- No hay herencia de structs.
152+
- No hay clases.
153+
- El polimorfismo se logra por composición y traits/interfaces.
154+
155+
## Métodos y Mutabilidad
156+
157+
Profundizando en el ejemplo anterior, en Rust las variables son
158+
inmutables por defecto, por lo que si queremos modificar un campo de la
159+
estructura debemos declararla como mutable, lo mismo ocurre en métodos:
160+
161+
```rust
162+
#struct Punto {
163+
# x: f64,
164+
# y: f64,
165+
#}
166+
#
167+
impl Punto {
168+
fn mover(&mut self, dx: f64, dy: f64) {
169+
self.x += dx;
170+
self.y += dy;
171+
}
172+
173+
fn calcular_distancia_a_(&self, otro: &Punto) -> f64 {
174+
let dx = otro.x - self.x;
175+
let dy = otro.y - self.y;
176+
(dx.powi(2) + dy.powi(2)).sqrt()
177+
}
178+
}
179+
180+
fn main() {
181+
let mut punto_a = Punto { x: 1.0, y: 2.0 }; // Declaramos punto_a como mutable
182+
let punto_b = Punto { x: 4.0, y: 6.0 };
183+
184+
punto_a.mover(1.0, 0.0); // Modificamos punto_a
185+
186+
let distancia = punto_a.calcular_distancia_a_(&punto_b);
187+
println!("La distancia entre los puntos es: {distancia}");
188+
}
189+
```
190+
191+
En Go no percibiremos muchos cambios, ya que las variables son mutables por
192+
defecto:
193+
194+
```go
195+
#type Punto struct {
196+
# X float64
197+
# Y float64
198+
#}
199+
#
200+
func (p *Punto) Mover(dx, dy float64) {
201+
p.X += dx
202+
p.Y += dy
203+
}
204+
205+
func (p *Punto) CalcularDistanciaA(otro *Punto) float64 {
206+
dx := otro.X - p.X
207+
dy := otro.Y - p.Y
208+
return math.Sqrt(dx*dx + dy*dy)
209+
}
210+
211+
func main() {
212+
puntoA := Punto{X: 1.0, Y: 2.0}
213+
puntoB := Punto{X: 4.0, Y: 6.0}
214+
215+
puntoA.Mover(3.0, 4.0) // No necesitamos declarar puntoA como mutable
216+
217+
distancia := puntoA.CalcularDistanciaA(&puntoB)
218+
fmt.Printf("La distancia entre los puntos es: %f\n", distancia)
219+
}
220+
```
221+
222+
Ahora que tenemos dos metodos asociados a la estructura `Punto`, podemos ver
223+
algunas de las ventajas de Rust, las cuales son:
224+
- Los métodos deben ser definidos en un scope especifico (`impl`), lo que evita
225+
la contaminación del espacio de nombres global.
226+
- Los métodos nos exigen distinguir entre instancias mutables e inmutables,
227+
lo que nos ayuda a evitar errores o detectar fácilmente donde ocurren
228+
modificaciones no deseadas.
229+
230+
## Constructores
231+
232+
En Go y en Rust no tenemos constructores integrados en el lenguaje,
233+
pero podemos definir funciones que actúen como constructores.
234+
235+
En el caso de Rust, veremos que se respeta muchisimo la convención de
236+
un método `new` que actúa como constructor:
237+
238+
```rust
239+
#struct Punto {
240+
# x: f64,
241+
# y: f64,
242+
#}
243+
#
244+
impl Punto {
245+
fn new(x: f64, y: f64) -> Self { // 👈 método clave
246+
Punto { x, y }
247+
}
248+
249+
// Resto de métodos ...
250+
# fn mover(&mut self, dx: f64, dy: f64) {
251+
# self.x += dx;
252+
# self.y += dy;
253+
# }
254+
#
255+
# fn calcular_distancia_a_(&self, otro: &Punto) -> f64 {
256+
# let dx = otro.x - self.x;
257+
# let dy = otro.y - self.y;
258+
# (dx.powi(2) + dy.powi(2)).sqrt()
259+
# }
260+
}
261+
262+
fn main() {
263+
let mut punto_a = Punto::new(1.0, 2.0);
264+
let punto_b = Punto::new(4.0, 6.0);
265+
266+
punto_a.mover(1.0, 0.0);
267+
268+
let distancia = punto_a.calcular_distancia_a_(&punto_b);
269+
println!("La distancia entre los puntos es: {distancia}");
270+
}
271+
```
272+
273+
En Rust utilizamos el tipo de dato `Self` para referirnos al tipo actual
274+
dentro del bloque `impl`. Esto es una convención que se utiliza para
275+
indicar que el método `new` devuelve una instancia del tipo `Punto`.
276+
Puedes considerarlo como un alias para el tipo actual.
277+
278+
En Go, no hay una convención estricta para los constructores, pero podemos
279+
definir una función que actúe como constructor de la siguiente manera:
280+
281+
```go
282+
#type Punto struct {
283+
# X float64
284+
# Y float64
285+
#}
286+
#
287+
func NewPunto(x, y float64) *Punto { // 👈 función clave
288+
return &Punto{X: x, Y: y}
289+
}
290+
291+
// Resto de métodos ...
292+
293+
#func (p *Punto) Mover(dx, dy float64) {
294+
# p.X += dx
295+
# p.Y += dy
296+
#}
297+
#
298+
#func (p *Punto) CalcularDistanciaA(otro *Punto) float64 {
299+
# dx := otro.X - p.X
300+
# dy := otro.Y - p.Y
301+
# return math.Sqrt(dx*dx + dy*dy)
302+
#}
303+
#
304+
func main() {
305+
puntoA := Punto{X: 1.0, Y: 2.0}
306+
puntoB := Punto{X: 4.0, Y: 6.0}
307+
308+
puntoA.Mover(3.0, 4.0)
309+
310+
distancia := puntoA.CalcularDistanciaA(&puntoB)
311+
fmt.Printf("La distancia entre los puntos es: %f\n", distancia)
312+
}
313+
```
314+
315+
## ¿Cómo mostrar una estructura?
316+
317+
- En Rust, se implementa el trait Display o Debug para imprimir structs.
318+
319+
- En Go, se puede definir el método String() (de fmt.Stringer).
320+
321+
Generalmente lo más recomendable es que utilices el trait `Debug` en Rust,
322+
ya que es más fácil de implementar y te permite imprimir la estructura de forma
323+
rápida, sencilla y además inspeccionar de forma recursiva los campos.
324+
325+
Como ejemplo, si quisiéramos imprimir la estructura `Punto` en Rust:
326+
327+
```rust
328+
#[derive(Debug)] // 👈 El trait Debug se derivara e implementara automáticamente
329+
struct Punto {
330+
x: f64,
331+
y: f64,
332+
}
333+
334+
fn main() {
335+
let punto = Punto { x: 1.0, y: 2.0 };
336+
println!("{punto:?}", ); // 👈 Imprime la estructura usando Debug
337+
}
338+
```
339+
340+
En Rust usamos `#[derive(Debug)]` para indicar que queremos que Rust implemente
341+
el trait `Debug` para nuestra estructura `Punto`. Esto nos habilitara imprimir
342+
la estructura usando el formato `?` (el cual significa depuración) en el
343+
`println!`, nosotros podemos especificarle a Rust que tipo de formato utilizar
344+
en las macros `println!` y `format!`, entre otras, utilizando `:`.
345+
346+
De esta manera `{punto:?}` imprimirá la estructura con formato de depuración.
347+
348+
En Go no existe un equivalente directo a `#[derive(Debug)]` como en Rust.
349+
350+
```go
351+
package main
352+
353+
import (
354+
"fmt"
355+
)
356+
357+
type Punto struct {
358+
X float64
359+
Y float64
360+
}
361+
362+
func main() {
363+
p := Punto{X: 1.0, Y: 2.0}
364+
fmt.Printf("%+v\n", p) // Imprime: {X:1 Y:2}
365+
}
366+
```
367+
368+
369+
Veremos más acerca de los `traits` y los derivables más adelante.

0 commit comments

Comments
 (0)