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 unasession
. 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 condefer
. 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étodoReadFrom
para leer de algo que implemente la interfaceReader
, por lo que le pasamosBody
. - 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 nuestrobuf Buffer
y la podemos convertir a unstring
invocando el métodoString()
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
-
Documentación del SDK de AWS:
-
Documentación de Go:
Stack Overvflow: Cómo convertir un
io.Reader
a unstring
?
Top comments (0)