멀티플렉서
ServeMux는 멀티플렉서로 HTTP 요청 라우팅을 처리한다. 여러 URL을 적절한 동작으로 연결해 주는 역할이다.
func ListenAndServe(addr string, handler Handler) error
ListenAndServe는 서버를 실행하고 요청을 기다린다. 첫번째 인자로 포트를, 두 번째 인자로 Handler를 받는다.
func main() {
// http://127.0.0.1:8080/
http.ListenAndServe(":8080", nil)
}
Handler를 nil로 작성하면 DefaultServeMux를 사용한다. 이 경우 Handler 함수를 등록할 때 바로 http에 등록한다. (자세한 건 아래에서)
func main() {
mux := http.NewServeMux()
http.ListenAndServe(":8080", mux)
}
직접 Handler를 생성해 등록할 수도 있다. 이때 NewServeMux를 사용한다. 일반적으로 권장하는 방법이다.
NewServeMux는 ServeMux를 반환하는데 왜 ListenAndServe 2번째 인자로 쓰이는지 혼란이 올 수 있다. 이는 ServeMux가 Handler 인터페이스의 구현체이기 때문이다. 다시 말해 ListenAndServe는 Handler 인터페이스를 받는데, 멀티플렉서인 ServeMux도 Handler 인터페이스를 만족한다. Handler 인터페이스에 대해서는 아래에서 자세히 다룬다.
Handler
Handler는 URL로 요청이 들어왔을 때 수행할 동작이다.
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Welcome")
})
http.ListenAndServe(":8080", mux)
}
HandleFunc는 Handler 메서드를 등록한다. 위 코드는 "/"로 접근했을 때 "Welcome"을 출력해 준다. 구체적으로 Handler 인터페이스는 ServeHTTP 메서드를 가진다.
type Handler interface{
ServeHTTP(ResponseWriter, *Request)
}
위에서 살펴봤던 HandleFunc는 두번째 인자로 ServeHTTP를 받은 것이다.
Handler 객체를 직접 구현하고 등록하는 방법도 있다.
type userHandler struct{}
func (h *userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint("Hello")
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", &userHandler{})
http.ListenAndServe(":8080", mux)
}
Handle을 사용하면 구현한 객체를 바로 전달할 수 있다.
참고로 DefaultServeMux를 사용하면 바로 http에 등록한다.
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Welcome")
})
http.ListenAndServe(":8080", nil)
}
정리
- ServeMux는 요청에 따라 적절한 Handler를 연결해 준다.
- Handler는 요청을 받아 작업을 수행한다.
- 요청에 대한 함수를 등록하고 싶다면 HandleFunc, 객체를 등록하고 싶다면 Handle을 사용하면 된다.
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux() // -> http.ServeMux (Handler)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello") // -> Handler.ServeHTTP
})
mux.HandleFunc("/new", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "new page")
})
http.ListenAndServe(":3000", mux)
}
이제 go를 실행하고 "http://127.0.0.1 + 포트"로 접속하면 화면을 볼 수 있다.
$ go run main.go
URL로 정보 받아오기
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name") // URL에서 name 가져오기
if name == "" {
name = "Unknown"
}
fmt.Fprintf(w, "Hello %s!", name)
})
http.ListenAndServe(":8080", nil)
}
정보는 URL?key= 형태로 요청된다. 만약 "127.0.0.1:8080/hello?name=Jin"라는 요청이 들어왔다면 "Hello Jin!"이 출력된다. name이 없으면 "Hello Unknown!"이 출력된다.
index.html 보여주기
앞선 예제는 Print를 통해 출력했다면, 이제는 index.html을 서빙하는 코드다. 파일 구조는 아래와 같다.
ROOT
|-- main.go
|-- static
|-- index.html
func main() {
http.Handle("/", http.FileServer(http.Dir("static")))
http.ListenAndServe(":8080", nil)
}
FileServer를 통해 "static" 폴더를 공유한다. 폴더에 index.html이 있다면 자동으로 보여준다.
파일 업로드
반대로 FileServer를 이용해 파일을 업로드해보겠다.
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 파일 업로드
uploadFile, header, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, err)
return
}
// 빈 파일 생성
filePath := fmt.Sprintf("%s/%s", "./uploads", header.Filename)
file, err := os.Create(filePath)
defer file.Close()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err)
return
}
// 업로드된 파일 저장
io.Copy(file, uploadFile)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "Uploaded successfully!")
}
func main() {
http.Handle("/", http.FileServer(http.Dir("static")))
http.HandleFunc("/uploads", uploadHandler)
http.ListenAndServe(":8080", nil)
}
POST 요청이 발생했을 때 f.FormFile과 key(name 속성)를 이용해 파일을 불러온다. 불러온 파일은 /uploads 폴더에 저장된다. Go에서 바로 파일을 저장할 수 없어 빈 파일을 생성하고 덮어쓰기 한 모습이다.
w.WriteHeader는 헤더 정보를 작성하는 함수로 상태 코드를 전달할 수 있다.
참고로 POST 요청을 보내는 폼은 아래와 같은 형태다.
<!-- index.html -->
<form action="/uploads" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="제출" />
</form>
참고: Tucker Programming