Computer >> Máy Tính >  >> Lập trình >> Ruby

Phát triển web trong Go:Middleware, Templating, Databases &Beyond

Trong phần trước của loạt bài này, chúng ta đã thảo luận sâu rộng về Go net / http gói và cách nó có thể được sử dụng cho các ứng dụng web sẵn sàng cho sản xuất. loại.

Bài viết này sẽ kết thúc cuộc thảo luận về ServeMux bằng cách trình bày các chức năng howmiddleware có thể được triển khai với bộ định tuyến mặc định và giới thiệu các gói thư viện tiêu chuẩn khác chắc chắn sẽ hữu ích khi phát triển các dịch vụ web với Go.

Phần mềm trung gian trong Go

Thực tiễn thiết lập chức năng được chia sẻ cần chạy cho nhiều hoặc tất cả các yêu cầu HTTP được gọi là phần mềm trung gian . Một số hoạt động, chẳng hạn như xác thực, ghi nhật ký và xác thực cookie, thường được triển khai dưới dạng các chức năng phần mềm trung gian, hoạt động theo yêu cầu một cách độc lập trước hoặc sau trình xử lý định tuyến thông thường.

Để triển khai phần mềm trung gian trong Go, bạn cần đảm bảo rằng bạn có kiểu đáp ứng giao diện http.Handler. Thứ nhất, điều này có nghĩa là bạn cần đính kèm một phương thức có chữ ký ServeHTTP (http.ResponseWriter, * http.Request) vào loại. Khi sử dụng phương pháp này, bất kỳ loại nào sẽ đáp ứng http.Handler giao diện.

Đây là một ví dụ đơn giản:

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)
}

Bất kỳ yêu cầu nào được gửi đến / john tuyến đường sẽ được chuyển thẳng đến helloHandler.ServeHTTP phương pháp. Bạn có thể quan sát điều này trong quá trình thực hiện bằng cách khởi động máy chủ và truy cập https:// localhost:8080 / john.

Phải thêm ServeHTTP phương thức thành một loại tùy chỉnh mỗi khi bạn muốn thực hiện một http.Handler sẽ khá tẻ nhạt, vì vậy net / http gói cung cấp http.HandlerFunc loại, cho phép sử dụng các hàm thông thường như trình xử lý HTTP.

Tất cả những gì bạn cần làm là đảm bảo rằng hàm của bạn có chữ ký sau: func (http.ResponseWriter, * http.Request) ; sau đó, chuyển đổi nó thành http.HandlerFunc loại.

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)
}

Bạn thậm chí có thể thay thế mux.Handle dòng trong main hàm trên với mux.HandleFunc và truyền trực tiếp hàm cho nó. Chúng tôi đã sử dụng mô hình này hoàn toàn trong bài viết trước.

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/john", helloJohnHandler)
    http.ListenAndServe(":8080", mux)
}

Tại thời điểm này, tên được mã hóa cứng thành chuỗi, không giống như trước đây khi chúng ta có thể đặt tên trong main trước khi gọi trình xử lý. Loại bỏ hạn chế này, chúng ta có thể đặt logic trình xử lý của mình vào một bao đóng, như được hiển thị dưới đây:

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 bản thân hàm không đáp ứng http.Handler nhưng nó tạo và trả về một chức năng ẩn danh. Chức năng này đóng trên name , có nghĩa là nó có thể truy cập bất cứ khi nào nó được gọi. Tại thời điểm này, helloHandler chức năng có thể được sử dụng lại cho nhiều tên khác nhau nếu cần.

Vì vậy, tất cả những điều này có liên quan gì đến phần mềm trung gian? Chà, việc tạo một chức năng phần mềm trung gian được thực hiện theo cách tương tự như chúng ta đã thấy ở trên. Thay vì bỏ qua một chuỗi cho bao đóng (như trong ví dụ), chúng ta có thể chuyển trình xử lý văn bản trong chuỗi dưới dạng một đối số.

Đây là mẫu hoàn chỉnh:

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 Hàm trên chấp nhận một trình xử lý và trả về một trình xử lý. Lưu ý cách chúng tôi có thể làm cho hàm ẩn danh thỏa mãn http.Handler bằng cách truyền nó tới một http.HandlerFunc loại hình. Khi kết thúc chức năng ẩn danh, quyền kiểm soát được chuyển sang tiếp theo xử lý bằng cách gọi ServeHTTP () phương pháp. Nếu bạn cần chuyển các giá trị giữa các trình xử lý, chẳng hạn như ID của người dùng đã xác thực, bạn có thể sử dụng http.Request.Context () được giới thiệu trong Go 1.7.

Hãy viết một hàm middleware thể hiện mô hình này một cách đơn giản. Chức năng này thêm một thuộc tính được gọi là requestTime tới đối tượng yêu cầu, được helloHandler sử dụng trước đây để hiển thị dấu thời gian của một yêu cầu.

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)
}

Phát triển web trong Go:Middleware, Templating, Databases &Beyond

Vì hàm phần mềm trung gian của chúng tôi chấp nhận và trả về một http.Handler loại, thật khó có thể tạo ra một chuỗi vô hạn các chức năng phần mềm trung gian được lồng bên trong eachother.

Ví dụ:

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

Bạn có thể sử dụng một thư viện như Alice để chuyển cấu trúc trên thành một biểu mẫu dễ đọc hơn, chẳng hạn như:

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

Tạo mẫu

Mặc dù việc sử dụng các mẫu đã giảm dần với sự ra đời của các ứng dụng trang đơn, nhưng nó vẫn là một khía cạnh quan trọng của một quá trình phát triển web hoàn chỉnh.

Go cung cấp hai gói cho tất cả các nhu cầu tạo khuôn mẫu của bạn: text / template html / template . Cả hai đều có giao diện giống nhau, nhưng giao diện thứ hai sẽ thực hiện một số mã hóa đằng sau hậu trường để bảo vệ chống lại việc khai thác chèn mã.

Mặc dù các mẫu Go không phải là biểu cảm nhất hiện có, nhưng chúng hoàn thành công việc tốt và có thể được sử dụng cho các ứng dụng sản xuất. Trên thực tế, đó là những gì Hugo, trình tạo trang web tĩnh phổ biến, dựa trên hệ thống tạo khuôn mẫu của nó.

Hãy cùng xem nhanh cách html / template gói có thể được sử dụng để gửi HTML đầu ra như một phản hồi cho một yêu cầu web.

Tạo mẫu

Tạo index.html tệp trong cùng thư mục với main.go của bạn gửi và thêm mã sau vào tệp:

<ul>
  {{ range .TodoItems }}
  <li>{{ . }}</li>
  {{ end }}
</ul>

Tiếp theo, thêm mã sau vào main.go của bạn tệp:

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)
    }
}

Nếu bạn thực hiện chương trình trên với , hãy chạy main.go . Bạn sẽ thấy đầu ra sau đây:

<ul>
  <li>Watch TV</li>
  <li>Do homework</li>
  <li>Play games</li>
  <li>Read</li>
</ul>

Xin chúc mừng! Bạn vừa tạo mẫu cờ vây đầu tiên của mình. Đây là bản rút gọn cú pháp mà chúng tôi đã sử dụng trong tệp mẫu:

  • Go sử dụng dấu ngoặc kép ( {{}} ) để phân định cấu trúc kiểm soát và đánh giá dữ liệu (được gọi là hành động ) trong các mẫu.
  • Phạm vi là cách chúng tôi có thể lặp lại các cấu trúc dữ liệu, chẳng hạn như các lát cắt.
  • . đại diện cho bối cảnh hiện tại. Trong phạm vi hành động, bối cảnh hiện tại là phần của todos . Bên trong khối, {{. }} đề cập đến từng phần tử trong lát.

Trong main.go tệp, template.ParseFiles được sử dụng để tạo một newtemplate từ một hoặc nhiều tệp. Mẫu này sau đó được thực thi bằng cách sử dụng template.Execute phương pháp; nó cần một io.Writer và dữ liệu, sẽ được áp dụng cho mẫu.

Trong ví dụ trên, mẫu được thực thi với đầu ra tiêu chuẩn, nhưng wecan thực thi nó đến bất kỳ đích nào, miễn là nó đáp ứng io.Writer giao diện. Ví dụ:nếu bạn muốn trả lại đầu ra như một phần của webrequest, tất cả những gì bạn cần làm là thực thi mẫu tới ResponseWriter giao diện, như được hiển thị bên dưới.

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)
}

Phát triển web trong Go:Middleware, Templating, Databases &Beyond

Phần này chỉ nhằm mục đích giới thiệu nhanh các gói mẫu của Go. Hãy đảm bảo xem tài liệu về thetext / template vàhtml / template nếu bạn quan tâm đến các trường hợp sử dụng phức tạp hơn.

Nếu bạn không phải là người yêu thích cách tạo khuôn mẫu của cờ vây, thì vẫn tồn tại các lựa chọn thay thế, chẳng hạn như thư viện Plush.

Làm việc với JSON

Nếu bạn cần làm việc với các đối tượng JSON, bạn sẽ rất vui khi biết rằng thư viện tiêu chuẩn của Go bao gồm mọi thứ bạn cần để phân tích cú pháp và mã hóa JSON thông qua encoding / json gói hàng.

Loại mặc định

Khi mã hóa hoặc giải mã một đối tượng JSON trong Go, các loại sau được sử dụng:

  • bool dành cho boolean JSON,
  • float64 cho số JSON,
  • string đối với chuỗi JSON,
  • nil đối với JSON null,
  • map [string] giao diện {} cho các đối tượng JSON và
  • [] giao diện {} cho mảng JSON.

Mã hóa

Để mã hóa cấu trúc dữ liệu dưới dạng JSON, json.Marshal chức năng được sử dụng. Đây là một ví dụ:

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))
}

Trong chương trình trên, chúng ta có một Person cấu trúc với bốn trường khác nhau. Trong main hàm, một bản sao của Person được tạo với tất cả các trường json.Marshal sau đó, phương thức được sử dụng để chuyển đổi p cấu trúc thành JSON. Phương thức này trả về một phần byte hoặc một lỗi mà chúng tôi phải xử lý trước khi truy cập vào dữ liệu JSON.

Để chuyển đổi một lát byte thành một chuỗi trong Go, chúng ta cần thực hiện chuyển đổi kiểu chữ, như đã trình bày ở trên. Chạy chương trình này sẽ tạo ra đầu ra sau:

{"FirstName":"Abraham","LastName":"Freeman","Age":100}

Như bạn có thể thấy, chúng tôi nhận được một đối tượng JSON hợp lệ có thể được sử dụng theo bất kỳ cách nào mà chúng tôi muốn. Lưu ý rằng email trường được bỏ ra ngoài kết quả. Điều này là do nó không được xuất từ ​​ Person đối tượng bằng cách bắt đầu bằng chữ cái viết hoa.

Theo mặc định, Go sử dụng cùng tên thuộc tính trong cấu trúc làm tên trường trong đối tượng JSON kết quả. Tuy nhiên, điều này có thể được thay đổi thông qua việc sử dụng các thẻ structfield.

type Person struct {
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    Age       int    `json:"age"`
    email     string `json:"email"`
}

Các thẻ trường struct ở trên chỉ định rằng bộ mã hóa JSON sẽ ánh xạ FirstName trong cấu trúc thành một first_name trong đối tượng JSON, v.v. Thay đổi này trong ví dụ trước tạo ra kết quả sau:

{"first_name":"Abraham","last_name":"Freeman","age":100}

Giải mã

json.Unmarshal hàm được sử dụng để giải mã một đối tượng JSON thành một Gostruct. Nó có chữ ký sau:

Lỗi
func Unmarshal(data []byte, v interface{}) error

Nó chấp nhận một byte dữ liệu JSON và một nơi để lưu trữ dữ liệu đã được giải mã. Nếu giải mã thành công, lỗi trả về sẽ là nil .

Giả sử chúng ta có đối tượng JSON sau,

json := "{"first_name":"John","last_name":"Smith","age":35, "place_of_birth": "London", gender:"male"}"

Chúng tôi có thể giải mã nó thành một bản sao của Person struct, như được hiển thị bên dưới:

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)
}

Và bạn nhận được kết quả sau:

{FirstName:John LastName:Smith Age:35 email:}

Không gây chết người chỉ giải mã các trường được tìm thấy trong kiểu đích. Trong trường hợp này, place_of_birth giới tính bị bỏ qua vì chúng không ánh xạ nhiều trường cấu trúc trong Person . Hành vi này có thể được tận dụng để chọn chỉ một vài trường cụ thể từ một đối tượng JSON lớn. Như trước đây, các trường chưa được báo cáo trong cấu trúc đích không bị ảnh hưởng ngay cả khi chúng có trường tương ứng trong đối tượng JSON. Đó là lý do tại sao email vẫn là một chuỗi trống trong đầu ra mặc dù nó có trong đối tượng JSON.

Cơ sở dữ liệu

database / sql gói cung cấp một giao diện chung xung quanh cơ sở dữ liệu SQL (hoặc giống SQL). Nó phải được sử dụng cùng với một trình điều khiển cơ sở dữ liệu, chẳng hạn như các lý thuyết được liệt kê ở đây. Khi nhập trình điều khiển cơ sở dữ liệu, bạn cần phải đặt trước nó bằng dấu gạch dưới _ để khởi tạo.

Ví dụ:đây là cách sử dụng gói MySQLdriver với database / sql :

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

Dưới mui xe, trình điều khiển tự đăng ký là có sẵn cho database / sql nhưng nó sẽ không được sử dụng trực tiếp trong mã của chúng tôi. Điều này giúp giảm bớt sự phụ thuộc vào một trình điều khiển cụ thể để có thể dễ dàng hoán đổi nó cho một trình điều khiển khác với nỗ lực tối thiểu.

Mở kết nối cơ sở dữ liệu

Để truy cập cơ sở dữ liệu, bạn cần tạo sql.DB đối tượng, như được hiển thị bên dưới:

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
    if err != nil {
        log.Fatal(err)
    }
}

sql.Open phương thức chuẩn bị cho cơ sở dữ liệu trừu tượng để sử dụng sau này. Nó không thiết lập kết nối với cơ sở dữ liệu hoặc xác thực các tham số kết nối. Nếu bạn muốn đảm bảo rằng cơ sở dữ liệu có sẵn và có thể truy cập ngay lập tức, hãy sử dụng db.Ping () phương pháp:

err = db.Ping()
if err != nil {
  log.Fatal(err)
}

Đóng kết nối cơ sở dữ liệu

Để đóng kết nối cơ sở dữ liệu, bạn có thể sử dụng db.Close () . Thông thường, bạn muốn hoãn lại việc đóng cơ sở dữ liệu cho đến khi hàm đã mở kết nối dữ liệu kết thúc, thường là main chức năng:

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 đối tượng được thiết kế để tồn tại lâu dài, vì vậy bạn không nên mở và đóng nó thường xuyên. Nếu bạn làm vậy, bạn có thể gặp phải các vấn đề, chẳng hạn như kết nối kém và chia sẻ kết nối kém, hết tài nguyên mạng có sẵn, lỗi không thường xuyên. Tốt nhất là chuyển sql.DB xung quanh hoặc làm cho nó khả dụng trên toàn cầu và chỉ đóng nó khi chương trình được truy cập xong vào kho lưu trữ đó.

Tìm nạp dữ liệu từ cơ sở dữ liệu

Truy vấn một bảng có thể được thực hiện trong ba bước. Đầu tiên, hãy gọi db.Query () . Sau đó, lặp lại các hàng. Cuối cùng, sử dụng row.Scan () để trích xuất từng hàng thành các biến. Đây là một ví dụ:

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)
}

Nếu truy vấn trả về một hàng, bạn có thể sử dụng db.QueryRow thay vì db.Query và tránh một số đoạn mã soạn sẵn dài dòng trong đoạn mã trước:

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)

Cơ sở dữ liệu NoSQL

Go cũng hỗ trợ tốt cho cơ sở dữ liệu NoSQL, chẳng hạn như Redis, MongoDB, Cassandra, và những thứ tương tự, nhưng nó không cung cấp giao diện tiêu chuẩn để làm việc với nó. Bạn sẽ phải hoàn toàn dựa vào gói trình điều khiển cho cơ sở dữ liệu cụ thể. Một số ví dụ được liệt kê bên dưới.

  • https://github.com/go-redis/redis (Trình điều khiển Redis).
  • https://github.com/mongodb/mongo-go-driver (trình điều khiển MongoDB).
  • https://github.com/gocql/gocql (trình điều khiển Cassandra).
  • https://github.com/Shopify/sarama (Trình điều khiển Apache Kafka)

Kết thúc

Trong bài viết này, chúng tôi đã thảo luận về một số khía cạnh thiết yếu của việc xây dựng các ứng dụng web với Go. Bây giờ bạn có thể hiểu tại sao nhiều Goprogrammers chấp nhận thư viện chuẩn. Nó rất toàn diện và cung cấp hầu hết các công cụ cần thiết cho một dịch vụ sẵn sàng sản xuất.

Nếu bạn yêu cầu giải thích rõ về bất kỳ điều gì chúng tôi đã đề cập ở đây, vui lòng gửi cho tôi thông báo trên Twitter. Trong phần tiếp theo và phần cuối cùng của loạt bài này, chúng ta sẽ thảo luận về go và cách sử dụng nó để giải quyết các nhiệm vụ phổ biến trong quá trình phát triển với Go.

Cảm ơn bạn đã đọc và chúc bạn viết mã vui vẻ!