Volver al blog

La palabra clave func en Go: funciones, métodos y closures

Cómo funciona la palabra clave func en Go: firmas de funciones, múltiples valores de retorno y resultados con nombre, funciones variádicas, receptores por valor frente a receptores puntero, valores de función, closures y variables de bucle, opciones funcionales, funciones genéricas, main e init.

La palabra clave func en Go: funciones, métodos y closures

La palabra clave func declara todo el comportamiento de un programa Go. func parse(body []byte) (Request, error) es una función. func (s *Server) Start() error es un método, porque tiene un receptor antes del nombre. func(w http.ResponseWriter, r *http.Request) { ... } sin nombre es un literal de función, que puede capturar las variables que lo rodean y es como Go escribe los closures. Una sola palabra clave cubre los tres casos, y las reglas de parámetros y resultados son las mismas en todos (especificación de Go).

Resumen rápido

  • Una declaración de función es func name(params) results { body }. Los parámetros consecutivos del mismo tipo lo comparten, como en clamp(n, lo, hi int).
  • Go no tiene sobrecarga ni argumentos por defecto. Dos funciones con el mismo nombre fallan con redeclared in this block. Usa otro nombre, un struct de opciones u opciones funcionales.
  • Las funciones pueden devolver varios valores. El par (T, error) es como Go informa de un fallo, y _ descarta un resultado que no necesitas.
  • Los resultados con nombre documentan lo que devuelve una función y permiten que un closure diferido anote err a la salida.
  • ...T hace que una función sea variádica. Dentro, el parámetro es un []T, y parts... expande un slice en la llamada.
  • Un método es una función con un receptor. Usa un receptor puntero cuando el método modifica el valor o el tipo contiene un sync.Mutex.
  • Un método con receptor puntero no está en el conjunto de métodos del tipo valor, así que API{} no satisface http.Handler cuando solo *API tiene ServeHTTP.
  • Las funciones son valores. Puedes guardarlas, pasarlas a slices.SortFunc o http.HandleFunc, y compararlas solo con nil.
  • Un literal de función captura las propias variables, no copias. Desde Go 1.22, cada iteración de un bucle tiene su propia variable de bucle, así que las goroutines lanzadas en un bucle ven el valor que esperan.
  • Las opciones funcionales (NewServer(addr, opts ...Option)) dan a los constructores ajustes opcionales. Desde Go 1.27, los métodos pueden declarar sus propios parámetros de tipo, como en func (c *Client) GetJSON[T any](path string) (T, error).

¿Cómo se declara una función en Go?

Escribe func, un nombre, la lista de parámetros y los tipos de resultado. Un parser de tokens es un buen ejemplo de función pequeña:

example.gogo
package main

import (
	"errors"
	"fmt"
	"strings"
)

func parseBearer(header string) (string, error) {
	token, ok := strings.CutPrefix(header, "Bearer ")
	if !ok || token == "" {
		return "", errors.New("missing bearer token")
	}
	return token, nil
}

func clamp(n, lo, hi int) int {
	return max(lo, min(n, hi))
}

func main() {
	token, err := parseBearer("Bearer eyJhbGciOi")
	fmt.Println(token, err)

	_, err = parseBearer("Basic dXNlcjpwYXNz")
	fmt.Println(err)

	fmt.Println(clamp(250, 1, 100))
}
example.texttext
eyJhbGciOi <nil>
missing bearer token
100

Los tipos van después de los nombres, tanto en los parámetros como en los resultados. Cuando varios parámetros seguidos comparten tipo, lo escribes una sola vez, así que clamp(n, lo, hi int) recibe tres int. Un único resultado sin nombre no necesita paréntesis. Dos o más resultados sí.

Los nombres de función siguen la misma regla de visibilidad que cualquier otro identificador de Go. ParseBearer se exportaría y se podría llamar desde otros paquetes, mientras que parseBearer se queda dentro del suyo. La palabra clave package en Go explica los nombres exportados con más detalle.

¿Tiene Go sobrecarga de funciones o argumentos por defecto?

Ni una cosa ni la otra. Un paquete solo puede tener una función con un nombre dado, sean como sean sus parámetros:

example.gogo
func dial(addr string) error { return nil }

func dial(addr string, timeout time.Duration) error { return nil }
example.texttext
./main.go:7:6: dial redeclared in this block
	./main.go:5:6: other declaration of dial

Los valores por defecto fallan ya en el parser, antes incluso de que empiece la comprobación de tipos:

example.texttext
./main.go:5:46: syntax error: unexpected = in parameter list; possibly missing comma or )

La biblioteca estándar muestra qué hace Go en su lugar. net.Dial y net.DialTimeout son dos nombres para dos comportamientos. http.Server es un struct cuyos campos a cero significan «usa el valor por defecto», así que solo rellenas los campos que te interesan. Para constructores con muchos ajustes opcionales, el código Go usa opciones funcionales, que se explican más abajo. El nombre en cada llamada te dice qué función se ejecuta.

¿Cómo funcionan los múltiples valores de retorno en Go?

Una función puede devolver cualquier número de valores, y quien la llama los recibe con una asignación múltiple. La forma más habitual es un resultado más un error, como en parseBearer de arriba. Quien llama comprueba el error antes de tocar el resultado, y el compilador se asegura de que una llamada con dos resultados no se use donde se espera un solo valor.

Cuando no necesitas uno de los valores, asígnalo al identificador en blanco _. No puedes omitirlo sin más. token := parseBearer(h) falla con assignment mismatch: 1 variable but parseBearer returns 2 values.

¿Cuándo conviene usar parámetros de resultado con nombre?

Los resultados con nombre le dan un nombre a cada resultado y lo declaran como una variable que empieza con su valor cero. Un return sin valores, llamado bare return (return desnudo), devuelve entonces lo que contengan esas variables:

example.gogo
func splitHostPort(addr string) (host, port string) {
	for i := len(addr) - 1; i >= 0; i-- {
		if addr[i] == ':' {
			host, port = addr[:i], addr[i+1:]
			return
		}
	}
	host = addr
	return
}

splitHostPort("api.levelupgo.dev:443") devuelve api.levelupgo.dev y 443. Los nombres ayudan aquí porque, sin ellos, dos resultados string no dirían nada sobre cuál es cuál. Los Go Code Review Comments recomiendan los bare returns solo en funciones cortas como esta. En una función de 60 líneas, un return desnudo oculta lo que se está devolviendo.

Donde más importan los resultados con nombre es cuando una función diferida tiene que modificar el error antes de que llegue a quien llama:

example.gogo
func loadConfig(path string) (cfg Config, err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("load config %s: %w", path, err)
		}
	}()

	data, err := os.ReadFile(path)
	if err != nil {
		return Config{}, err
	}
	err = json.Unmarshal(data, &cfg)
	return cfg, err
}
example.texttext
load config /etc/levelupgo/config.json: open /etc/levelupgo/config.json: no such file or directory

Cada return asigna primero sus valores a cfg y err, y después se ejecuta la función diferida. Como err es un resultado con nombre, el closure puede envolverlo una sola vez para todas las salidas. Sin el nombre, la función diferida no tiene forma de llegar al valor que se devuelve.

¿Qué son las funciones variádicas en Go?

Un último parámetro escrito como ...T acepta cero o más argumentos de tipo T. Dentro de la función es un []T normal:

example.gogo
func buildURL(base string, segments ...string) string {
	return strings.TrimSuffix(base, "/") + "/" + strings.Join(segments, "/")
}

func main() {
	fmt.Println(buildURL("https://api.levelupgo.dev/", "v1", "users", "42"))
	fmt.Println(buildURL("https://api.levelupgo.dev"))

	parts := []string{"v1", "orders", "1001"}
	fmt.Println(buildURL("https://api.levelupgo.dev", parts...))
}
example.texttext
https://api.levelupgo.dev/v1/users/42
https://api.levelupgo.dev/
https://api.levelupgo.dev/v1/orders/1001

parts... pasa un slice existente como argumento variádico sin copiarlo. No puedes mezclar las dos formas, así que buildURL(base, "v2", parts...) falla:

example.texttext
./main.go:18:58: too many arguments in call to buildURL
	have (string, string, []string...)
	want (string, ...string)

La biblioteca estándar está llena de ellas. fmt.Println(a ...any), append(s, elems...), errors.Join(errs ...error) y los pares clave-valor de slog.Info(msg, args ...any) usan todos esta forma.

¿Cómo funcionan los métodos en Go?

Un método es una función con un receptor, escrito entre paréntesis entre func y el nombre. Los receptores suelen ser structs, que la palabra clave struct en Go explica desde los campos hasta la incrustación. El receptor puede ser el propio tipo o un puntero a él, y esa elección decide si el método trabaja sobre el valor de quien llama o sobre una copia. Un limitador de tasa con receptor por valor muestra la diferencia:

example.gogo
type RateLimiter struct {
	limit int
	used  int
}

func (r RateLimiter) Allow() bool {
	if r.used >= r.limit {
		return false
	}
	r.used++
	return true
}

func main() {
	limiter := RateLimiter{limit: 2}
	for range 4 {
		fmt.Print(limiter.Allow(), " ")
	}
	fmt.Println()
}
example.texttext
true true true true

El limitador nunca bloquea nada. Cada llamada recibe una copia nueva de limiter, incrementa used en la copia y la descarta. Cambiar el receptor a func (r *RateLimiter) Allow() bool hace que el método actualice el original, y la salida pasa a ser true true false false. No tienes que escribir (&limiter).Allow(). Go toma la dirección por ti cuando la variable es direccionable.

¿Cuándo conviene usar un receptor puntero?

Usa un receptor puntero cuando se cumpla cualquiera de estas condiciones:

  • El método modifica el receptor, como Allow de arriba.
  • El tipo contiene un sync.Mutex u otro valor que no debe copiarse.
  • El struct es grande, así que copiarlo en cada llamada cuesta más que seguir un puntero.
Receptor por valor func (r T)Receptor puntero func (r *T)
Modifica el valor de quien llamaNo, trabaja sobre una copiaSí
Seguro con un campo sync.MutexNo, el lock se copiaSí
Se puede llamar sobre una variable TSíSí, Go toma la dirección
En el conjunto de métodos de TSíNo
En el conjunto de métodos de *TSíSí

El caso del mutex es un bug que detecta go vet:

example.gogo
type SessionCache struct {
	mu       sync.Mutex
	sessions map[string]string
}

func (c SessionCache) Get(token string) (string, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	user, ok := c.sessions[token]
	return user, ok
}
example.texttext
main.go:13:9: Get passes lock by value: app.SessionCache contains sync.Mutex

Cada llamada bloquea su propia copia del mutex, así que dos goroutines que llaman a Get nunca se esperan entre sí y el lock no protege nada. Los tipos pequeños e inmutables como time.Time o un UserID están bien con receptores por valor. Los Code Review Comments también piden que no mezcles los dos en un mismo tipo. Si un método necesita un receptor puntero, dáselo a todos (wiki de Go).

¿Por qué mi tipo no implementa una interfaz?

Una causa habitual es un receptor puntero. El conjunto de métodos de un tipo valor T solo incluye los métodos con receptor T. El conjunto de métodos de *T incluye los dos tipos de receptor. Así que cuando ServeHTTP tiene un receptor puntero, solo *API es un http.Handler:

example.gogo
type API struct {
	version string
}

func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, a.version)
}

func main() {
	var h http.Handler = API{version: "v1"}
	_ = h
}
example.texttext
./main.go:17:23: cannot use API{…} (value of struct type API) as http.Handler value in variable declaration: API does not implement http.Handler (method ServeHTTP has pointer receiver)

Escribir &API{version: "v1"} lo soluciona. La regla existe porque una interfaz puede contener una copia de un valor, y un método con receptor puntero llamado sobre esa copia cambiaría algo que quien llama nunca ve. La palabra clave var en Go muestra el patrón var _ http.Handler = (*T)(nil), que lo comprueba en tiempo de compilación, y la palabra clave interface en Go explica a fondo los conjuntos de métodos y la satisfacción implícita.

¿Qué son los method values y las method expressions?

srv.health sin paréntesis es un method value. Es una función con el receptor ya enlazado, así que puedes pasarla directamente a cualquier cosa que espere una función normal:

example.gogo
type Server struct {
	version string
}

func (s *Server) health(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "ok %s\n", s.version)
}

func main() {
	srv := &Server{version: "1.4.2"}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", srv.health)

	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, httptest.NewRequest("GET", "/health", nil))
	fmt.Print(rec.Body.String())

	handle := (*Server).health
	fmt.Printf("%T\n", handle)
}
example.texttext
ok 1.4.2
func(*main.Server, http.ResponseWriter, *http.Request)

Muchos servidores Go registran las rutas así. Los handlers son métodos sobre un struct que contiene la base de datos, el logger y la configuración, y mux.HandleFunc recibe los method values enlazados. (*Server).health es una method expression. Convierte el receptor en un primer parámetro normal, lo que resulta útil cuando una tabla elige en tiempo de ejecución qué método llamar.

¿Qué son los valores de función y los tipos función?

Las funciones son valores en Go. Puedes asignarlas a variables, guardarlas en campos de struct y en maps, pasarlas como argumentos y devolverlas desde otras funciones. El tipo de un valor de función es su firma:

example.gogo
type Order struct {
	ID    int64
	Total int64
}

func main() {
	orders := []Order{{ID: 1, Total: 4999}, {ID: 2, Total: 1250}, {ID: 3, Total: 8900}}

	slices.SortFunc(orders, func(a, b Order) int {
		return cmp.Compare(b.Total, a.Total)
	})
	fmt.Println(orders)

	bigOrder := func(o Order) bool { return o.Total > 5000 }
	fmt.Println(slices.IndexFunc(orders, bigOrder))
	fmt.Printf("%T\n", bigOrder)
}
example.texttext
[{3 8900} {1 4999} {2 1250}]
0
func(main.Order) bool

slices.SortFunc no sabe cómo ordenar tus structs, así que le pasas una función de comparación. La misma idea recorre toda la biblioteca estándar: http.HandleFunc recibe una función handler, strings.FieldsFunc recibe una prueba de separador y sync.OnceValue recibe la función que se ejecuta una sola vez.

El valor cero de un tipo función es nil, y llamar a una función nil provoca un panic. Esa es también la única comparación que Go permite:

example.texttext
./main.go:9:5: invalid operation: health == ready (func can only be compared to nil)

Como no puedes comparar dos funciones, tampoco pueden ser claves de un map. Cuando quieras un conjunto de handlers, usa su nombre como clave del map.

Una firma también puede tener su propio nombre con type, y un tipo función con nombre puede tener métodos. Así es como http.HandlerFunc convierte una función normal en un http.Handler, y la palabra clave type en Go lo explica paso a paso junto con un ejemplo de RetryPolicy.

¿Cómo funcionan los closures en Go?

Un literal de función puede usar variables de la función que lo rodea. No las copia. Captura las propias variables, así que los cambios de cualquiera de los dos lados son visibles para ambos. Un helper de reintentos lo muestra:

example.gogo
var errUnavailable = errors.New("503 service unavailable")

func retry(attempts int, fn func() error) error {
	var err error
	for range attempts {
		if err = fn(); err == nil {
			return nil
		}
	}
	return err
}

func main() {
	calls := 0
	err := retry(3, func() error {
		calls++
		return errUnavailable
	})
	fmt.Println(calls, err)
}
example.texttext
3 503 service unavailable

calls vive en main, pero el literal lo incrementa, y main ve 3 después. La variable capturada sigue viva mientras algún closure haga referencia a ella, incluso después de que la función que la rodea retorne. El compilador mueve esas variables al heap cuando lo necesita.

El middleware HTTP es uno de los closures más habituales en código Go. El handler devuelto conserva logger y next de la llamada que lo construyó:

example.gogo
func logRequests(logger *slog.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		next.ServeHTTP(w, r)
		logger.Info("request", "method", r.Method, "path", r.URL.Path)
	})
}

Si envuelves un handler y le mandas DELETE /sessions/42, queda registrado level=INFO msg=request method=DELETE path=/sessions/42. Cada llamada a logRequests produce un closure distinto con su propio next, así que puedes envolver cada ruta con un handler diferente. Para ver la pila completa de producción construida con closures como este, con el orden, la recuperación de panics y los tests, consulta buenas prácticas de middleware en Go.

También verás mucho dos formas cortas. defer func() { ... }() ejecuta un closure cuando la función retorna, que es como el ejemplo loadConfig anota su error y como se llama a recover. go func() { ... }() lanza un closure en una goroutine nueva. El () final llama al literal. Si te lo dejas, el compilador se queja de que la expresión en defer o go debe ser una llamada a función.

¿Arregló Go 1.22 los closures en bucles?

Sí. Antes de Go 1.22, un bucle for tenía una sola variable de bucle compartida por todas las iteraciones, y los closures capturaban esa única variable. Este comprobador de salud lanzaba tres goroutines que a menudo imprimían todas la última URL:

example.gogo
urls := []string{"/health", "/ready", "/metrics"}

var wg sync.WaitGroup
for _, url := range urls {
	wg.Add(1)
	go func() {
		defer wg.Done()
		fmt.Println("checking", url)
	}()
}
wg.Wait()

Con go 1.21 en go.mod, todas las líneas de tres ejecuciones distintas imprimieron checking /metrics, y go vet avisó con loop variable url captured by func literal. Con go 1.22 o posterior, cada iteración tiene su propio url, y el mismo código comprueba las tres rutas en el orden en que terminen las goroutines (blog de Go). El cambio depende de la línea go de go.mod, así que un módulo antiguo mantiene el comportamiento antiguo hasta que subas esa versión. La vieja copia url := url dentro del bucle ya no hace falta, y el modernizador forvar de go fix la elimina. La palabra clave for en Go cubre las demás reglas de las variables de bucle, como por qué cambiar el valor del range no cambia el slice.

¿Qué son las opciones funcionales en Go?

Las opciones funcionales combinan parámetros variádicos y closures para dar a un constructor ajustes opcionales sin sobrecarga ni argumentos por defecto. Cada opción es una función que modifica el valor que se está construyendo:

example.gogo
type Server struct {
	addr         string
	readTimeout  time.Duration
	maxBodyBytes int64
	logger       *slog.Logger
}

type Option func(*Server)

func WithReadTimeout(d time.Duration) Option {
	return func(s *Server) {
		s.readTimeout = d
	}
}

func WithMaxBodyBytes(n int64) Option {
	return func(s *Server) {
		s.maxBodyBytes = n
	}
}

func NewServer(addr string, opts ...Option) *Server {
	s := &Server{
		addr:         addr,
		readTimeout:  5 * time.Second,
		maxBodyBytes: 1 << 20,
		logger:       slog.Default(),
	}
	for _, opt := range opts {
		opt(s)
	}
	return s
}

func main() {
	a := NewServer(":8080")
	b := NewServer(":8080", WithReadTimeout(30*time.Second), WithMaxBodyBytes(10<<20))
	fmt.Println(a.readTimeout, a.maxBodyBytes)
	fmt.Println(b.readTimeout, b.maxBodyBytes)
}
example.texttext
5s 1048576
30s 10485760

NewServer(":8080") recibe todos los valores por defecto, y quien llama solo nombra lo que cambia. Añadir una opción más adelante no rompe ninguna llamada existente. grpc.NewServer(opts ...ServerOption) de gRPC y muchos clientes de bases de datos usan este patrón. Para un tipo con dos o tres ajustes, un struct de configuración normal es más simple y más fácil de leer, así que recurre a las opciones cuando la lista de ajustes no deja de crecer.

¿Cómo funcionan las funciones genéricas en Go?

Una función puede declarar parámetros de tipo entre corchetes, entre su nombre y sus parámetros. Map funciona con cualquier tipo de entrada y de salida:

example.gogo
func Map[T, U any](items []T, fn func(T) U) []U {
	out := make([]U, 0, len(items))
	for _, item := range items {
		out = append(out, fn(item))
	}
	return out
}

emails := Map(users, func(u User) string { return u.Email })

El compilador infiere T como User y U como string a partir de los argumentos, así que la llamada no necesita corchetes. Los paquetes slices, maps y cmp de la biblioteca estándar están construidos con funciones genéricas como esta.

Hasta Go 1.27, los métodos no podían declarar sus propios parámetros de tipo. Podían usar los parámetros de tipo de su tipo receptor, y nada más. Go 1.27 quita ese límite, así que un cliente puede tener un método genérico que decodifica en el tipo que pida quien llama (notas de la versión Go 1.27):

example.gogo
type Client struct {
	baseURL string
	http    *http.Client
}

func (c *Client) GetJSON[T any](path string) (T, error) {
	var out T
	resp, err := c.http.Get(c.baseURL + path)
	if err != nil {
		return out, err
	}
	defer resp.Body.Close()
	err = json.NewDecoder(resp.Body).Decode(&out)
	return out, err
}

u, err := c.GetJSON[User]("/users/1")

Con un servidor de pruebas que devuelve un usuario, esto imprime {1 [email protected]} <nil>. Aquí hay que escribir el argumento de tipo porque nada en "/users/1" le dice al compilador qué es T. Novedades de Go 1.27 explica los métodos genéricos y sus límites, incluido por qué no pueden satisfacer métodos de interfaz.

¿Qué son main e init en Go?

Hay dos nombres de función especiales. func main() en package main es donde empieza un programa, y el programa termina cuando retorna. No recibe argumentos ni devuelve nada:

example.texttext
./main.go:9:6: func main must have no arguments and no return values

Los argumentos de línea de comandos vienen de os.Args o del paquete flag, y el código de salida de os.Exit. La palabra clave package en Go explica package main a fondo.

func init() se ejecuta automáticamente después de que se inicialicen las variables del paquete y antes de main. Un paquete puede tener varias funciones init, incluso en un mismo archivo, y se ejecutan en el orden en que aparecen. No puedes llamar a ninguna tú mismo. init() en tu propio código falla con undefined: init. init se usa sobre todo para registrar cosas, como un driver de base de datos que se añade a sí mismo a database/sql. La palabra clave var en Go explica el orden de inicialización y cuándo un var a nivel de paquete es la opción más clara.

Dónde encaja LevelUpGo

LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Go Basics presenta las funciones, los múltiples valores de retorno y los errores como valores. Go Language Deep Dives profundiza en las funciones variádicas, los resultados con nombre, panic y recover. El Training Ground tiene ejercicios cortos e independientes sobre firmas, métodos y closures para practicar fuera de un curso. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.

Preguntas frecuentes

¿Es func una palabra clave en Go?

Sí. func es una de las 25 palabras clave reservadas de Go, así que no puedes usarla como nombre. Declara funciones y métodos, e inicia los literales de función y los tipos función. Cuando el código Go necesita una variable que guarde una función, suele llamarla fn.

¿Admite Go la sobrecarga de funciones o los parámetros por defecto?

Ni una cosa ni la otra. Cada nombre solo se puede declarar una vez por paquete, así que un segundo func dial(...) falla con dial redeclared in this block, y un valor por defecto en una lista de parámetros es un error de sintaxis. Go usa nombres distintos, como net.Dial y net.DialTimeout, un struct de configuración cuyos campos a cero significan «usa el valor por defecto», como http.Server, u opciones funcionales.

¿Debo usar un receptor por valor o un receptor puntero en Go?

Usa un receptor puntero cuando el método modifica el receptor, cuando el tipo contiene un sync.Mutex o algo similar, o cuando el struct es grande. Usa un receptor por valor para tipos pequeños que se comportan como valores, como time.Time. Mantén coherentes todos los métodos de un mismo tipo. Recuerda que solo *T tiene los métodos puntero en su conjunto de métodos, y eso importa para satisfacer interfaces.

¿Qué es func() en Go?

func() es el tipo de una función que no recibe argumentos y no devuelve nada. Cualquier firma escrita sin nombre es un tipo función, como func(context.Context) error. Estos tipos se usan para callbacks, campos de struct y parámetros como sync.OnceFunc(f func()). func() { ... } con cuerpo es un literal de función de ese tipo.

¿Se pueden comparar funciones en Go?

Solo con nil. health == ready falla con func can only be compared to nil, y por la misma razón un tipo función no puede ser clave de un map. Cuando necesites buscar handlers, guárdalos en un map[string]http.HandlerFunc con el nombre como clave.

Fuentes

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita