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