Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Go에서의 웹 개발:미들웨어, 템플릿, 데이터베이스 및 그 이상

이 시리즈의 이전 기사에서 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)
}

Go에서의 웹 개발:미들웨어, 템플릿, 데이터베이스 및 그 이상

미들웨어 함수가 http.Handler를 수락하고 반환하기 때문에 유형, 그것은 서로 중첩된 미들웨어 기능의 무한 체인을 만드는 것이 가능합니다.

예를 들어,

mux := http.NewServeMux()
mux.Handle("/", middleware1(middleware2(appHandler)))

Alice와 같은 라이브러리를 사용하여 위의 구성을 다음과 같이 더 읽기 쉬운 형식으로 변환할 수 있습니다.

alice.New(middleware1, middleware2).Then(appHandler)

템플릿

단일 페이지 애플리케이션의 출현으로 템플릿 사용이 줄어들었지만 완전한 웹 개발 솔루션의 중요한 측면으로 남아 있습니다.

Go는 모든 템플릿 요구 사항에 대한 두 가지 패키지를 제공합니다. text/templatehtml/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에서의 웹 개발:미들웨어, 템플릿, 데이터베이스 및 그 이상

이 섹션은 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:     "abraham.freeman@hey.com",
    }

    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": "john.smith@hmail.com"}`
    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_birthgender 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로 개발하는 과정에서 일반적인 작업을 처리하는 방법

읽어주셔서 감사합니다. 행복한 코딩을 하세요!