이 시리즈의 이전 기사에서 Gonet/http
패키지 및 프로덕션 준비 웹 애플리케이션에 사용할 수 있는 방법. 우리는 주로 http.ServeMux
의 라우팅 측면과 기타 단점 및 기능에 중점 유형.
이 기사는 ServeMux
에 대한 토론을 마무리합니다. 기본 라우터로 미들웨어 기능을 구현하는 방법을 보여주고 Go로 웹 서비스를 개발할 때 유용할 다른 표준 라이브러리 패키지를 소개합니다.
Go의 미들웨어
많은 구두 HTTP 요청에 대해 실행해야 하는 공유 기능을 설정하는 관행을 미들웨어라고 합니다. . 인증, 로깅 및 쿠키 유효성 검사와 같은 일부 작업은 미들웨어 기능으로 구현되는 경우가 많으며, 이는 일반 경로 처리기 이전 또는 이후에 독립적으로 요청에 대해 작동합니다.
Go에서 미들웨어를 구현하려면 http.Handler 인터페이스를 충족하는 유형이 있는지 확인해야 합니다. 일반적으로 이것은 서명ServeHTTP(http.ResponseWriter, *http.Request)
유형에. 이 메서드를 사용하면 모든 유형이 http.Handler
를 충족합니다. 인터페이스.
다음은 간단한 예입니다.
package main
import "net/http"
type helloHandler struct {
name string
}
func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello " + h.name))
}
func main() {
mux := http.NewServeMux()
helloJohn := helloHandler{name: "John"}
mux.Handle("/john", helloJohn)
http.ListenAndServe(":8080", mux)
}
/john
으로 전송된 모든 요청 경로는 helloHandler.ServeHTTP
로 바로 전달됩니다. 방법. 서버를 시작하고 https://localhost:8080/john으로 이동하면 이 동작을 관찰할 수 있습니다.
ServeHTTP
를 추가해야 함 http.Handler
를 구현하고자 할 때마다 사용자 정의 유형에 메소드 상당히 지루할 수 있으므로 net/http
패키지는 http.HandlerFunc
를 제공합니다. HTTP 핸들러로 일반 기능을 사용할 수 있는 유형입니다.
함수에 다음 서명이 있는지 확인하기만 하면 됩니다.func(http.ResponseWriter, *http.Request)
; 그런 다음 http.HandlerFunc
로 변환합니다. 유형.
package main
import "net/http"
func helloJohnHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello John"))
}
func main() {
mux := http.NewServeMux()
mux.Handle("/john", http.HandlerFunc(helloJohnHandler))
http.ListenAndServe(":8080", mux)
}
mux.Handle
을 대체할 수도 있습니다. main
줄 위의 mux.HandleFunc
함수 함수를 직접 전달합니다. 이 패턴은 이전 기사에서 단독으로 사용했습니다.
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/john", helloJohnHandler)
http.ListenAndServe(":8080", mux)
}
이 시점에서 이름은 main
에서 이름을 설정할 수 있었던 이전과 달리 문자열에 하드코딩됩니다. 핸들러를 호출하기 전에 함수. 이 제한을 없애기 위해 아래와 같이 핸들러 로직을 클로저에 넣을 수 있습니다.
package main
import "net/http"
func helloHandler(name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello " + name))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("/john", helloHandler("John"))
http.ListenAndServe(":8080", mux)
}
helloHandler
함수 자체가 http.Handler
를 충족하지 않습니다. 인터페이스이지만 그렇게 하는 익명의 함수를 생성하고 반환합니다. 이 함수는 name
위에 닫힙니다. 매개변수는 호출될 때마다 액세스할 수 있음을 의미합니다. 이 시점에서 helloHandler
함수는 필요한 만큼 다른 이름으로 재사용할 수 있습니다.
그렇다면 이 모든 것이 미들웨어와 어떤 관련이 있습니까? 음, 미들웨어 기능을 만드는 것은 위에서 본 것과 같은 방식으로 수행됩니다. 클로저에 문자열을 전달하는 대신(예제에서와 같이), 체인의 다음 핸들러를 인수로 전달할 수 있습니다.
전체 패턴은 다음과 같습니다.
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Middleware logic goes here...
next.ServeHTTP(w, r)
})
}
middleware
위의 함수는 핸들러를 수락하고 핸들러를 반환합니다. 익명 함수가 http.Handler
를 만족하도록 만드는 방법에 주목하세요. http.HandlerFunc
로 캐스팅하여 인터페이스 유형. 익명 기능이 끝나면 제어권이 next
로 넘어갑니다. ServeHTTP()
를 호출하여 핸들러 방법. 인증된 사용자의 ID와 같은 핸들러 간에 값을 전달해야 하는 경우 http.Request.Context()
를 사용할 수 있습니다. 방법은 Go 1.7에 도입되었습니다.
이 패턴을 간단하게 보여주는 미들웨어 함수를 작성해 보겠습니다. 이 함수는 requestTime
이라는 속성을 추가합니다. helloHandler
에 의해 후속적으로 활용되는 요청 객체에 요청의 타임스탬프를 표시합니다.
package main
import (
"context"
"net/http"
"time"
)
func requestTime(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, "requestTime", time.Now().Format(time.RFC3339))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func helloHandler(name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
responseText := "<h1>Hello " + name + "</h1>"
if requestTime := r.Context().Value("requestTime"); requestTime != nil {
if str, ok := requestTime.(string); ok {
responseText = responseText + "\n<small>Generated at: " + str + "</small>"
}
}
w.Write([]byte(responseText))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("/john", requestTime(helloHandler("John")))
http.ListenAndServe(":8080", mux)
}
미들웨어 함수가 http.Handler
를 수락하고 반환하기 때문에 유형, 그것은 서로 중첩된 미들웨어 기능의 무한 체인을 만드는 것이 가능합니다.
예를 들어,
mux := http.NewServeMux()
mux.Handle("/", middleware1(middleware2(appHandler)))
Alice와 같은 라이브러리를 사용하여 위의 구성을 다음과 같이 더 읽기 쉬운 형식으로 변환할 수 있습니다.
alice.New(middleware1, middleware2).Then(appHandler)
템플릿
단일 페이지 애플리케이션의 출현으로 템플릿 사용이 줄어들었지만 완전한 웹 개발 솔루션의 중요한 측면으로 남아 있습니다.
Go는 모든 템플릿 요구 사항에 대한 두 가지 패키지를 제공합니다. text/template
및 html/template
. 둘 다 동일한 인터페이스를 가지고 있지만 후자는 코드 삽입 악용을 방지하기 위해 배후에서 일부 인코딩을 수행합니다.
Go 템플릿이 가장 표현력이 뛰어나지는 않지만 작업을 잘 수행하고 프로덕션 애플리케이션에 사용할 수 있습니다. 실제로 인기 있는 정적 사이트 생성기인 whatHugo는 템플릿 시스템을 기반으로 합니다.
html/template
패키지는 웹 요청에 대한 응답으로 HTML 출력을 보내는 데 사용할 수 있습니다.
템플릿 만들기
index.html
생성 main.go
와 동일한 디렉토리에 있는 파일 파일에 다음 코드를 추가합니다.
<ul>
{{ range .TodoItems }}
<li>{{ . }}</li>
{{ end }}
</ul>
다음으로 main.go
에 다음 코드를 추가합니다. 파일:
package main
import (
"html/template"
"log"
"os"
)
func main() {
t, err := template.ParseFiles("index.html")
if err != nil {
log.Fatal(err)
}
todos := []string{"Watch TV", "Do homework", "Play games", "Read"}
err = t.Execute(os.Stdout, todos)
if err != nil {
log.Fatal(err)
}
}
위의 프로그램을 go run main.go
로 실행하면 . 다음 출력이 표시되어야 합니다.
<ul>
<li>Watch TV</li>
<li>Do homework</li>
<li>Play games</li>
<li>Read</li>
</ul>
축하합니다! 첫 번째 Go 템플릿을 만들었습니다. 다음은 템플릿 파일에서 사용한 구문에 대한 간략한 설명입니다.
- Go는 이중 중괄호(
{{
및}}
) 데이터 평가 및 제어 구조를 구분합니다(액션이라고 함). ) 템플릿에서. range
action은 슬라이스와 같은 데이터 구조를 반복하는 방법입니다..
현재 컨텍스트를 나타냅니다.range
작업에서 현재 컨텍스트는todos
의 조각입니다. . 블록 내에서{{ . }}
슬라이스의 각 요소를 나타냅니다.
main.go
에서 파일, template.ParseFiles
메서드는 하나 이상의 파일에서 새 템플릿을 만드는 데 사용됩니다. 이 템플릿은 이후에 template.Execute
를 사용하여 실행됩니다. 방법; io.Writer
가 필요합니다. 템플릿에 적용될 데이터입니다.
위의 예에서 템플릿은 표준 출력으로 실행되지만 io.Writer
를 만족하는 한 모든 대상에서 실행할 수 있습니다. 상호 작용. 예를 들어 웹 요청의 일부로 출력을 반환하려면 ResponseWriter
에 템플릿을 실행하기만 하면 됩니다. 아래와 같이 인터페이스.
package main
import (
"html/template"
"log"
"net/http"
)
func main() {
t, err := template.ParseFiles("index.html")
if err != nil {
log.Fatal(err)
}
todos := []string{"Watch TV", "Do homework", "Play games", "Read"}
http.HandleFunc("/todos", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
err = t.Execute(w, todos)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
http.ListenAndServe(":8080", nil)
}
이 섹션은 Go의 템플릿 패키지에 대한 간략한 소개를 위한 것입니다. 더 복잡한 사용 사례에 관심이 있다면 text/template 및html/template 문서를 확인하세요.
Go의 템플릿 방식이 마음에 들지 않는다면 Plush 라이브러리와 같은 대안이 있습니다.
JSON으로 작업
JSON 개체로 작업해야 하는 경우 Go의 표준 라이브러리에 encoding/json
을 통해 JSON을 구문 분석하고 인코딩하는 데 필요한 모든 것이 포함되어 있다는 소식을 듣게 되어 기쁩니다. 패키지.
기본 유형
Go에서 JSON 개체를 인코딩하거나 디코딩할 때 다음 유형이 사용됩니다.
bool
JSON 부울의 경우float64
JSON 숫자의 경우string
JSON 문자열의 경우nil
JSON null의 경우map[string]interface{}
JSON 개체 및[]interface{}
JSON 배열의 경우.
인코딩
데이터 구조를 JSON으로 인코딩하려면 json.Marshal
기능이 사용됩니다. 다음은 예입니다.
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
FirstName string
LastName string
Age int
email string
}
func main() {
p := Person{
FirstName: "Abraham",
LastName: "Freeman",
Age: 100,
email: "[email protected]",
}
json, err := json.Marshal(p)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(json))
}
위의 프로그램에는 Person
이 있습니다. 4개의 서로 다른 필드가 있는 구조체. main
에서 함수, Person
의 인스턴스 모든 필드가 초기화된 상태로 생성됩니다. json.Marshal
메소드는 p
를 변환하는 데 사용됩니다. 구조를 JSON으로. 이 메서드는 JSON 데이터에 액세스하기 전에 처리해야 하는 바이트 조각 또는 오류를 반환합니다.
Go에서 바이트 조각을 문자열로 변환하려면 위에서 설명한 것처럼 유형 변환을 수행해야 합니다. 이 프로그램을 실행하면 다음과 같은 출력이 생성됩니다.
{"FirstName":"Abraham","LastName":"Freeman","Age":100}
보시다시피 원하는 방식으로 사용할 수 있는 유효한 JSON 개체를 얻습니다. email
필드는 결과에서 제외됩니다. Person
에서 내보내지 않았기 때문입니다. 소문자로 시작하여 개체를 지정합니다.
기본적으로 Go는 결과 JSON 객체의 필드 이름과 같은 구조의 속성 이름을 사용합니다. 그러나 이것은 structfield 태그를 사용하여 변경할 수 있습니다.
type Person struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
email string `json:"email"`
}
위의 구조체 필드 태그는 JSON 인코더가 FirstName
first_name
에 대한 구조체의 속성 JSON 객체의 필드 등등. 이전 예의 이러한 변경은 다음과 같은 출력을 생성합니다.
{"first_name":"Abraham","last_name":"Freeman","age":100}
디코딩
json.Unmarshal
함수는 JSON 객체를 Gostruct로 디코딩하는 데 사용됩니다. 다음과 같은 서명이 있습니다:
func Unmarshal(data []byte, v interface{}) error
JSON 데이터의 바이트 조각과 디코딩된 데이터를 저장할 장소를 허용합니다. 디코딩이 성공하면 반환된 오류는 nil
입니다. .
다음 JSON 객체가 있다고 가정하면
json := "{"first_name":"John","last_name":"Smith","age":35, "place_of_birth": "London", gender:"male"}"
Person
인스턴스로 디코딩할 수 있습니다. 아래와 같이 구조체:
func main() {
b := `{"first_name":"John","last_name":"Smith","age":35, "place_of_birth": "London", "gender":"male", "email": "[email protected]"}`
var p Person
err := json.Unmarshal([]byte(b), &p)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", p)
}
그리고 다음과 같은 결과를 얻습니다.
{FirstName:John LastName:Smith Age:35 email:}
Unmarshal
대상 유형에 있는 필드만 디코딩합니다. 이 경우 place_of_birth
및 gender
Person
의 모든 구조체 필드를 매핑하지 않으므로 무시됩니다. . 이 동작을 활용하여 큰 JSON 개체에서 몇 가지 특정 필드만 선택할 수 있습니다. 이전과 마찬가지로 대상 구조체에서 내보내지 않은 필드는 JSON 개체에 해당 필드가 있더라도 영향을 받지 않습니다. 그래서 email
JSON 객체에 존재하더라도 출력에 빈 문자열로 남아 있습니다.
데이터베이스
database/sql
패키지는 SQL(또는 SQL과 유사한) 데이터베이스에 대한 일반 인터페이스를 제공합니다. 여기에 나열된 것과 같은 데이터베이스 드라이버와 함께 사용해야 합니다. 데이터베이스 드라이버를 가져올 때 밑줄 _
접두사를 붙여야 합니다. 초기화합니다.
예를 들어, 다음은 database/sql
과 함께 MySQLdriver 패키지를 사용하는 방법입니다. :
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
내부적으로 드라이버는 database/sql
패키지에 포함되지만 코드에서 직접 사용되지는 않습니다. 이렇게 하면 특정 드라이버에 대한 종속성을 줄여 최소한의 노력으로 다른 드라이버로 쉽게 교체할 수 있습니다.
데이터베이스 연결 열기
데이터베이스에 액세스하려면 sql.DB
를 생성해야 합니다. 아래와 같이 개체:
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
}
sql.Open
메소드는 나중에 사용할 수 있도록 데이터베이스 추상화를 준비합니다. 데이터베이스에 대한 연결을 설정하거나 연결 매개변수의 유효성을 검사하지 않습니다. 데이터베이스를 즉시 사용할 수 있고 액세스할 수 있는지 확인하려면 db.Ping()
을 사용하세요. 방법:
err = db.Ping()
if err != nil {
log.Fatal(err)
}
데이터베이스 연결 닫기
데이터베이스 연결을 닫으려면 db.Close()
를 사용할 수 있습니다. . 일반적으로 defer
하고 싶습니다. 데이터베이스 연결을 연 함수가 끝날 때까지 데이터베이스 닫기, 일반적으로 main
기능:
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
sql.DB
물체는 수명이 길도록 설계되었으므로 자주 열고 닫지 마십시오. 그렇게 하면 잘못된 재사용 및 연결 공유, 사용 가능한 네트워크 리소스 부족 또는 간헐적인 오류와 같은 문제가 발생할 수 있습니다. sql.DB
를 전달하는 것이 가장 좋습니다. 메서드를 사용하거나 전역적으로 사용할 수 있도록 하고 프로그램이 해당 데이터 저장소에 액세스할 때만 닫습니다.
데이터베이스에서 데이터 가져오기
테이블 쿼리는 세 단계로 수행할 수 있습니다. 먼저 db.Query()
를 호출합니다. . 그런 다음 행을 반복합니다. 마지막으로 rows.Scan()
을 사용합니다. 각 행을 변수로 추출합니다. 다음은 예입니다:
var (
id int
name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
쿼리가 단일 행을 반환하는 경우 db.QueryRow
를 사용할 수 있습니다. db.Query
대신 메소드 그리고 이전 코드 스니펫의 긴 상용구 코드를 피하세요.
var (
id int
name string
)
err = db.QueryRow("select id, name from users where id = ?", 1).Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
fmt.Println(id, name)
NoSQL 데이터베이스
Go는 또한 Redis, MongoDB, Cassandra 등과 같은 NoSQL 데이터베이스를 잘 지원하지만 작업을 위한 표준 인터페이스를 제공하지 않습니다. 특정 데이터베이스에 대한 드라이버 패키지에 전적으로 의존해야 합니다. 몇 가지 예가 아래에 나열되어 있습니다.
- https://github.com/go-redis/redis(Redis 드라이버).
- https://github.com/mongodb/mongo-go-driver(MongoDB 드라이버).
- https://github.com/gocql/gocql(카산드라 드라이버).
- https://github.com/Shopify/sarama(Apache Kafka 드라이버)
마무리
이 기사에서는 Go를 사용하여 웹 애플리케이션을 구축하는 데 필요한 몇 가지 필수 측면에 대해 논의했습니다. 이제 많은 Goprogrammer가 표준 라이브러리를 사용하는 이유를 이해할 수 있을 것입니다. 매우 포괄적이며 프로덕션 준비 서비스에 필요한 대부분의 도구를 제공합니다.
여기에서 다룬 내용에 대해 설명이 필요한 경우 Twitter에서 저에게 메시지를 보내주십시오. 이 시리즈의 다음 기사이자 마지막 기사에서는 go
도구 및 이 도구를 사용하여 Go로 개발하는 과정에서 일반적인 작업을 처리하는 방법
읽어주셔서 감사합니다. 행복한 코딩을 하세요!