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:

PendekatanKasus PenggunaanKeterbatasan
http.Get(url)Prototyping cepat, GET sederhanaTidak bisa set header atau timeout
http.Post(url, ct, body)POST sekali pakaiTidak bisa set header custom
http.Client + http.NewRequest()Produksi, perlu header/timeoutLebih 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.

Referensi

  1. 1type Client — Package net/http, Go Standard Library
  2. 2HTTP Clients — Go by Example
  3. 3func ReadAll — Package io, Go Standard Library