Cómo Obtener un Objeto de S3 en Go
Tags: aws, principiante, go, espaol
Published: 2019-04-14 13:00:00

---
title: Cómo Obtener un Objeto de S3 en Go
date: "2019-04-14T13:00:00.284Z"
tags: aws, principiante, go, español
series: Aprendiendo Go
published: true
---
_Publicado originalmente en [mi blog](https://maurogestoso.github.io/blog/2019/como-obtener-un-objeto-de-s3-en-go/)_

_Para este ejemplo, asumo que ya tienes `go` instalado y `aws-cli` instalado y configurado con un perfil de AWS._

En este ejemplo quiero mostrar cómo obtener el contenido de un objeto almacenado en AWS S3 de forma programática usando Go.

Para seguir este ejemplo en tu máquina necesitas:

- Un nuevo proyecto de Go
- El SDK de AWS para Go, lo puedes descargar con este comando: `go get github.com/aws/aws-sdk-go`
- Un archivo de texto en un _bucket_ de S3, un object JSON por ejemplo. Apunta el nombre del _bucket_ y la dirección al objeto.

## 1️⃣ Crea un Cliente de S3

En [la documentación del servicio `s3`](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#New), uno de los primeros métodos que encuentro es `New()`, que recibe configuración y devuelve un tipo `*S3`, que es nuestro cliente inicializado. Este cliente nos da acceso a todos los métodos públicos del servicio S3.

```go
package main

import (
  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/service/s3"
)

func main() {
  s3Client := s3.New(session.New(), &aws.Config{
    Region: aws.String("eu-west-1"),
  })
}

```

- El primer argumento de `s3.New()` es una `session`. No se muy bien para que sirve porque todavía no lo necesité.
- El segundo argumento de `s3.New()` es del tipo `*aws.Config` y aquí especifico en que región de AWS quiero trabajar. No entiendo por qué el SDK no la lee automáticamente de mi configuración de perfil (situada en `~/.aws/config`)...

## 2️⃣ Invoca el Método `GetObject()`

Ahora podemos invocar el método `GetObject()` para obtener el objeto que queremos. En [la documentación del método](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#S3.GetObject) vemos que recibe un argumento de tipo `*GetObjectInput` en el que podemos especificar el nombre del _bucket_ y la dirección del objeto que habíamos apuntado al principio (si todavía no has creado un objeto en S3, ahora es un buen momento 😉).

```go
package main

import (
  // 👇 imports nuevos 👇
  "fmt"
  "log"
  // ☝️ imports nuevos ️️☝️

  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/service/s3"
)

func main() {
  s3Client := s3.New(session.New(), &aws.Config{
    Region: aws.String("eu-west-1"),
  })
  // ☝️ código existente ☝️

  // 👇 código nuevo 👇
  bucket := "mi-bucket"
  key := "ejemplo.json"

  result, err := s3Client.GetObject(&s3.GetObjectInput{
    Bucket: aws.String(bucket),
    Key:    aws.String(key),
  })
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(result)
}
```

- AWS nos da una utilidad para convertir _strings_ al formato correcto. No entiendo muy bien por qué no puedo pasar los _strings_ directamente, pero parace ser una práctica aceptada (copiado y pegado de la documentación)
- En la terminal vemos que imprimimos algo así como:

```
{
  AcceptRanges: "bytes",
  Body: buffer(0xc0003d6100),
  ContentLength: 116,
  ContentType: "binary/octet-stream",
  ETag: "\"65ae86d6ce8c3139528a137657122255\"",
  LastModified: 2019-04-14 10:35:37 +0000 UTC,
  Metadata: {

  }
}
```

El contenido del objeto debería estar bajo `Body`, pero vemos que recibimos algo llamado `buffer` y lo que parece información binaria representada en hexadecimal. Interesante 🤔.

## 3️⃣ Interpreta los Resultados

En [la documentación del tipo `GetObjectOutput`](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#GetObjectOutput) confirmamos que el contenido del objecto está en la propiedad `Body`. Pero por qué no vemos el texto que guardamos en nuestro archivo en S3? La clave está en que S3 puede almacenar cualquier formato de información, por lo que tiene sentido que nos devuelva esa información en el denominador común de la comunicación digital: binario. Ahora es nuestro trabajo interpretar esta información y convertirla a un formato más útil para nosotros.

En la documentación vemos que `Body` es del tipo `io.ReadCloser`. La _interface_ `io.Reader` es una de las más usadas en Go, la implementa por ejemplo el resultado de leer un archivo del disco duro. Como yo lo entiendo, es la forma que tiene Go de representar información binaria en memoria que se puede leer. El tipo `io.ReadCloser` quiere decir que implementa la _interface_ `Reader` y `Closer`, por lo tanto podemos leer la información y convertirla a otro formato.

Como te podrás imaginar, no soy un experto en Go, así que no te voy a engañar: en este punto fui directamente a preguntarle a Don Google y encontré [esta pregunta en Stack Overflow](https://stackoverflow.com/questions/9644139/from-io-reader-to-string-in-go).

```go
package main

import (
  "bytes"
  "fmt"
  "log"

  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/service/s3"
)

func main() {
  s3Client := s3.New(session.New(), &aws.Config{
    Region: aws.String("eu-west-1"),
  })

  bucket := "mi-bucket"
  key := "ejemplo.json"

  result, err := s3Client.GetObject(&s3.GetObjectInput{
    Bucket: aws.String(bucket),
    Key:    aws.String(key),
  })
  if err != nil {
    log.Fatal(err)
  }
  // ☝️ código existente ☝️

  // 👇 código nuevo 👇
  defer result.Body.Close()

  buf := new(bytes.Buffer)
  buf.ReadFrom(result.Body)

  fmt.Println(buf.String())
}
```

- Como el _buffer_ permite cerrarlo (recuerda, también implementa la _interface_ `Closer`), lo diferimos al final de la ejecución de la función con `defer`. Esto no es necesario pero es buena práctica.
- El paquete `bytes` nos permite crear un _buffer_ de bytes y luego podemos utilizar el método `ReadFrom` para leer de algo que implemente la _interface_ `Reader`, por lo que le pasamos `Body`.
- El resultado de `buf.ReadFrom()` no devuelve la información leída, devuelve el número de bytes leídos. La información esta contenida en nuestro `buf Buffer` y la podemos convertir a un `string` invocando el método `String()`

Una posible modificación que yo haría es extraer la funcionalidad de descargar el contenido del objeto a una función para que el recorrido principal de nuestro programa se lea más claro:

```go
package main

import (
	"bytes"
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)

func main() {
	s3Client := s3.New(session.New(), &aws.Config{
		Region: aws.String("eu-west-1"),
	})

	data, err := downloadS3Data(s3Client, "mi-bucket", "ejemplo.json")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data))
}

func downloadS3Data(s3Client *s3.S3, bucket string, key string) ([]byte, error) {
	result, err := s3Client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, err
	}
	defer result.Body.Close()

	fmt.Println(result)

	buf := new(bytes.Buffer)
	buf.ReadFrom(result.Body)

	return buf.Bytes(), nil
}
```

- Fíjate como le paso el cliente de S3 a la función, esto nos permitirá _testear_ la función mucho más fácil en el futuro ya que le podemos pasar un cliente falso y comprobar que se invoca con los argumentos correctos o falsificar los resultados para que nuestras _tests_ no dependan del acceso a AWS.
- También modifiqué la funcionalidad para que la función devuelva un _slice_ de bytes en lugar de un _string_, esto hace que la función sea más flexible en cuanto a los formatos de información que puede descargar.

## Eso es todo...

Espero que este ejemplo haya sido útil, a mí me ayudo mucho a entender mejor cómo funciona Go, sobre todo el sistema de tipos y las _interfaces_. Si tienes alguna pregunta no dudes en dejar un comentario 👍

## Enlaces
- [Gist con el código](https://gist.github.com/maurogestoso/b437d7f15e8ef45f69d165f0916060a8)

- Documentación del SDK de AWS:

  - [`s3.New()`](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#New)
  - [`GetObject()`](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#S3.GetObject)
  - [`GetObjectInput`](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#GetObjectInput) / [`GetObjectOutput`](https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#GetObjectOutput)

- Documentación de Go:

  - [`bytes`](https://golang.org/pkg/bytes/)
  - [`Buffer.ReadFrom()`](https://golang.org/pkg/bytes/#Buffer.ReadFrom)
  - [`io`]()
  - [`type Reader`](https://golang.org/pkg/io/#Reader)
  - [`type ReadCloser`](https://golang.org/pkg/io/#ReadCloser)

- Stack Overvflow: [Cómo convertir un `io.Reader` a un `string`?](https://stackoverflow.com/questions/9644139/from-io-reader-to-string-in-go)