lets's go를 배워보자 1일차 - go project 설정과 web 기본

2

lets-go

목록 보기
1/15

reddit에서 가장 인기있는 go 웹개발 기본 서적은 let's go이다.

https://lets-go.alexedwards.net/

쉽게 쓰여지기도 했고, 자세한 내용과 정갈한 코드 덕에 정말 많은 공부가 되었다. 여유가 된다면 꼭 책을 구매하여 한 번쯤 정독하길 바란다.

Let's go

1. Project Setup

go에서는 project 당 하나의 module path를 가진다. 이는 nodejs의 package.json과도 같이 제 3의 라이브러리나 모듈들을 관리하고, 해당 project의 버전도 표시할 수 있도록 한다.

golang의 module path는 canonical name 또는 프로젝트의 identifier이다. module path로 어떤 것이든 쓸 수 있지만, 중요한 것은 unique(유일)해야 한다는 것이다. 그래서 go에서는 conventional하게 개발자가 소유한 URL를 module path 맨 앞에 붙인다.

간단 명료하고, 다른 사람들이 사용하지 않을만한 이름 중에 가장 많이들 애용하는 것이 바로 github주소이다. 가령 github 유저 이름이 gyu-young-park이고 repository가 snippetbox이라면

github.com/gyu-young-park/snippetbox

로 이름을 짓는다.

이는 다른 사람들이 사용하지 않을 만하고, unique하다고 볼 수 있다.

go에서 module 이름을 지을 때는 go mod init을 사용한다.

go mod init github.com/gyu-young-park/snippetbox

이후에 go.mod 파일이 생성되고, 확인해보면 다음과 같다.

  • go.mod
module github.com/gyu-young-park/snippetbox

go 1.18

해당 project의 module이름이 적혀있고, golang 버전이 적혀있다. go.mod에 third-party package들이 추가되면서 go.mod 파일에 이들의 이름과 버전이 적혀 점점 파일이 커지게 될 것이다.

한 가지 확인해야할 것은 golang에서 사용 중인 environment가 어떤 지를 확인해야 한다. go env를 통해 golang의 environment variable을 확인해보도록 하자.

go env

가장 눈여겨 봐야할 부분은 GO111MODULE="auto"이다. golang은 시간이 지나 점차 지금의 module 시스템이 완성된 것이지 처음부터 지금의 module 시스템이 만들어진 것은 아니다. 지금의 go.mod로 각 module의 패키지를 표현하기 이전까지만 해도 environment variable로 특정 디렉토리의 패키지를 타겟으로 삼고 빌드 중인지를 설정해주어야만 했다.

이후 module system이 등장하면서, 자동으로 현재 개발 중인 module을 타겟팅해주고, 패키지를 매니징하기 시작하였다. 이를 위해서 사용되는 golang environment variable이 바로 GO111MODULE이다. GO111MODULE는 다음의 값을 가진다.

  1. off: 빌드 중 environment variable로 GOPATH에 있는 패키지를 빌드한다.
  2. on: 빌드 중 environment variable로 GOPATH 대신, 모듈에 있는 패키지를 빌드한다.
  3. auto: 현재 디렉터리에 go.mod이 포함된 경우 해당 모듈을 사용하고, 그렇지 않으면 GOPATH의 패키지를 빌드한다.

해당 환경변수를 바꾸고 싶다면 go env -w를 사용하면 된다.

go env -w GO111MODULE=on

On, Auto는 문제가 없지만 Off인 경우는 현재 module의 package가 아닌 GOPATH에 있는 패키지를 빌드하기 때문에 문제가 발생할 수 있다. 따라서, On, Auto로 바꿔주도록 하자.

2. Web Application Basics

golang으로 http web application을 만들기 위해서는 다음이 필요하다.

  1. handler: MVC 패턴에 익숙하다면 Controller와 같은 역할을 한다고 보면 된다. application logic을 처리하고, http 응답 헤더와 body를 작성한다.
  2. router: golang 용어로는 servemux라고 한다. URL pattern을 저장한다음 application 자체 또는 handler에 mapping 시켜준다. 즉, 특정 URL에 오는 요청을 handler로 routing 처리해준다.
  3. web server: go의 좋은 특징 중 하나는 web server를 만들 수 있고, 들어오는 request를 application 자체의 한 부분으로 listen할 수 있다는 것이다. 즉, third-party server인 Nginx또는 Apache가 필요하지 않는다.

main.go 파일에 web application을 만들어보도록 하자.

  • main.go
package main
import (
    "log"
    "net/http"
)
    // Define a home handler function which writes a byte slice containing
    // "Hello from Snippetbox" as the response body.
    func home(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello from Snippetbox"))
    }
    func main() {
    // Use the http.NewServeMux() function to initialize a new servemux, then
    // register the home function as the handler for the "/" URL pattern.
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)
    // Use the http.ListenAndServe() function to start a new web server. We pass in
    // two parameters: the TCP network address to listen on (in this case ":4000")
    // and the servemux we just created. If http.ListenAndServe() returns an error
    // we use the log.Fatal() function to log the error message and exit. Note
    // that any error returned by http.ListenAndServe() is always non-nil.
    log.Println("Starting server on :4000")
    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

home 핸들러는 그저 go의 일반 함수와 같다. 다만 특별한 parameter type 2개를 가지는데, http.ResponseWriter는 HTTP 응답에 필요한 method들을 제공하는 타입이고, *http.Request는 현재 reuqest에 대한 정보를 가진 구조체의 포인터이다. 가령 어떤 HTTP method로 요청되고 URL로 요청되었는 지를 알 수 있다.

코드를 실행해보도록 하자.

go run main.go

2018/08/02 10:08:07 Starting server on :4000

web server가 4000 port를 listen하고 있다는 로그와 함께 서버가 실행된 것이다. 이제 localhost4000 port의 특정 URL로 오는 요청은 web server에서 router로 router에서 URL mapping을 하여 handler로 처리가 된다.

우리는 mux라는 router를 만들었고, mux.HandleFunc("/", home)url-handler mapping을 하였으니 http://localhost:4000/으로 요청을 보내보도록 하자.

curl http://localhost:4000/

Hello from Snippetbox

주의 할 것이 있는데, golang은 기본적으로 url mapping을 catch-all로 한다. 즉, / 아래로 특정한 handler가 따로 있지 않다면 /의 handler로 귀결된다는 것이다.

curl http://localhost:4000/eweqw/eqwe

다음과 같이 우리는 /eweqw/eqwe라는 URL에 matching되는 handler를 만들지 않았지만 Hello from Snippetbox라는 응답을 얻게 된다. 이는 golangcatch-all로 가장 가까운 url-handler mapping 응답을 보내는 것이라는 걸 알 수 있다.

3. Routing Requests

여러 routing request를 만들어 보도록 하자.

URL PatternHandlerAction
'/home'homeDisplay the home page
'/snippet'showSnippetDisplay a specific snippet
'/snippet/create'createSnippetCreate a new snippet

다음을 구현하기 위해 main.go를 수정해보자.

  • main.go
package main

import (
	"log"
	"net/http"
)

func home(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello from Snippetbox"))
}

// Add a showSnippet handler function.
func showSnippet(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Display a specific snippet..."))
}

// Add a createSnippet handler function.
func createSnippet(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Create a new snippet..."))
}
func main() {
	// Register the two new handler functions and corresponding URL patterns with
	// the servemux, in exactly the same way that we did before.
	mux := http.NewServeMux()
	mux.HandleFunc("/", home)
	mux.HandleFunc("/snippet", showSnippet)
	mux.HandleFunc("/snippet/create", createSnippet)
	log.Println("Starting server on :4000")
	err := http.ListenAndServe(":4000", mux)
	log.Fatal(err)
}

이제 실행해보도록 하고 각 URL에 요청을 보내보자.

go run main.go

curl http://localhost:4000/ 
curl http://localhost:4000/snippet
curl http://localhost:4000/snippet/create

응답은 다음과 같다.

Hello from Snippetbox
Display a specific snippet...
Create a new snippet...

4. Fixed Path adn Subtree Patterns

Go의 router인 servemux(코드에서는 mux)는 두 가지 다른 URL 패턴의 타입을 가진다. 하나는 fixed paths이고 하나는 subtree paths이다. fixed paths는 url pattern의 마지막이 /(slash)로 끝나서는 안된다. 반면에 subtree paths는 마지막이 /(slash)로 끝나야 한다.

/snippet/snippet/createfixed paths로 오직 해당 url이 매칭되는 경우에만 handler가 호출된다.

반면에 /subtree paths인데 이는 정확히 fixed한 url로 매칭되는 것이 아니라, /에 와일드카드인 *가 있듯이 매칭되는 모든 패턴에 적용된다. 가령 /**가 있는 것처럼 말이다.

단, 몇가지 재밌는 점이 있는데, /foo/라는 subtree paths를 만들었는데 요청이 foo로 오는 경우 /foo/301 Permanent Redirect된다는 점이다. 이는 sub tree path가 등록되었는데, 요청은 subtree path인 /없이 오는 경우가 그렇다.

또한, URL path에 이상한 값이 들어가는 경우 자동으로 이를 삭제해주거나 걸러준다. 가령 /foo/bar/...//baz처럼 .//이 들어가도 301 Permanent Redirect/foo/baz로 보낸다.

마지막으로 golang의 URL은 longest pattern에 매칭을 해준다. 이는 여러 개의 pattern이 일치해도 가장 긴 pattern이 매칭되는 handler에 routing된다는 것이다.

5. Restricting the Root URL Pattern

그렇다면 만약 /subtree paths로 작동하는 걸 원치 않는다면 어떻게 해야할까?? 즉, catch-all 상태로 작동하지 않으려면 어떻게 해야할까??

이는 handler를 통해서 구현할 수 있다.

func home(w http.ResponseWriter, r *http.Request) {
	// Check if the current request URL path exactly matches "/". If it doesn't, use
	// the http.NotFound() function to send a 404 response to the client.
	// Importantly, we then return from the handler. If we don't return the handler
	// would keep executing and also write the "Hello from SnippetBox" message.
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	w.Write([]byte("Hello from Snippetbox"))
}

요청으로 들어오느 path/가 아니라면 404 page not found에러를 반환하는 코드이다. http.NotFound()를 통해 간단히 404 page not found를 전송할 수 있다.

6. The DefaultServeMux

우리의 코드는 mux라는 servemux를 하나만들어서 사용하고 있는데, 굳이 하나 만들 필요없이 DefaultServeMux를 사용하면 된다.

func main() {
    http.HandleFunc("/", home)
    http.HandleFunc("/snippet", showSnippet)
    http.HandleFunc("/snippet/create", createSnippet)
    
    log.Println("Starting server on :4000")
    err := http.ListenAndServe(":4000", nil)
    log.Fatal(err)
}

DefaultServeMuxnet/http 패키지 내에서 global variable로 자동으로 생성된다. 이는 우리가 따로 만들어서 사용하던 mux와 동일하며 특별히 다를 바가 없다.

var DefaultServeMux = NewServeMux()

단, production level에서는 DefaultServeMux를 사용하는 것을 추천하지 않는다. DefaultServeMux는 global하게 설정되므로 third-party packages에서 DefaultServeMux를 사요하게 되면 routing path가 자연스럽게 등록이 되고, 원치 않는 경로가 web에서 노출되는 것이다.

이러한 보안상의 이유로 DefaultServeMux를 사용하는 것을 멀리하고, locally-scoped servemux를 사용하는 것이 더 좋다.

7. Customizing HTTP Headers

이제 더 고도화를 시켜보도록 하자. POST method로 snippet에 대한 정보를 받아 snippet을 생성하도록 해보자.

MethodPatternHandlerAction
ANY/homeDisplay the home page
ANY/snippetshowSnippetDisplay a specific snippet
POST/snippet/createcreateSnippetCreate a new snippet

snippet을 만드는 것은 추후에 DB를 도입할 경우 DB에 데이터를 넣어야하는 작업이 필요하다. 이는 non-idempotent하다. non-idempotent하다는 것은 멱등성이 지켜지지 않는다는 의미이며, 매 요청마다 같은 결과가 나오지 않는다는 의미이다.

가령, A라는 데이터를 생성하는 POST를 처음에 보면 A가 생성되었다는 응답이 오지만, 또 A를 생성하는 요청을 보내면 이미 생성되었음으로 실패했다는 응답을 반환해야한다. 이를 non-idempotent하다고 하고, 이러한 작업은 POST에 매우 걸맞다.

POST 메서드가 전달되어야 할 /snippet/create url pattern에 HTTP status code를 설정해주도록 하자. 만약 POST로 요청이 오지 않다면 405(method not allowed)를 전달하도록 하는 것이다.

// Add a createSnippet handler function.
func createSnippet(w http.ResponseWriter, r *http.Request) {
	// Use r.Method to check whether the request is using POST or not. Note that
	// http.MethodPost is a constant equal to the string "POST".
	if r.Method != http.MethodPost {
		// If it's not, use the w.WriteHeader() method to send a 405 status
		// code and the w.Write() method to write a "Method Not Allowed"
		// response body. We then return from the function so that the
		// subsequent code is not executed.
		w.WriteHeader(405)
		w.Write([]byte("Method Not Allowed"))
		return
	}
	w.Write([]byte("Create a new snippet..."))
}

w.WriteHeader()는 오직 하나의 응답에서 단 한번만 가능하다. statuc code(상태코드)가 한 번 쓰여진 순간부터는 변경이 불가능하다. 만약 w.WriteHeader()를 두번째 호출하려고 하면 go에서 warning 메시지를 log에 남긴다.

w.WriteHeader()를 명시적으로 적어주지 않으면 w.Write()에서 자동적으로 200 OK 상태코드를 넣어준다. 따라서 상태 코드를 200이외에 다른 값을 넣어주고 싶다면 w.Write이전애 w.WriteHeader()에 설정해야한다.

이제 서버를 다시 시작하고 잘 설정되었는 지 확인해보도록 하자.

curl -i -X POST http://localhost:4000/snippet/create

HTTP/1.1 200 OK
Date: Mon, 05 Dec 2022 05:11:39 GMT
Content-Length: 23
Content-Type: text/plain; charset=utf-8

-X POST로 요청을 전송하였을 때 200 OK응답을 확인할 수 있었다. 그럼 GET이나 DELETE 등 다른 메서드로 요청을 보내보도록 하자.

curl -i -X GET http://localhost:4000/snippet/create

HTTP/1.1 405 Method Not Allowed
Date: Mon, 05 Dec 2022 05:11:43 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

의도한대로 405 Method Not Allowed가 온 것을 확인할 수 있었다.

잘못된 method로 user가 요청을 보내는 경우, user에게 header로 어떤 메서드가 가능한 지를 응답으로 보내 방법이 있다. 즉 Allow: POST를 header에 넣어서 보내는 것이다.

header에 커스텀한 키-값을 넣고싶다면 w.Header().Set() 메서드를 응답 header map에 추가하여 전송하는 방법을 사용하면 된다.

func createSnippet(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        // Use the Header().Set() method to add an 'Allow: POST' header to the
        // response header map. The first parameter is the header name, and
        // the second parameter is the header value.
        w.Header().Set("Allow", http.MethodPost)
        w.WriteHeader(405)
        w.Write([]byte("Method Not Allowed"))
        return
    }
    w.Write([]byte("Create a new snippet..."))
}

w.WriteHeader() 또는 w.Write() 이후에 w.Header().Set()이 호출되면 적용이 되지 않는다. 따라서, w.WriteHeader() ,w.Write() 이전에 w.Header().Set()으로 header 값들을 모두 설정해야 한다.

이제 서버를 다시켜고 잘못된 메서드로 요청을 보내보도록 하자

curl -i -X GET http://localhost:4000/snippet/create

HTTP/1.1 405 Method Not Allowed
Allow: POST
Date: Mon, 05 Dec 2022 05:21:44 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8

Allow: POST 헤더가 잘 도착한 것을 확인할 수 있다.

참고로, Go에서는 응답을 보낼 때 개발자를 위해 자동으로 3개의 header들을 만들어준다. Date, Content-Length, Content-Type이다.

Content-Type를 알기위해 go는 http.DetectContentType() 함수를 호출하여 응답 body의 타입이 무엇인지를 확인한다. 만약, 해당 함수에서 어떤 타입인지 유추하지 못하면 go는 header에 Content-Type: application/octet-stream를 자동으로 넣어준다.

http.DetectContentType()은 꽤 잘 작동하지만, 문자열 속에 있는 json은 알아내기 어렵다. 따라서 기본적으로 JSON 응답은 따로 설정해주어야 한다. 안그러면 Content-Type: text/plain; charset=utf-8로 응답이 전송되는 경우를 만날 것이다.

w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"name":"Alex"}`))

다음과 같이 application/json을 설정해주도록 하자.

8. The http.Error Shortcut

만약 200 status code가 아니고 plain-text response body를 전송한다면, 굳이 w.WriteHeader, w.Write를 쓸 것이 아니라, http.Error()와 같은 shortcut이 있다. 이는 간단한 helper function으로 message와 status code를 받아서 내부적으로 w.WriteHeader(), w.Write() 메서드를 호출해준다.

func createSnippet(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.Header().Set("Allow", http.MethodPost)
		// Use the http.Error() function to send a 405 status code and "Method Not
		// Allowed" string as the response body.
		http.Error(w, "Method Not Allowed", 405)
		return
	}
	w.Write([]byte("Create a new snippet..."))
}

http.ResponseWriter를 다른 메서드에 전달하여 대신 응답을 전송하게 만드는 패턴은 매우 흔한 패턴이다. 이를 응용하여 추후에 다른 메서드들을 사용해보도록 하자.

9. Header Canonicalization

Header mapAdd(), Get(), Set(), Del() 메서드를 사용하면 header name은 textproto.CanonicalMIMEHeaderKey()함수에 의해 canonicalized된다. 이는 첫번째 letter와 hyphen으로 이어진 단어는 대문자가 되고, 나머지는 소문자가 된다.

만약, 이러한 설정을 하고 싶지않다면 직접 Header map에 인스턴스를 바꿔넣어주면 된다.

w.Header()["X-XSS-Protection"] = []string{"1; mode=block"}

다음과 같이 강제로 w.Header의 값을 설정할 수 있으며, 반대로 값을 죄다 없애기 위해서 nil을 넣을수도 있다. 참고로 Del()을 사용해도 시스템이 자동으로 넣는 header 값들을 사라지지 않는다.

만약, http/2 connection이 사용되면 go는 자동으로 header name들과 값들을 각 http/2 specification에 맞춰 lowercase로 바꿔준다.

10. URL Query Strings

이번에는 query string을 사용하여 /snippet에서 값을 가져올 때 특정 값을 가져오도록 하자.

MethodPatternHandlerAction
ANY/homeDisplay the home page
ANY/snippet?id=1Display a specific snippet
POST/snippet/createcreateSnippetCreate a new snippet

/snippet?id=1가 추가되었다.

추후에 DB에서 저장된 id값에 해당하는 snippet 데이터를 가져와 보여주도록 하고, 현재는 id parameter를 받아서 이 id값을 응답에 보내주도록 하자.

위 기능이 제대로 동작하기 위해서는 showSnippet 핸들러 함수에 다음 두 가지 기능이 동작해야 한다.

  1. URL query string으로부터 id 파라미터를 받기위해 r.URL.Query().Get() 메서드를 사용하도록 하자. 해당 함수는 parameter에 해당하는 값을 넘겨주는데, 만약 해당 파라미터의 값이 없다면 ""가 나온다.

  2. id 파라미터는 user가 입력하는 값으로 정확하지 않는 값이 나올 수 있다. 때문에 validation이 필요한데, 가령 postive integer value인지 검사가 필요하다. 이는 strconv.Atoi()함수를 통해 정수인지를 확인할 수 있고, 해당 정수값이 0보다 큰지를 검사하여 확인할 수 있다.

func showSnippet(w http.ResponseWriter, r *http.Request) {
	// Extract the value of the id parameter from the query string and try to
	// convert it to an integer using the strconv.Atoi() function. If it can't
	// be converted to an integer, or the value is less than 1, we return a 404 page
	// not found response.
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err != nil || id < 1 {
		http.NotFound(w, r)
		return
	}
	// Use the fmt.Fprintf() function to interpolate the id value with our response
	// and write it to the http.ResponseWriter.
	fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

이제 서버를 실행하여 요청을 보내보도록 하자.

curl http://localhost:4000/snippet?id=123

응답으로 다음이 오면 성공이다.

Display a specific snippet with ID 123...

이제 이상한 값들을 보내보도록 하자.

curl http://localhost:4000/snippet?id=ew
curl http://localhost:4000/snippet?id=0
curl http://localhost:4000/snippet?id=-1

다음과 같이 문자열이나 양의 정수가 아닌 값을 보내면

404 page not found

다음과 같은 응답이 나오게 된다.

한 가지 재밌는 것은 fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)으로 응답을 보내는데, 이는 io.Writer interface를 http.ResponseWriter가 구현하기 때문에 가능하다. 즉, Fprintf내부적으로 http.ResponseWriterWrite 메서드를 호출하게 되고 자동으로 header값을 설정하고 body를 전달하게 되는 것이다.

11. Project Structure and Organization

gopher 분들은 프로그램 구조를 만들 때 굉장히 자유롭고, 형식화된 것을 좋아하지 않는다. 그리고 어떠한 형식을 강제하는 것 자체를 굉장히 싫어한다. 이는 golang이라는 언어의 특성에서도, 또 다양한 패키지에서도 볼 수 있는 특징이다.

우리의 프로그램 구조화도 정답이 아니다. 다만, 많이 쓰이고, tried-and-tested 접근법을 따르는 구조를 사용할 것이다.

다음의 명령어로 프로그램을 구조화하자.

rm main.go
mkdir -p cmd/web pkg ui/html ui/static
touch cmd/web/main.go
touch cmd/web/handlers.go

이후 만들어진 구조는 다음과 같다.

.
├── cmd
│   └── web
│       ├── handlers.go
│       └── main.go
├── go.mod
├── pkg
└── ui
    ├── html
    └── stati

각 directory들이 어떤 역할을 하고 어떻게 사용되는 지를 알아보도록 하자.

  1. cmd directory는 application-specific 코드로 프로젝트의 실행 가능한(executable)한 application을 포함할 것이다. 현재는 하나의 executable application(web application)을 갖는다.

  2. pkg는 프로젝트에 사용되는 부수적인(ancillay) non-application-specific한 코드를 포함할 것이다. ui/html은 html templates를 포함하고, ui/static은 static file(css and images)를 포함할 것이다.

왜 이러한 구조를 선택했는가?? 여기에는 두 가지 이점이 있다.

  1. 이러한 구조는 go와 non-go assets에 사이에 대한 분명한 분리(separation)을 제공한다. 우리가 작성할 모든 go code는 cmdpkg 디렉토리들 아래에서 베타적으로 동작할 것이다. 이는 우리의 프로젝트 root가 ui files, makefile 그리고 module 정의들(go.mod)과 같은 non-go assets를 가지고 있는 데에 있어 자유롭게 한다. 이는 우리의 application을 미래에 building하고 deploying하는데에 있어 훨씬 더 관리하기 쉽게 만들어 줄 것이다.

  2. 해당 구조는 또 다른 executalbe application을 우리의 프로젝트에 추가해야할 때 확장성이 좋다. 가령, 만약 관리적인 측면에서의 일을 자동화하기 위한 CLI을 추가하고 싶다면, 우리의 구조에서 CLIcmd/cli아래에 두어 만들면 된다. 또한, 이는 개발자가 만든 pkg를 재사용하고 import하기도 좋다.

12. Refactoring Your Existing Code

이전에 작성한 코드를 새로운 구조에 적어보도록 하자.

  • main.go
package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", home)
	mux.HandleFunc("/snippet", showSnippet)
	mux.HandleFunc("/snippet/create", createSnippet)
	log.Println("Starting server on :4000")
	err := http.ListenAndServe(":4000", mux)
	log.Fatal(err)
}
  • handler.go
package main

import (
	"fmt"
	"net/http"
	"strconv"
)

func home(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	w.Write([]byte("Hello from Snippetbox"))
}
func showSnippet(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err != nil || id < 1 {
		http.NotFound(w, r)
		return
	}
	fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}
func createSnippet(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.Header().Set("Allow", http.MethodPost)
		http.Error(w, "Method Not Allowed", 405)
		return
	}
	w.Write([]byte("Create a new snippet..."))
}

cmd/web 아래에 여러가지 go source code가 있으므로 다음의 명령어로 실행해보도록 하자.

go run ./cmd/web

0개의 댓글