DEV Community

Mauro Gestoso
Mauro Gestoso

Posted on

Cómo Obtener un Objeto de S3 en Go

Publicado originalmente en mi blog

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, 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.

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 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 😉).

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 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.

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:

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

Top comments (0)