Interfaces
- Go no es un lenguaje OOP
- No soporta herencia (JAG approves)
- Soporta interfaces (polimorfismo)
- La implementacion de una interfaz no es explicita
interface{}
seria el equivalente a java.lang.Object
.
Cualquier tipo "implementa" un interfaz vacia
Type Assertion
El operador para el type assertion es ".(type)
"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // panic
fmt.Println(f)
}
Type switches
Esto seria como un switch mezclado con un instanceof
. Adicionalmente, dentro del case
la variable ya fue convertida al tipo del case.
func main() {
do(21)
do("hello")
do(true)
}
func do(i interface{}) {
switch v := i.(type) {
case int:
// v ya esta convertido a int
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
// v ya esta convertido a string
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
Error Handling
Golang es un poco verboso con los errores. Basicamente, cualquier metodo/funcion que puede fallar, retorna como ultimo parametro un error
. error
es una interface
type error interface {
Error() string
}
Sentinel errors
Los sentinel errors (errores definidos estaticamente), forman parte del API pública porque el usuario del api va a comparar contra ese error.
var ErrInvalidKey = errors.New("invalid key")
func (s *service) Get(key string) (*Value, error) {
if len(key) == 0 {
return nil, ErrInvalidKey
}
}
...
if err := service.Get(""); err == ErrFoo {
// handles ErrInvalidKey
}
A la hora de comprar contra sentinel errors, ss preferible usar
errors.Is
al operator==
Custom Error types
Crear un tipo de error custom que implemente la interface error
, y pueda tener informacion relevante.
type BarError struct {
Reason string
}
func (e BarError) Error() string {
return fmt.Sprintf("bar error: %s", e.Reason)
}
Wrapping errors
Es una buena practica, no retornar errores que devuelvan otras API, sino mas bien wrappearlos y que devuelvan adicianalmente un contexto mayor del fallo. En ese caso se tiene que proveer adicionalmente la forma de hacer el Unwrap
.
type BazError struct {
Reason string
Inner error
}
func (e BazError) Error() string {
if e.Inner != nil {
return fmt.Sprintf("baz error: %s: %v", e.Reason, e.Inner)
}
return fmt.Sprintf("baz error: %s", e.Reason)
}
func (e BazError) Unwrap() error {
return e.Inner
}
Tambien hay formas de wrappear errores, ni necesidad de usar un custom error type.
func process(j Job) error {
result, err := preprocess(j)
if err != nil {
// wrapeamos el error de preprocess con un mensaje para dar contexto del error.
return fmt.Errorf("error preprocessing job: %w", err)
}
...
By default, when you encounter an error in a function and need to return it to the caller, wrap it with some context about what went wrong, using
fmt.Errorf
and the new%w
verb.
Para validad si un error wrappea a otro error en particular, usamos errors.Is
que cheaquea recursivamente si internamente el error es de ese tipo.
err := f()
if errors.Is(err, ErrFoo) {
// you know you got an ErrFoo
// respond appropriately
}
Si queremos validar si un error
es de un tipo en particular (type assertion), es preferible utilizar errors.As
a utilizar el operador de type assertion .()
.
var bar *BarError
if errors.As(err, &bar) {
// you know you got a BarError
// bar's fields are populated
// respond appropriately
}
En el ejemplo de arriba, As
retorna true
si err
es del tipo BarError
y dentro del cuerpo del if, la variable bar
esta correctamente seteada.
Concurrency
mutex (semaforos)
type ConcurrentCounter struct {
mutex *sync.Mutex
count int
}
func (c *ConcurrentCounter) Add(value int) int {
mutex.Lock()
defer mutex.Unlock()
c.count += 1
return c.count
}
Tambien existen tambien lost RWMutex
, que permiten ser mas granulares con las primitivas de acceso.
func (c *ConcurrentCounter) Get() int {
mutex.RLock()
defer mutex.RUnlock()
return c.count
}
En este caso, multiples go routines pueden obtener el RLock
de forma simultanea.
Waitgroup
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
// llamada bloqueante q espera todos los wg.Done()
// similar a un join para threads/procesos
wg.Wait()
}
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
Channels
- Son colas de comunicacion sincrónica entre go routines
- Tienen multiples formas de uso
- Tanto los reads como los writes son bloqueantes
Algunos ejemplos
Como implementar Futures con channels
future := make(chan int, 1)
go func() { future <- process() }()
result := <-future
Como implementar Async + await
c := make(chan int, 1)
go func() { c <- process() }() // async
v := <-c // await
Scatter + Gather
// Scatter
c := make(chan result, 10)
for i := 0; i < cap(c); i++ {
go func() {
val, err := process()
c <- result{val, err}
}()
}
// Gather
var total int
for i := 0; i < cap(c); i++ {
res := <-c
if res.err != nil {
total += res.val
}
}
Fan-out
func main() {
concurrency := 10
// el segundo parametro, es para poder escribir
c := make(chan int, concurrency)
// lanzo el fan-out con 10 go routines
fanOut(c, concurrency)
for i := 0; i < 100; i ++ {
// mando los valores en el channel de entrada
c <- i
}
// cierro el channel para que los workers salgan del for + range
close(c)
}
func fanOut(input <-chan int, concurrency int) {
for i := 0; i < concurrency; i++ {
go func() {
for value := range input {
// process value
process(value)
}
}()
}
}
Fan-in
func fanIn(input1, input2 <-chan int ) <-chan int {
c := make(chan int)
go func() {
for {
select {
case s:= <- input1
c <- s
case s:= <- input2
c <- s
}
}
}()
return c
}
Hay formas de hacer esto mismo, pero mas genérico con un array de channels
Repository Structure
The basic idea is to have two top-level directories, pkg and cmd. Underneath pkg, create directories for each of your libraries. Underneath cmd, create directories for each of your binaries. All of your Go code should live exclusively in one of these locations.
github.com/peterbourgon/foo/
circle.yml
Dockerfile
cmd/
foosrv/
main.go
foocli/
main.go
pkg/
fs/
fs.go
fs_test.go
mock.go
mock_test.go
merge/
merge.go
merge_test.go
api/
api.go
api_test.go
Dep Injection
Resulta similar a la manera en la que usamos los @Inject
por constructor, aunque en go necesitamos pasar las dependencias explicitamente por parametro al construir el type
type UserRepository interface {
GetUser(id string) (*User, error)
// tambien podria devolver un User
}
type RetrieveUser struct {
repo UserRepository
}
// Constructor para dep injection
func NewRetrieveUser(repo UserRepository) *RetrieveUser {
return &RetrieveUser{repo: repo}
}
func (r *RetrieveUser) Retrieve(id string) (*User, error) {
return r.repo.GetUser(id)
}
Context & cancel propagation
Funciones del pacakge context
// retorna el context y la funcion q puedo llamar para cancelarlo
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// retorna contextos que se cancelan automaticamente expirado el tiempo definido. La funcion de cancel se usar para liberar los recursos de timers alocados para los TOs.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// Esta dos funciones retornan un context "vacio", aunque tienen propositos semánticos de uso distintos.
func Background() Context
func TODO() Context
// context con valores, similar a lo que vemos en flux
func WithValue(parent Context, key, val interface{}) Context
Ejemplo de cancelacion con timeout
// recibo un context por parametro
func main() {
// retorna un context generico "vacio"
ctx := context.Background()
// creo un context con timeout despues de 3 segundos
ctx, cancel := context.WithTimeout(ctx, time.Second * 3)
// me aseguro de llamar la func cancel para liberar los recursos de los timers del TO
defer cancel()
// hago la llamada a foo, con el ctx + TO
foo(ctx)
}
func foo(ctx context.Context) {
// creo el channel para la respuesta
future := make(chan int, 1)
go func() {
// process invoca un api call que demora algunos segundos
future <- process()
}()
// el select ejecuta el primer evento que llegue de alguno de estos dos channel
select {
case value <- future:
// proceso la respuesta del api call
fmt.Println(value)
case <- ctx.Done():
// se vencio el TO antes q finalice la llamada a process
fmt.Println("el context se cancelo antes que termine el api call")
}
}