Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Integrando com um serviço externo

Por hora o nosso servidor apenas retorna um singelo Hello!, vamos estender ele para retornar a resposta de uma das três APIs que vimos quando estávamos construindo o client a partir da URI.

A idéia vai ser:

  • Caso a URI seja /chuck: retornar a piada aleatória
  • Caso a URI seja /pokemon: retornar um pokemon aleatório (note que a API dos pokemons não tem essa funcionalidade)
  • Caso a URI seja /rickandmorty: retornar um personagem do rick and morty aleatório (mesmo caso do de cima)

Antes de continuarmos, vamos isolar a lógica de fazer a requisição em um arquivo separado, esse é o mesmo código do client que estávamos usando encapsulado numa função, você pode chamar do que quiser:

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

// Esse é o mesmo código do GURL que fizemos encapsulado numa função retornando os bytes ao invés de imprimir eles
// copiado e colado de arco1/gurl.go

func enviarRequisicao(url string) []byte {
	resposta, err := http.Get(url) // A boa notícia é que GO já tem toda a parte complicada pronta, então podemos só usar!!
	if err != nil {
		fmt.Println("nao foi possível se comunicar com o servidor: ", err)
		os.Exit(1)
	}

	// Só fazemos ler a reposta e imprimir na tela
	bytesDaResposta := lerResposta(resposta)
	return bytesDaResposta
}

func lerResposta(resp *http.Response) []byte {
	body, err := io.ReadAll(resp.Body) // Aqui lemos a resposta do servidor para um array de bytes
	resp.Body.Close()                  // Precisamos sempre fechar as "streams" em GO (isso seria discussão para um outro workshop)

	if err != nil {
		fmt.Println("Não foi possível ler a resposta: ", err)
		os.Exit(1)
	}

	return body // o que é uma string se não um array de bytes?
}

Integrando

Você talvez já deva imaginar como ficariam os handlers para cada uma das APIs agora. Ficaria algo assim:

// server v1
package main

import (
	"fmt"
	"math/rand"
	"net/http"
	"os"
	"strconv"
)

func main() {
	// Cadastramos os 4 patterns, ao receber uma requisição com /pokemon na URI, somente o handlerPokemon será chamado
	http.HandleFunc("/", handlerDefault)
	http.HandleFunc("/chuck", handlerChuck)
	http.HandleFunc("/pokemon", handlerPokemon)
	http.HandleFunc("/rickandmorty", handlerRickAndMorty)

	err := http.ListenAndServe("localhost:8000", nil)

	// Novamente o nosso stub de tratamento de erro
	if err != nil {
		fmt.Println("erro inciando o servidor: ", err)
		os.Exit(1)
	}
}

// Os 3 handlers fazem a mesma coisa, fazem a requisição para a API externa e retornam os resultados!
func handlerChuck(
	writer http.ResponseWriter,
	request *http.Request,
) {
	// Podemos gerar um número aleatório para buscar a cada request
	url := "https://api.chucknorris.io/jokes/random"

	handleAPI(writer, url)
}

func handlerPokemon(
	writer http.ResponseWriter,
	request *http.Request,
) {
	// Podemos gerar um número aleatório para buscar a cada request
	randomNumber := rand.Intn(100) + 1
	url := "hthttps://pokeapi.co/api/v2/pokemon/" + strconv.Itoa(randomNumber) // converte o num pra string e concatena

	handleAPI(writer, url)
}

func handlerRickAndMorty(
	writer http.ResponseWriter,
	request *http.Request,
) {
	// Podemos gerar um número aleatório para buscar a cada request
	randomNumber := rand.Intn(100) + 1
	url := "https://rickandmortyapi.com/api/character/" + string(randomNumber)

	handleAPI(writer, url)
}

// Chamado por todos
func handleAPI(writer http.ResponseWriter, url string) {
	resposta := enviarRequisicao(url)
	writer.Write(resposta)
}

// Essa é a função que será chamada caso a URI não seja nenhuma das outras
func handlerDefault(
	writer http.ResponseWriter,
	request *http.Request,
) {
	helloEmBytes := []byte("Oii, para acessar alguma API, use /chuck, /pokemon ou /rickandmorty!\n")
	writer.Write(helloEmBytes)
}

Se você preferir separar o arquivo do client, você precisará passar esse arquivo como parâmetro para o go run também! Caso contrário ocorrerá erro

Simples, né? Essa é a nossa V1, mas ela tem um problema grave! Você consegue imaginar o que vai acontecer se ocorrer algum erro ao enviar a requisição para alguma API? Tente simular colocando a string errada e veja o que vai acontecer rsrs

Tratando erros

O problema é que o client está finalizando o processo ao encontrar o menor erro, isso não é muito interessante...,

Para isso, vamos seguir o padrão idiomático do GO, a função enviarRequisicao passa a retornar duas coisas, um []byte e um err indicando um possível problema que possa ter ocorrido.
O handlers agora fariam algo parecido como:

// server v2
handleAPI(writer http.ResponseWriter, url string) {
	resposta, err := enviarRequisicao(url)

	if err != nil {
		writer.Write([]byte(err.Error()))
		return
	}

	writer.Write(resposta)
}

(os arquivos em arco2/server_v2 tem as implementações atualizadas)

Lendo a resposta e montando payload

Podemos retornar por exemplo o tamanho do resultado em um outro campo.

Vamos retornar um JSON com dois campos, { tamanho: number; resultado: any }, mas como podemos fazer isso?

Para o tamanho, podemos apenas chamar o length
Para retornar o dado estruturado em json, fazemos um Marshal (ou serialização) de uma struct anonima combinando essas duas variáveis, o handleAPI ficaria assim:

// server v3

// Chamado por todos
func handleAPI(writer http.ResponseWriter, url string) {
	respostaDaApi, err := enviarRequisicao(url)
	if err != nil {
		writer.Write([]byte(err.Error()))
	}

	// Aqui transformamos os bytes retornados em objeto estruturado para serializarmos depois
	var respostaDaApiEmJson any
	json.Unmarshal(respostaDaApi, &respostaDaApiEmJson) // o unmarshal vai montar uma struct com os campos do json

	// Aqui tem um conceito novo, structs anonimas, parecidas com os types do typescript
	// IMPORTANTE: os campos precisam começar com letra maiúscula, caso contrário
	// seriam tratados como campos privados e não seriam serializados!
	resposta := struct {
		Tamanho  int
		Resposta any // isso significa que a tipagem desse campo é desconhecida
	}{
		Tamanho:  len(respostaDaApi),
		Resposta: respostaDaApiEmJson,
	}

	respostaJson, err := json.Marshal(resposta)
	if err != nil {
		writer.Write([]byte(err.Error()))
		return
	}

	writer.Write(respostaJson)

Com isso nós temos um pequeno servidor com diversas coisas já funcionando! Temos roteamento de requests, integramos com serviços externos e trabalhamos serialização/desserialização.

Se você quiser testar como seu server lida com várias requests ao mesmo tempo, pode usar o script em arco2/requests.sh passando a quantidade de requests como parâmetro