Aplikasi Go sering berperan sebagai klien: memanggil API eksternal, mengunduh berkas, atau saling berkomunikasi antar layanan. Package net/http menyediakan http.Client yang lengkap — mulai dari pemakaian satu baris sampai pengaturan timeout dan header custom. Pada chapter ini kita akan mengirim request dengan http.Get dan http.NewRequest, membaca body dengan aman, lalu melindungi program dari server lambat dengan Client.Timeout. Seluruh contoh diarahkan ke server uji lokal dari net/http/httptest, sehingga hasil eksekusi tetap deterministik tanpa koneksi internet.

Request Paling Singkat: http.Get #

Untuk permintaan GET yang cepat, fungsi http.Get cukup dipanggil dengan satu argumen URL:

go
resp, err := http.Get("https://example.com/data")

Fungsi tersebut mengembalikan *http.Response dan error, serta memakai http.DefaultClient — variabel package-level yang dibagikan ke seluruh program. URL yang dikirim harus absolut, lengkap dengan skema https atau http; URL relatif seperti /data akan ditolak. Bentuk ini cocok untuk skrip kecil, sedangkan aplikasi yang menangani banyak request membutuhkan http.Client sendiri.

http.Client yang Dipakai Ulang #

Praktik terbaiknya adalah membuat satu http.Client lalu memakai client tersebut untuk seluruh request. Sebuah http.Client menyimpan http.Transport yang mengelola pool koneksi dan fitur keep-alive, sehingga request berikutnya ke server yang sama bisa memakai ulang koneksi lama. Membuat client baru pada setiap request membuang koneksi yang sudah terbentuk dan menambah beban jabat tangan TCP.

go
var client = &http.Client{Timeout: 5 * time.Second}

http.NewRequest dan client.Do #

Ketika Anda membutuhkan method selain GET, header custom, atau body request, bangun request secara eksplisit dengan http.NewRequest lalu kirim melalui client.Do:

go
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(bodyJSON))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)

Fungsi http.NewRequest menerima method, URL absolut, dan sumber body bertipe io.Reader. Header request diatur lewat req.Header.Set sebelum request dikirim. Method client.Do mengirim request tersebut dan mengembalikan *http.Response beserta error jaringan, sehingga pemeriksaan err tetap menjadi langkah pertama setelah pemanggilan.

Membaca Body dan Menutupnya #

Field resp.Body bertipe io.ReadCloser — aliran yang harus dibaca lalu ditutup. Pola standarnya:

go
body, err := io.ReadAll(resp.Body)
resp.Body.Close()

Fungsi io.ReadAll memuat seluruh body menjadi []byte. Pemanggilan Close wajib dilakukan agar koneksi bisa dikembalikan ke pool untuk dipakai ulang; tanpa Close, koneksi menumpuk dan lama-kelamaan file descriptor habis. Selain memeriksa err, periksa juga resp.StatusCode: status 4xx dan 5xx tetap menghasilkan response valid, bukan error, sehingga body tetap perlu dibaca dan ditutup.

Client.Timeout Melindungi dari Server Lambat #

Field Timeout pada http.Client membatasi seluruh siklus satu request, mulai dari koneksi, pengiriman, hingga menunggu header response. Ketika batas waktu terlampaui, client mengembalikan error bertipe *url.Error yang membungkus context.DeadlineExceeded. Pemeriksaan dengan errors.Is(err, context.DeadlineExceeded) menghasilkan true, dan pesan aslinya berbunyi context deadline exceeded (Client.Timeout exceeded while awaiting headers).

Program Utuh: Client Teruji Tanpa Internet #

Berkas client-demo.go pada bagian bawah halaman menggabungkan seluruh pola di atas. Program tersebut membangun server uji lokal dengan tiga rute — /halo yang cepat, /simpan yang membalas 201, dan /lambat yang sengaja tidur 200 milidetik — lalu menyerangnya dengan tiga variasi client.

go
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"time"
)

func main() {
	// Server uji lokal dari net/http/httptest berperan sebagai server jauh.
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/halo":
			fmt.Fprintln(w, "Halo dari server lokal")
		case "/simpan":
			w.Header().Set("X-Request-Id", "req-001")
			w.WriteHeader(http.StatusCreated)
			fmt.Fprintln(w, `{"id":7,"pesan":"item tersimpan"}`)
		case "/lambat":
			time.Sleep(200 * time.Millisecond)
			fmt.Fprintln(w, "akhirnya sampai")
		}
	}))
	defer ts.Close()

	// Bagian 1: cara singkat memakai http.Get (memakai DefaultClient).
	resp, err := http.Get(ts.URL + "/halo")
	if err != nil {
		fmt.Println("gagal:", err)
		return
	}
	body, _ := io.ReadAll(resp.Body)
	resp.Body.Close()
	fmt.Printf("GET /halo -> %s\n", resp.Status)
	fmt.Printf("body      : %s", body)

	// Bagian 2: satu http.Client dipakai ulang + header custom.
	client := &http.Client{Timeout: 2 * time.Second}
	req, err := http.NewRequest(http.MethodPost, ts.URL+"/simpan",
		strings.NewReader(`{"nama":"Buku Go"}`))
	if err != nil {
		fmt.Println("gagal membuat request:", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer token-demo")

	resp2, err := client.Do(req)
	if err != nil {
		fmt.Println("gagal:", err)
		return
	}
	body2, _ := io.ReadAll(resp2.Body)
	resp2.Body.Close()
	fmt.Printf("POST /simpan -> %s\n", resp2.Status)
	fmt.Printf("body         : %s", body2)
	fmt.Printf("X-Request-Id : %s\n", resp2.Header.Get("X-Request-Id"))

	// Bagian 3: server tidur 200 ms, client hanya menunggu 50 ms.
	clientCepat := &http.Client{Timeout: 50 * time.Millisecond}
	_, err = clientCepat.Get(ts.URL + "/lambat")
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		fmt.Println("GET /lambat (Timeout 50 ms) -> gagal")
		fmt.Println("sebab        :", urlErr.Err)
		fmt.Println("DeadlineExceeded:", errors.Is(err, context.DeadlineExceeded))
	}
}

Jalankan berkas tersebut dengan perintah berikut dari direktori chapter:

bash
go run client-demo.go

Output yang Dihasilkan:

text
GET /halo -> 200 OK
body      : Halo dari server lokal
POST /simpan -> 201 Created
body         : {"id":7,"pesan":"item tersimpan"}
X-Request-Id : req-001
GET /lambat (Timeout 50 ms) -> gagal
sebab        : context deadline exceeded (Client.Timeout exceeded while awaiting headers)
DeadlineExceeded: true

Mari kita telaah baris demi baris program di atas:

  1. Baris 17-29 (ts := httptest.NewServer(...)): membangun server uji nyata di alamat loopback; httptest memilih port acak, tetapi program hanya mencetak status dan body — bukan URL server — sehingga output identik di setiap eksekusi.
  2. Baris 33 (resp, err := http.Get(ts.URL + "/halo")): memakai cara singkat http.Get terhadap rute /halo.
  3. Baris 38-39 (body, _ := io.ReadAll(...)): membaca seluruh body menjadi []byte lalu menutup resp.Body segera setelah selesai dibaca.
  4. Baris 44 (client := &http.Client{Timeout: 2 * time.Second}): membuat satu client dengan timeout yang dipakai ulang untuk request berikutnya.
  5. Baris 45-52 (req, err := http.NewRequest(...) dst.): membangun request POST berisi body JSON, lalu mengatur header Content-Type dan Authorization sebelum dikirim.
  6. Baris 54-63 (resp2, err := client.Do(req) dst.): mengirim request melalui client.Do, membaca body, lalu mencetak status, body, dan header X-Request-Id dari response.
  7. Baris 66-72 (clientCepat := ...): menguji timeout dengan client ber-Timeout 50 milidetik terhadap server yang tidur 200 milidetik; errors.As mengambil *url.Error agar URL tidak ikut tercetak, dan errors.Is membuktikan sebabnya context.DeadlineExceeded.

Setelah sisi klien berjalan, mari berpindah kursi ke sisi server. Chapter berikutnya membangun handler dan server HTTP pertama Anda dengan net/http.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

client-demo.go

Unduh berkas
go
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"time"
)

func main() {
	// Server uji lokal dari net/http/httptest berperan sebagai server jauh.
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/halo":
			fmt.Fprintln(w, "Halo dari server lokal")
		case "/simpan":
			w.Header().Set("X-Request-Id", "req-001")
			w.WriteHeader(http.StatusCreated)
			fmt.Fprintln(w, `{"id":7,"pesan":"item tersimpan"}`)
		case "/lambat":
			time.Sleep(200 * time.Millisecond)
			fmt.Fprintln(w, "akhirnya sampai")
		}
	}))
	defer ts.Close()

	// Bagian 1: cara singkat memakai http.Get (memakai DefaultClient).
	resp, err := http.Get(ts.URL + "/halo")
	if err != nil {
		fmt.Println("gagal:", err)
		return
	}
	body, _ := io.ReadAll(resp.Body)
	resp.Body.Close()
	fmt.Printf("GET /halo -> %s\n", resp.Status)
	fmt.Printf("body      : %s", body)

	// Bagian 2: satu http.Client dipakai ulang + header custom.
	client := &http.Client{Timeout: 2 * time.Second}
	req, err := http.NewRequest(http.MethodPost, ts.URL+"/simpan",
		strings.NewReader(`{"nama":"Buku Go"}`))
	if err != nil {
		fmt.Println("gagal membuat request:", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer token-demo")

	resp2, err := client.Do(req)
	if err != nil {
		fmt.Println("gagal:", err)
		return
	}
	body2, _ := io.ReadAll(resp2.Body)
	resp2.Body.Close()
	fmt.Printf("POST /simpan -> %s\n", resp2.Status)
	fmt.Printf("body         : %s", body2)
	fmt.Printf("X-Request-Id : %s\n", resp2.Header.Get("X-Request-Id"))

	// Bagian 3: server tidur 200 ms, client hanya menunggu 50 ms.
	clientCepat := &http.Client{Timeout: 50 * time.Millisecond}
	_, err = clientCepat.Get(ts.URL + "/lambat")
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		fmt.Println("GET /lambat (Timeout 50 ms) -> gagal")
		fmt.Println("sebab        :", urlErr.Err)
		fmt.Println("DeadlineExceeded:", errors.Is(err, context.DeadlineExceeded))
	}
}