GET 요청
이 페이지의 모든 예제는 httptest.NewServer로 자기 서버를 띄우고 localhost의 실제 TCP 연결로 호출합니다. srv.URL을 https://api.example.com 같은 실제 주소로 바꿔도 클라이언트 코드는 그대로입니다.
여기서 보이는 세 가지 규칙:
err부터 확인하세요. 응답을 아예 받지 못한 실패를 알려 줍니다. DNS 오류, 거부된 연결, 타임아웃, TLS 문제 등입니다.- 오류 확인 바로 다음에
defer resp.Body.Close(). 본문이 연결을 열어 둡니다. 닫지 않은 본문은 연결을 누수시킵니다. - 본문을 읽거나 디코딩하세요. 텍스트는
io.ReadAll, JSON은json.NewDecoder(resp.Body).Decode(&v)입니다.
상태 코드는 오류가 아니다
http.Get은 404와 500을 포함해 서버가 보낸 모든 응답에 대해 err == nil을 반환합니다. 상태는 직접 확인해야 합니다:
io.LimitReader는 오류 본문을 읽는 양에 상한을 두므로, 이상하게 동작하는 서버 때문에 오류 메시지에 몇 메가바이트를 불러오는 일이 없습니다. 많은 API가 성공에 201, 202, 204를 반환합니다. 2xx라면 무엇이든 괜찮을 때는 resp.StatusCode >= 200 && resp.StatusCode < 300을 확인하세요.
타임아웃: 실제 트래픽에 기본 클라이언트를 쓰지 마라
http.Get, http.Post, http.DefaultClient에는 전체 타임아웃이 없습니다(기본 트랜스포트는 연결 시도와 TLS 핸드셰이크만 제한합니다). 연결은 받아 놓고 응답하지 않는 서버는 고루틴을 영원히 블록합니다. 직접 클라이언트를 만드세요:
Client.Timeout은 연결, 리다이렉트, 본문 읽기를 포함한 전체 교환을 다룹니다. 반환되는 오류는 Timeout() == true를 보고하는 *url.Error이며, errors.As가 net.Error 인터페이스를 통해 찾아냅니다. 클라이언트는 하나 만들어 어디서나 재사용하세요. http.Client는 동시에 써도 안전합니다. 연결 풀은 Client가 아니라 그 Transport에 있습니다. 요청마다 새 http.Client{}를 만들어도 http.DefaultTransport와 그 풀을 공유하지만, 요청마다 새 http.Transport를 만들면 매번 빈 풀로 시작합니다.
컨텍스트와 헤더가 있는 요청
http.NewRequestWithContext는 client.Do로 보내기 전에 다듬을 수 있는 요청을 만듭니다. 컨텍스트는 클라이언트의 타임아웃 위에 요청별 마감 시간이나 취소를 더하고, 서버에서는 나가는 호출을 들어온 요청의 수명에 묶어 줍니다.
사용자 입력을 URL에 이어 붙이면("?q=" + q) &, #, 공백에서 깨집니다. url.Values와 그 Encode 메서드가 이스케이프를 처리하고 키를 정렬해 줍니다. 경로 세그먼트에는 url.PathEscape를 쓰세요.
JSON POST
client.Post(url, contentType, body)는 그 Content-Type 헤더를 단 요청의 지름길입니다. 그 밖의 경우(인증 헤더, PUT, PATCH, DELETE)에는 메서드를 지정한 http.NewRequestWithContext와 client.Do를 쓰세요. 폼 전송이라면 client.PostForm(url, url.Values{...})가 값을 인코딩하고 application/x-www-form-urlencoded를 설정합니다.
연결 재사용
클라이언트는 이전 본문을 끝까지 읽고 닫았을 때만 TCP 연결을 재사용합니다. (예를 들어 오류 상태를 받은 뒤) 일찍 읽기를 멈췄다면, 같은 호스트에 요청을 많이 보낼 때는 닫기 전에 본문을 비우세요:
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
본문이 거대할 수 있다면 대신 io.LimitReader로 비우거나, 연결을 잃는 것을 받아들이세요.
리다이렉트
클라이언트는 리다이렉트를 최대 10번까지 자동으로 따라가며, resp.Request.URL이 최종 도착지를 알려 줍니다. 예를 들어 Location 헤더를 읽기 위해 첫 리다이렉트에서 멈추려면 CheckRedirect를 설정하세요:
client := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // return the 3xx response itself
},
}
흔한 실수
- 운영 환경에서
http.Get이나http.DefaultClient를 씀. 타임아웃이 없습니다.Timeout이 있는http.Client를 만드세요. resp.Body.Close()를 잊음. 요청이 실패하기 시작할 때까지 연결이 누수됩니다.err를 확인하기 전에 본문을 닫음.err가nil이 아니면resp는nil이고resp.Body.Close()에서 패닉이 납니다.err == nil을 성공으로 취급함.resp.StatusCode를 확인하세요.- 요청마다 새
http.Transport를 만듦. 트랜스포트마다 자기 연결 풀이 있으므로 요청마다 새 연결(과 새 TLS 핸드셰이크)이 열립니다. 보통 공유하는http.Client하나를 통해 트랜스포트 하나를 공유하세요. - 쿼리 문자열을 직접 만듦.
url.Values를 쓰세요.
자주 묻는 질문
Go에서 GET 요청은 어떻게 보내나요?
resp, err := http.Get(url)로 보내고 err를 확인한 뒤, defer resp.Body.Close()를 쓰고 io.ReadAll(resp.Body)로 본문을 읽거나 json.NewDecoder(resp.Body).Decode(&v)로 디코딩합니다. resp.StatusCode도 확인하세요. 404나 500은 http.Get의 오류가 아닙니다.
Go에서 HTTP 요청에 타임아웃을 두려면 어떻게 하나요?
타임아웃이 있는 클라이언트 client := &http.Client{Timeout: 10 * time.Second}를 만들고, 전체 타임아웃이 없는 http.Get이나 http.DefaultClient 대신 그것을 쓰세요. 연결은 받아 놓고 응답하지 않는 서버는 호출을 영원히 블록합니다. 요청별로 제한하려면 context.WithTimeout으로 만든 컨텍스트와 함께 http.NewRequestWithContext로 요청을 만드세요.
Go에서 JSON 본문으로 POST 요청을 보내려면 어떻게 하나요?
값을 마샬링한 뒤 보냅니다: body, _ := json.Marshal(v)와 http.Post(url, "application/json", bytes.NewReader(body)). Authorization 같은 헤더를 추가하려면 http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))로 요청을 만들고, req.Header.Set("Content-Type", "application/json")을 설정한 뒤 client.Do(req)로 보내세요.
Go에서 resp.Body를 닫아야 하는 이유는 무엇인가요?
본문은 열린 연결 위의 스트림입니다. 닫으면 연결이 재사용을 위해 풀로 돌아가고, 잊으면 프로그램이 한도에 이를 때까지 연결과 파일 디스크립터가 누수됩니다. 읽지 않더라도 닫고, 연결을 재사용하고 싶다면 끝까지 읽으세요.