BAB 49: HTTP Client — Mengonsumsi API dari Go
Pelajari cara membuat HTTP request dari program Go menggunakan net/http: GET, POST dengan form data, membaca respons JSON, dan mengelola header.
Di bab sebelumnya program Go berdiri sebagai server — menerima request dari luar dan membalas dengan JSON. Sekarang giliran membalik posisinya. Banyak program Go yang perlu mengonsumsi API dari layanan lain: mengambil data cuaca dari API publik, membaca berita dari feed eksternal, atau berkomunikasi antar service di dalam sistem yang sama. Untuk semua itu, program Go perlu berperan sebagai klien HTTP.
package net/http tidak hanya bisa membuat server — ia juga menyediakan http.Client sebagai HTTP client yang lengkap. Dengan http.Client, program Go bisa mengirim GET, POST, dan method lainnya; mengatur header; serta membaca dan mendecode respons JSON.
GET Request Sederhana
Cara paling cepat membuat GET request adalah http.Get() — satu fungsi, satu baris, langsung menghasilkan respons.
// cek-api.go
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://httpbin.org/get")
if err != nil {
fmt.Println("error:", err)
return
}
defer resp.Body.Close()
isi, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("error membaca body:", err)
return
}
fmt.Println("Status :", resp.Status)
fmt.Println("Body :", string(isi))
}
Dua hal penting yang tidak boleh dilewatkan. Pertama, defer resp.Body.Close() — Body adalah stream yang harus ditutup setelah selesai dibaca, seperti menutup file setelah selesai ditulis di Bab 44. Jika tidak ditutup, koneksi akan tetap terbuka dan membuang resource. Kedua, io.ReadAll() membaca seluruh isi body sekaligus ke dalam []byte.
Selalu tutup resp.Body setelah selesai dibaca. Pola defer resp.Body.Close() harus ditulis segera setelah memastikan err == nil dari http.Get() — bukan setelah blok if. Jika err != nil, resp bisa nil dan memanggil Close() akan menyebabkan panic.
Decode Respons JSON Langsung
Untuk API yang mengembalikan JSON, tidak perlu membaca body ke []byte dulu lalu memanggil json.Unmarshal(). json.NewDecoder() bisa membaca langsung dari stream body — lebih efisien untuk respons yang besar.
Misalnya, platform KontenKu ingin mengambil data dari layanan metadata eksternal yang mengembalikan informasi artikel:
// ambil-metadata.go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type MetadataArtikel struct {
ID int `json:"id"`
Judul string `json:"title"`
Penulis string `json:"userId"`
Selesai bool `json:"completed"`
}
func ambilMetadata(id int) (*MetadataArtikel, error) {
url := fmt.Sprintf("https://jsonplaceholder.typicode.com/todos/%d", id)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("request gagal: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status tidak OK: %s", resp.Status)
}
var metadata MetadataArtikel
err = json.NewDecoder(resp.Body).Decode(&metadata)
if err != nil {
return nil, fmt.Errorf("decode gagal: %w", err)
}
return &metadata, nil
}
func main() {
data, err := ambilMetadata(3)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("ID : %d\n", data.ID)
fmt.Printf("Judul : %s\n", data.Judul)
fmt.Printf("Selesai: %v\n", data.Selesai)
}
Output:
ID : 3
Judul : fugiat veniam minus
Selesai: false
Pola ambilMetadata() yang mengembalikan (*MetadataArtikel, error) adalah cara idiomatic Go untuk fungsi yang bisa gagal. Jika request berhasil, pointer ke struct dikembalikan. Jika gagal di manapun — koneksi, status code, atau decode — error dikembalikan dan pemanggil bisa menangani sesuai kebutuhan.
HTTP Client dengan Kontrol Penuh
http.Get() praktis tapi terbatas — tidak bisa mengatur header, timeout, atau opsi lainnya. Untuk kebutuhan yang lebih spesifik, gunakan http.Client bersama http.NewRequest().
// klien-kontenku.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
type RingkasanArtikel struct {
ID int `json:"id"`
Judul string `json:"title"`
UserID int `json:"userId"`
}
func main() {
klien := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest(http.MethodGet, "https://jsonplaceholder.typicode.com/posts", nil)
if err != nil {
fmt.Println("error membuat request:", err)
return
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "KontenKu-Aggregator/1.0")
resp, err := klien.Do(req)
if err != nil {
fmt.Println("error mengirim request:", err)
return
}
defer resp.Body.Close()
var daftarArtikel []RingkasanArtikel
err = json.NewDecoder(resp.Body).Decode(&daftarArtikel)
if err != nil {
fmt.Println("error decode:", err)
return
}
fmt.Printf("Total artikel diterima: %d\n\n", len(daftarArtikel))
for i, a := range daftarArtikel {
if i >= 3 {
break
}
fmt.Printf("[%d] %s\n", a.ID, a.Judul)
}
}
Output:
Total artikel diterima: 100
[1] sunt aut facere repellat provident occaecati excepturi optio reprehenderit
[2] qui est esse
[3] ea molestias quasi exercitationem repellat qui ipsa sit aut
http.Client dengan Timeout memastikan program tidak menunggu selamanya jika server eksternal tidak merespons — ini penting untuk aplikasi yang berjalan terus-menerus. http.NewRequest() memungkinkan pengaturan header secara manual sebelum request dikirim.
POST dengan Form Data
Tidak semua API menerima JSON. Ada layanan yang masih menggunakan format form-encoded — format yang sama dengan yang dikirim browser ketika user mengklik tombol submit. Untuk kasus ini, url.Values dari Bab 46 kembali digunakan.
// kirim-laporan.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
type ResponLaporan struct {
Form map[string]string `json:"form"`
URL string `json:"url"`
}
func kirimLaporan(judulArtikel string, alasan string) error {
formData := url.Values{}
formData.Set("judul_artikel", judulArtikel)
formData.Set("alasan", alasan)
formData.Set("platform", "KontenKu")
bodyEncoded := bytes.NewBufferString(formData.Encode())
req, err := http.NewRequest(http.MethodPost, "https://httpbin.org/post", bodyEncoded)
if err != nil {
return fmt.Errorf("error membuat request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
klien := &http.Client{}
resp, err := klien.Do(req)
if err != nil {
return fmt.Errorf("error mengirim request: %w", err)
}
defer resp.Body.Close()
var hasil ResponLaporan
err = json.NewDecoder(resp.Body).Decode(&hasil)
if err != nil {
return fmt.Errorf("error decode: %w", err)
}
fmt.Println("Data yang diterima server:")
for k, v := range hasil.Form {
fmt.Printf(" %s = %s\n", k, v)
}
return nil
}
func main() {
err := kirimLaporan("Goroutine dan Concurrency", "konten mengandung informasi yang salah")
if err != nil {
fmt.Println("gagal:", err)
}
}
Output:
Data yang diterima server:
alasan = konten mengandung informasi yang salah
judul_artikel = Goroutine dan Concurrency
platform = KontenKu
}
url.Values.Encode() menghasilkan string dalam format key=value&key2=value2 — persis format yang diharapkan oleh server ketika Content-Type adalah application/x-www-form-urlencoded. bytes.NewBufferString() membungkusnya menjadi io.Reader yang bisa diterima http.NewRequest() sebagai body.
httpbin.org adalah layanan publik yang sangat berguna untuk menguji HTTP request. Endpoint /get, /post, dan lainnya memantulkan kembali request yang diterima sebagai JSON — jadi kita bisa melihat persis apa yang server terima dari klien kita.
Perbandingan Pendekatan
Ada tiga cara membuat HTTP request di Go, masing-masing punya trade-off yang jelas:
| Pendekatan | Kasus Penggunaan | Keterbatasan |
|---|---|---|
http.Get(url) | Prototyping cepat, GET sederhana | Tidak bisa set header atau timeout |
http.Post(url, ct, body) | POST sekali pakai | Tidak bisa set header custom |
http.Client + http.NewRequest() | Produksi, perlu header/timeout | Lebih verbose |
Untuk kode production, selalu gunakan http.Client dengan Timeout yang disetel — http.Get() tidak punya timeout dan bisa membuat program menggantung selamanya jika server tidak merespons.
Latihan
Latihan 1 — Ambil array JSON:
Buat fungsi ambilSemuaArtikel() yang mengambil dari https://jsonplaceholder.typicode.com/posts dan mengembalikan []RingkasanArtikel. Tampilkan 5 artikel pertama dengan format "[ID] Judul (UserID: X)".
Latihan 2 — Filter di sisi klien:
Modifikasi fungsi dari Latihan 1 agar menerima parameter userID int dan hanya mengembalikan artikel milik user tersebut. Hint: filter dilakukan setelah decode, bukan di URL.
Latihan 3 — Gabungkan dengan web service:
Jalankan server dari Bab 48 di port 8080. Buat program klien terpisah yang mengonsumsi http://localhost:8080/api/artikel dan mencetak setiap artikel yang diterima. Ini simulasi nyata komunikasi dua service Go.
Dengan http.Client, program Go bisa berperan sebagai konsumen data dari layanan manapun yang berbicara HTTP. Pola http.NewRequest() + klien.Do() + json.NewDecoder().Decode() adalah siklus lengkap yang akan muncul berulang kali di kode Go produksi. Sejauh ini seluruh komunikasi HTTP berjalan dalam format teks — di bab-bab selanjutnya, ada topik-topik yang membangun di atas fondasi ini untuk kebutuhan yang lebih spesifik.