HTTP Requests

HTTP Requests

Em Go podemos utilizar o pacote net/http para realizar requisições http

Iremos utilizar esse endpoint público para realizarmos um get: https://jsonplaceholder.typicode.com/posts

GET

Pare realizar uma operação de get podemos simplesmente passar a url para a função get do pacote http.

package main

import (
	"io"
	"log"
	"net/http"
)

func main() {
	resp, err := http.Get("https://jsonplaceholder.typicode.com/posts")
	if err != nil {
		log.Fatalln(err)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalln(err)
	}

	stringBody := string(body)
	log.Printf(stringBody)
}

No exemplo acima fazemos a requisição, depois lemos o slice de bytes dentro da resposta e o transformamos em uma string para ser impressa.

Essa será a saída no nosso programa:

.
.
.
 {
    "userId": 1,
    "id": 4,
    "title": "eum et est occaecati",
    "body": "ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit"
  },
  {
    "userId": 1,
    "id": 5,
    "title": "nesciunt quas odio",
    "body": "repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque"
  },
.
.
.

POST

Realizar uma operação de post também é simples, uma das maneiras mais simples é essa:

Para esse exemplo vamos utilizar o endpoint público para realizar uma operação de post: https://postman-echo.com/post

Ele irá retornar dentro do campo data o valor que passarmos como payload.

package main

import (
	"bytes"
	"encoding/json"
	"io"
	"log"
	"net/http"
)

func main() {
	body, _ := json.Marshal(map[string]string{
		"id":       "01",
		"category": "abc",
	})

	bodyBuffer := bytes.NewBuffer(body)

	resp, err := http.Post("https://postman-echo.com/post", "application/json", bodyBuffer)
	if err != nil {
		log.Fatalf(err.Error())
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalln(err)
	}

	respString := string(respBody)
	log.Printf(respString)

}

No exemplo acima o payload de um mapa de string/string.

body, _ := json.Marshal(map[string]string{
    "id":       "01",
    "category": "abc",
})

Em seguida transformamos o nosso payload em um ponteiro de bytes.Buffer, precisamos fazer isso pois a função post do pacote http espera que você passe um valor de payload que implemente a interface io.Reader.

Após isso a sequência é basicamente amesma do exemplo anterior, e essa será a saída do nosso programa:

2025/01/04 13:05:45 {
  "args": {},
  "data": {
    "category": "abc",
    "id": "01"
  },
  "files": {},
  "form": {},
  "headers": {
    "host": "postman-echo.com",
    "x-request-start": "t1736006745.555",
    "connection": "close",
    "content-length": "28",
    "x-forwarded-proto": "https",
    "x-forwarded-port": "443",
    "x-amzn-trace-id": "Root=1-67795c59-114730206605fb0f1eb8f476",
    "content-type": "application/json",
    "accept-encoding": "gzip",
    "user-agent": "Go-http-client/2.0"
  },
  "json": {
    "category": "abc",
    "id": "01"
  },
  "url": "https://postman-echo.com/post"
}

HEADERS

Em uma aplicação no mundo real na maioria das vezes vamos precisar enviar caçalhos junto com a requisição.

Aqui está um exemplo:

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body, _ := json.Marshal(map[string]string{
		"id":       "01",
		"category": "abc",
	})

	bodyBuffer := bytes.NewBuffer(body)
	request, err := http.NewRequest(http.MethodPost, "https://postman-echo.com/post", bodyBuffer)
	if err != nil {
		log.Fatalf(err.Error())
	}

	request.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		log.Fatalf(err.Error())
	}
	defer response.Body.Close()

	responseData, err := io.ReadAll(response.Body)
	if err != nil {
		log.Fatalln(err)
	}

	fmt.Println(string(responseData))
}

Agora podemos ver que primeiro criamos uma variável chamada request que é um ponteiro para a struct http/Request.

request, err := http.NewRequest(http.MethodPost, "https://postman-echo.com/post", bodyBuffer)

Criamos essa variável através da funções NewRequest, onde passamos como argumentos o método, a url e o nosso payload.

Dessa forma, agora podemos adicionar o cabeçalho à nossa variável request.

request.Header.Set("Content-Type", "application/json")

O resto da execução segue o mesmo padrão dos exemplos anteriores:

E essa será a saída do nosso programa:

{
  "args": {},
  "data": {
    "category": "abc",
    "id": "01"
  },
  "files": {},
  "form": {},
  "headers": {
    "host": "postman-echo.com",
    "x-request-start": "t1736007539.761",
    "connection": "close",
    "content-length": "28",
    "x-forwarded-proto": "https",
    "x-forwarded-port": "443",
    "x-amzn-trace-id": "Root=1-67795f73-78f91a361ceb881313a5d351",
    "content-type": "application/json",
    "accept-encoding": "gzip",
    "user-agent": "Go-http-client/2.0"
  },
  "json": {
    "category": "abc",
    "id": "01"
  },
  "url": "https://postman-echo.com/post"
}

Utilizando structs

Esse é um exemplo um pouco mais completo e mais parecido com as necessidades do mundo real.

Nele vamos utilizar structs para o nosso payload e para receber também a resposta da requisição.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

type MyRequest struct {
	Id       string `json:"id"`
	Category string `json:"category"`
}

type ResponseData struct {
	Id       string `json:"id"`
	Category string `json:"category"`
}

//type MyResponse struct {
//	Headers map[string]string `json:"headers"`
//	Data    struct {
//		Id       string `json:"id"`
//		Category string `json:"category"`
//	} `json:"data"`
//}

type MyResponse struct {
	Headers map[string]string `json:"headers"`
	Data    ResponseData      `json:"data"`
}

func main() {

	defaultHeaders := map[string]string{
		"Accept":       "*/*",
		"Content-Type": "application/json",
	}

	myRequest := MyRequest{
		Id:       "01",
		Category: "abc",
	}

	body, _ := json.Marshal(myRequest)

	bodyBuffer := bytes.NewBuffer(body)
	request, err := http.NewRequest(http.MethodPost, "https://postman-echo.com/post", bodyBuffer)
	if err != nil {
		log.Fatalf(err.Error())
	}

	for k, v := range defaultHeaders {
		request.Header.Add(k, v)
	}

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		log.Fatalf(err.Error())
	}
	defer response.Body.Close()

	var myResponse MyResponse
	if err := json.NewDecoder(response.Body).Decode(&myResponse); err != nil {
		log.Fatal(err)
	}

	for v, k := range myResponse.Headers {
		fmt.Printf("%s: %s\n", k, v)
	}

	fmt.Printf("Id: %s - Category: %s\n", myResponse.Data.Id, myResponse.Data.Category)
}

A struct MyRequest, será o nosso payload agora.

type MyRequest struct {
	Id       string `json:"id"`
	Category string `json:"category"`
}

Essa parte entre aspas ao lados dos campos é chamada de struct tags a função dela é dizer que durante o processo de serialização/desserialização o campo deve ser mapeado para o valor que definirmos.

Em seguida temos as structs ResponseData e MyResponse:

type ResponseData struct {
	Id       string `json:"id"`
	Category string `json:"category"`
}

//type MyResponse struct {
//	Headers map[string]string `json:"headers"`
//	Data    struct {
//		Id       string `json:"id"`
//		Category string `json:"category"`
//	} `json:"data"`
//}

type MyResponse struct {
	Headers map[string]string `json:"headers"`
	Data    ResponseData      `json:"data"`
}

Elas também possuem struct tags, e elas serão responsáveis por receberem o valor da resposta da nossa chamada.

Note que a struct ResponseData é utilizada dentro da struct MyResponse, essa é uma composição

Mas repare também que deixamos uma struct comentada que faz o mesmo papel da composição, a escolha de qual utilizar vai depender muito do tipo de necessidade.

Aqui definimos quais são os nossos cabeçalhos, utilizamos um mapa de string/string para isso:

defaultHeaders := map[string]string{
    "Accept":       "*/*",
    "Content-Type": "application/json",
}

Em seguida definimos a variável myRequest como uma instância de MyRequest com o valores do nosso payload.

myRequest := MyRequest{
    Id:       "01",
    Category: "abc",
}

Aqui vamos serializar em json o valor da que está em myRequest:

body, _ := json.Marshal(myRequest)

O processo de executar a requisição é o mesmo dos exemplos anteriores.

A diferença agora é como tratamos a resposta.

var myResponse MyResponse
if err := json.NewDecoder(response.Body).Decode(&myResponse); err != nil {
    log.Fatal(err)
}

Desserealizamos a resposta passando os valores para a variável myResponse que é uma instância de MyResponse

Em seguida imprimimos todos os cabeçahos e os campos Id e Category.

Essa é a saída do nosso programa:

gzip: accept-encoding
postman-echo.com: host
t1736008142.131: x-request-start
close: connection
28: content-length
https: x-forwarded-proto
application/json: content-type
443: x-forwarded-port
Root=1-677961ce-2a641835506a36864b6cd976: x-amzn-trace-id
*/*: accept
Go-http-client/2.0: user-agent
Id: 01 - Category: abc

TIMEOUT

Outro aspecto muito importante em requisições HTTP é definirmos o tempo máximo que podemos esperar por aquela requisição.

O exmeplo abaixo mostra como podemos definir o timeout:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"
)

type MyRequest struct {
	Id       string `json:"id"`
	Category string `json:"category"`
}

type ResponseData struct {
	Id       string `json:"id"`
	Category string `json:"category"`
}

//type MyResponse struct {
//	Headers map[string]string `json:"headers"`
//	Data    struct {
//		Id       string `json:"id"`
//		Category string `json:"category"`
//	} `json:"data"`
//}

type MyResponse struct {
	Headers map[string]string `json:"headers"`
	Data    ResponseData      `json:"data"`
}

func main() {

	defaultHeaders := map[string]string{
		"Accept":       "*/*",
		"Content-Type": "application/json",
	}

	myRequest := MyRequest{
		Id:       "01",
		Category: "abc",
	}

	body, _ := json.Marshal(myRequest)

	bodyBuffer := bytes.NewBuffer(body)
	request, err := http.NewRequest(http.MethodPost, "https://postman-echo.com/post", bodyBuffer)
	if err != nil {
		log.Fatalf(err.Error())
	}

	for k, v := range defaultHeaders {
		request.Header.Add(k, v)
	}

	client := &http.Client{
		Timeout: 15 * time.Second,
	}

	response, err := client.Do(request)
	if err != nil {
		log.Fatalf(err.Error())
	}
	defer response.Body.Close()

	var myResponse MyResponse
	if err := json.NewDecoder(response.Body).Decode(&myResponse); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Id: %s - Category: %s\n", myResponse.Data.Id, myResponse.Data.Category)
}

Nos exemplos anteriores nós utilizamos o DefaultClient para realizarmos as requisições.

No exemplo acima criamos um novo cliente para dessa forma podermos definir o timeout

client := &http.Client{
    Timeout: 15 * time.Second,
}

response, err := client.Do(request)

Isso indica que o tempo total da requisição pode demorar no máximo 15 segundos.

Na grande maioria dos casos especificar somente o timeout dessa maneira já é o suficiente para manter sua aplicação saudável.

Mas existem casos onde você precisa definir melhor o tempo de espera máxima em cada etapa do processo de requisição.

Para esses casos poderíamos configurar o timeout dessa forma:

transport := &http.Transport{
    DialContext: (&net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 30 * time.Second,
    }).DialContext,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

client := &http.Client{
    Transport: transport,
    Timeout:   15 * time.Second,
}

response, err := client.Do(request)

O Dailer configura como as conexões são feitas, sendo que:

Timeout: define o tempo máximo até uma conexão ser estabelecida, se ultrapassar esse valor, a operação irá falhar.

KeeAlive: define o tempo máximo que uma conexão pode ficar ociosa entes de enviar um sinal do tipo keep-alive (manter a conexão ativa). Isso ajuda a manter a conexão aberta para futuras requisições sem precisar reconectar.

Já nas configurações de transport temos:

IdleConnTimeout: define o tempo máximo que uma conexão ociosa pode ficar aberta, se ultrapassar esse valor, ela será fechada.

TLSHandshakeTimeout: define o tempo máximo permitido para o processo de handshake quando estabelecemos conexões seguras HTTPS.

ExpectContinueTimeout: define o tempo máximo que o cliente espera para receber uma resposta do servidor após enviar um cabeçalho HTTP do tipo Expect: 100-Continue.

Essa é a saída do nosso programa:

Id: 01 - Category: abc