Funciones en Go
En Go, las funciones son ciudadanos de primera clase, son el componente más básico de Go y también el núcleo de Go.
Declaración
El formato de declaración de una función es el siguiente
func nombreFunción([lista de parámetros]) [valor de retorno] {
cuerpo de la función
}Hay dos formas de declarar funciones, una es mediante la palabra clave func directamente, y la otra es mediante la palabra clave var, como se muestra a continuación
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}La firma de una función está compuesta por el nombre de la función, la lista de parámetros y el valor de retorno. A continuación se muestra un ejemplo completo, el nombre de la función es Sum, tiene dos parámetros a y b de tipo int, y el tipo de valor de retorno es int.
func Sum(a int, b int) int {
return a + b
}Hay otro punto muy importante, Go no soporta sobrecarga de funciones, el siguiente código no puede compilar
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}La filosofía de Go es que si las firmas son diferentes, son dos funciones completamente diferentes, entonces no deberían tener el mismo nombre. La sobrecarga de funciones hace que el código sea confuso y difícil de entender. Si esta filosofía es correcta o no es cuestión de opinión, al menos en Go puedes saber lo que hace una función solo por su nombre, sin tener que buscar cuál sobrecarga es.
Parámetros
En Go, los nombres de los parámetros pueden no tener nombre, generalmente esto se usa cuando se declaran interfaces o tipos de función, sin embargo, para mejorar la legibilidad, generalmente se recomienda poner nombres a los parámetros
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}Para parámetros del mismo tipo, solo se necesita declarar el tipo una vez, la condición es que deben ser adyacentes
func Log(format string, a1, a2 any) {
...
}Los parámetros de longitud variable pueden recibir 0 o más valores, deben declararse al final de la lista de parámetros, el ejemplo más típico es la función fmt.Printf.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}Cabe mencionar que en Go, los parámetros de función se pasan por valor, es decir, al pasar parámetros se copia el valor del argumento. Si crees que pasar un slice o map copiará mucha memoria, te puedo decir que no hay de qué preocuparse, porque estas dos estructuras de datos son esencialmente punteros.
Valores de retorno
A continuación se muestra un ejemplo simple del valor de retorno de una función, la función Sum devuelve un valor de tipo int.
func Sum(a, b int) int {
return a + b
}Cuando una función no tiene valor de retorno, no se necesita void, simplemente no poner valor de retorno.
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go permite que las funciones tengan múltiples valores de retorno, en este caso se deben usar paréntesis para rodear los valores de retorno.
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 no puede ser divisor")
}
return a / b, nil
}Go también soporta valores de retorno con nombre, no pueden repetirse con los nombres de los parámetros, cuando se usan valores de retorno con nombre, la palabra clave return no necesita especificar qué valores devolver.
func Sum(a, b int) (ans int) {
ans = a + b
return
}Al igual que con los parámetros, cuando hay múltiples valores de retorno con nombre del mismo tipo, se puede omitir la declaración de tipo repetida
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}Independientemente de cómo se declaren los valores de retorno con nombre, siempre prevalecen los valores después de la palabra clave return.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c y d no serán devueltos
return a + b, a * b
}Funciones anónimas
Una función anónima es una función sin firma, por ejemplo la función func(a, b int) int a continuación, no tiene nombre, así que solo podemos llamarla poniendo paréntesis inmediatamente después de su cuerpo.
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}Al llamar a una función, cuando su parámetro es un tipo de función, el nombre ya no es importante, y se puede pasar directamente una función anónima, como se muestra a continuación
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}Este es un ejemplo de ordenamiento con reglas personalizadas, slices.SortFunc acepta dos parámetros, uno es el slice y el otro es la función de comparación, si no se considera la reutilización, podemos pasar directamente una función anónima.
Clausuras
El concepto de clausura (Closure), en algunos lenguajes también se llama expresión Lambda, se usa junto con funciones anónimas, clausura = función + referencia al entorno, veamos un ejemplo
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}Salida
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512El valor de retorno de la función Exp es una función, aquí la llamaremos función grow, cada vez que se llama, la variable e crece exponencialmente. La función grow referencia dos variables de la función Exp: e y n, nacen en el ámbito de la función Exp, en circunstancias normales, a medida que termina la llamada a la función Exp, la memoria de estas variables se reciclaría al salir de la pila. Pero como la función grow las referencia, no pueden ser recicladas, sino que escapan al heap, aunque el ciclo de vida de la función Exp ha terminado, el ciclo de vida de las variables e y n no ha terminado, dentro de la función grow aún se pueden modificar directamente estas variables, la función grow es una función de clausura.
Usando clausuras, se puede implementar muy simplemente una función para calcular la sucesión de Fibonacci, el código es el siguiente
func main() {
// 10 números de Fibonacci
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}La salida es
0
1
1
2
3
5
8
13
21
34Llamada diferida
La palabra clave defer puede hacer que una función se llame de forma diferida, antes de que la función retorne, todas estas funciones descritas por defer serán ejecutadas una por una, veamos un ejemplo
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}Salida
2
1Como defer se ejecuta antes de que la función retorne, también puedes modificar el valor de retorno de la función en defer
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}Cuando hay múltiples funciones descritas por defer, se ejecutarán en orden LIFO (último en entrar, primero en salir).
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1La llamada diferida generalmente se usa para liberar recursos de archivos, cerrar conexiones de red, etc., otro uso es capturar panic, pero esto se tratará en la sección de manejo de errores.
Bucles
Aunque no está explícitamente prohibido, generalmente se recomienda no usar defer en bucles for, como se muestra a continuación
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}La salida es
4
3
2
1
0El resultado de este código es correcto, pero el proceso quizás no lo sea. En Go, cada vez que se crea un defer, se necesita solicitar un espacio de memoria en la goroutine actual. Supongamos que en el ejemplo anterior no es un simple bucle for n, sino un flujo de procesamiento de datos relativamente complejo, cuando las solicitudes externas aumentan repentinamente, se crearán una gran cantidad de defer en poco tiempo, cuando el número de iteraciones es grande o incierto, puede causar que el uso de memoria aumente repentinamente, esto generalmente se llama fuga de memoria.
Pre-cálculo de parámetros
Hay algunos detalles contraintuitivos sobre las llamadas diferidas, por ejemplo el siguiente caso
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}Este caso es muy sutil, el autor antes pasó medio día tratando de encontrar la causa debido a este problema, puedes intentar adivinar cuál es la salida, la respuesta es
2
3
1Mucha gente podría pensar que la salida es
3
2
1Según la intención del usuario, fmt.Println(Fn1()) debería ejecutarse después de que termine el cuerpo de la función, fmt.Println efectivamente se ejecuta al final, pero Fn1() es inesperado, el siguiente ejemplo hace que la situación sea aún más clara.
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}Su salida definitivamente es 3 y no 7, si se usa una clausura en lugar de una llamada diferida, el resultado es diferente
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}La salida de la clausura es 7, ¿y si combinamos la llamada diferida con la clausura?
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}Esta vez está bien, la salida es 7. Modifiquemos de nuevo, ya no hay clausura
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}La salida vuelve a ser 3. Comparando los ejemplos anteriores, se puede ver que este código
defer fmt.Println(sum(a,b))en realidad es equivalente a
defer fmt.Println(3)Go no espera hasta el final para llamar a la función sum, la función sum fue llamada mucho antes de que se ejecutara la llamada diferida, y se pasó como parámetro a fmt.Println. En resumen, para la función directamente afectada por defer, sus parámetros son pre-calculados, esto causa el fenómeno extraño del primer ejemplo, para este caso, especialmente cuando el valor de retorno de una función se usa como parámetro en una llamada diferida, se debe tener especial cuidado.
