Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm Chủ Phát Triển Web Go: Middleware, Templating, Database & More

Middleware Trong Go

Thực hành thiết lập shared functionality cần chạy cho nhiều hoặc tất cả HTTP requests được gọi là middleware. Một số operation, như authentication, logging, và cookie validation, thường được implement as middleware functions, which act on a request independently before or after the regular route handlers.

Để implement middleware trong Go, bạn cần chắc chắn bạn có type thỏa mãn http.Handler interface. Thông thường, nghĩa là bạn cần attach method với signature ServeHTTP(http.ResponseWriter, *http.Request) đến type. Khi dùng method này, bất kỳ type nào sẽ thỏa mãn http.Handler interface.

Đây là 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ỳ request gửi đến route /john sẽ được passed straight đến helloHandler.ServeHTTP method. Bạn có thể observe this in action bằng cách start server và heading to https://localhost:8080/john.

Việc phải thêm ServeHTTP method vào custom type mỗi khi bạn muốn implement http.Handler sẽ khá tedious, vì vậy net/http package cung cấp type http.HandlerFunc, cho phép dùng ordinary functions as HTTP handlers.

Tất cả bạn cần làm là ensure function của bạn có signature: func(http.ResponseWriter, *http.Request); sau đó, convert nó to http.HandlerFunc type.

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ể replace mux.Handle line trong main function trên với mux.HandleFunc và pass function to it directly. Chúng ta used this pattern exclusively in previous article.


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

Ở điểm này, name được hardcoded vào string, không giống trước khi chúng ta able to set name in main function before calling handler. Để remove limitation này, chúng ta có thể put handler logic vào closure, như dưới:


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 function itself không thỏa mãn http.Handler interface, nhưng nó creates và returns anonymous function that does. Function này closes over name parameter, nghĩa là nó có thể access nó whenever it is called. Tại điểm này, helloHandler function có thể reused cho nhiều name khác nhau.

Vậy, all this có liên quan gì đến middleware? Well, tạo middleware function được done in the same way as we've seen above. Thay vì pass string to closure (as in example), chúng ta có thể pass next handler in chain as argument.

Đây là complete pattern:


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 function trên accepts a handler và returns a handler. Chú ý cách chúng ta able to make anonymous function thỏa mãn http.Handler interface bằng cách cast nó to http.HandlerFunc type. At the end of anonymous function, control transferred to next handler bằng cách invoke ServeHTTP() method. Nếu bạn cần pass values giữa handlers, như ID của authenticated user, bạn có thể dùng http.Request.Context() method introduced in Go 1.7.

Hãy viết middleware function đơn giản demonstrate pattern này. Function này thêm property gọi là requestTime vào request object, which is subsequently utilized by helloHandler để display timestamp của request.


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

Làm Chủ Phát Triển Web Go: Middleware, Templating, Database & More

Vì middleware function của chúng ta accepts và returns http.Handler type, nó possible to create infinite chain of middleware functions nested inside each other.

Ví dụ,


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

Bạn có thể dùng library như Alice để transform above construct to a more readable form như:


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

Templating

Mặc dù việc dùng templates đã wane với advent of single-page applications, nó vẫn là important aspect của complete web development solution.

Go cung cấp hai packages cho all your templating needs: text/template và html/template. Cả hai có same interface, nhưng cái sau sẽ do some encoding behind the scenes để guard against code injection exploits.

Mặc dù Go templates không phải most expressive out there, chúng get the job done just fine và có thể dùng cho production applications. In fact, it's what Hugo, the popular static site generator, bases its templating system on.

Hãy take a quick look at how html/template package may be used to send HTML output as response to a web request.

Tạo Một Template

Tạo file index.html trong same directory as your main.go file và add following code to file:


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

Tiếp theo, add following code to your main.go file:


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 execute above program with go run main.go. You should see following output:


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

Chúc mừng! Bạn vừa tạo first Go template. Đây là short explanation của syntax chúng ta used in template file:

  • Go uses double braces ({{ và }}) để delimit data evaluation và control structures (known as actions) in templates.
  • range action là how we're able to iterate over data structures, như slices.
  • . represents current context. In range action, current context là slice của todos. Inside block, {{ . }} refers to each element in slice.

Trong main.go file, template.ParseFiles method được used to create new template từ one or more files. Template này subsequently executed using template.Execute method; it takes an io.Writer và the data, which will be applied to template.

Trong above example, template executed to standard output, nhưng chúng ta có thể execute it to any destination, as long as it thỏa mãn io.Writer interface. Ví dụ, nếu bạn want return output as part of web request, all you need to do is execute template to ResponseWriter interface, as shown below.


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

Làm Chủ Phát Triển Web Go: Middleware, Templating, Database & More

Section này only meant to be quick intro to Go's template packages. Hãy chắc chắn check out documentation cho text/template và html/template nếu bạn interested in more complex use cases.

Nếu bạn không fan của cách Go làm templating, alternatives exist, như Plush library.

Working With JSON

Nếu bạn need to work with JSON objects, bạn sẽ pleased to hear that Go's standard library includes everything you need to parse và encode JSON through encoding/json package.

Default Types

Khi encoding hoặc decoding JSON object in Go, following types are used:

  • bool cho JSON booleans,
  • float64 cho JSON numbers,
  • string cho JSON strings,
  • nil cho JSON null,
  • map[string]interface{} cho JSON objects, và
  • []interface{} cho JSON arrays.

Encoding

Để encode data structure as JSON, json.Marshal function is used. Here's an example:


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 above program, we have a Person struct with four different fields. In main function, an instance of Person is created with all fields initialized. json.Marshal method is then used to convert p structure to JSON. This method returns a slice of bytes or an error, which we have to handle before accessing JSON data.

Để convert slice of bytes to string in Go, we need to perform type conversion, as demonstrated above. Running this program will produce following output:


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

As you can see, we get a valid JSON object that can be used in any way we want. Note that email field is left out of result. This is because it is not exported from Person object by virtue of starting with a lowercase letter.

By default, Go uses same property names in struct as field names in resulting JSON object. However, this can be changed through the use of struct field tags.


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

The struct field tags above specify that the JSON encoder should map the FirstName property in the struct to a first_name field in the JSON object and so on. This change in the previous example produces the following output:


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

Decoding

json.Unmarshal function is used for decoding a JSON object into a Go struct. It has the following signature:


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

It accepts a byte slice of JSON data và a place to store the decoded data. If the decoding is successful, the error returned will be nil.

Giả sử chúng ta có following JSON object,


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

We can decode it to an instance of the Person struct, as shown below:


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

And you get the following output:


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

Unmarshal only decodes fields that are found in the destination type. In this case, place_of_birth và gender are ignored since they do not map to any struct field in Person. This behavior can be leveraged to pick only a few specific fields out of a large JSON object. As before, unexported fields in the destination struct are unaffected even if they have a corresponding field in the JSON object. That's why email remains an empty string in the output even though it is present in the JSON object.

Databases

database/sql package provides a generic interface around SQL (or SQL-like) databases. It must be used in conjunction with a database driver, như những cái listed here. Khi importing a database driver, bạn need to prefix it with an underscore _ to initialize it.

Ví dụ, here's how to use the MySQL driver package with database/sql:


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

Under the hood, the driver registers itself as being available to the database/sql package, but it won't be used directly in our code. This helps us reduce dependency on a specific driver so that it can be easily swapped out for another with minimal effort.

Mở Một Database Connection

Để access a database, bạn need to create a sql.DB object, as shown below:


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

sql.Open method prepares the database abstraction for later use. It does not establish a connection to the database or validate the connection parameters. Nếu bạn want ensure that the database is available and accessible immediately, use the db.Ping() method:


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

Đóng Một Database Connection

Để close a database connection, bạn có thể dùng db.Close(). Normally, you want to defer the closing of the database until the function that opened the database connection ends, usually the main function:


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 object is designed to be long-lived, so you should not open and close it frequently. Nếu bạn làm vậy, bạn may experience problems, như poor reuse và sharing of connections, running out of available network resources, hoặc sporadic failures. It's best to pass the sql.DB method around or make it available globally và only close it when the program is done accessing that datastore.

Lấy Dữ Liệu Từ Database

Querying a table can be done in three steps. First, call db.Query(). Then, iterate over the rows. Finally, use rows.Scan() to extract each row into variables. Here's an example:


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 query returns a single row, bạn có thể dùng db.QueryRow method instead of db.Query và avoid some of the lengthy boilerplate code in previous code snippet:


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 Databases

Go also has good support for NoSQL databases, như Redis, MongoDB, Cassandra, và the like, but it does not provide a standard interface for working with them. You'll have to rely entirely on the driver package for the specific database. Some examples are listed below.

  • https://github.com/go-redis/redis (Redis driver).
  • https://github.com/mongodb/mongo-go-driver (MongoDB driver).
  • https://github.com/gocql/gocql (Cassandra driver).
  • https://github.com/Shopify/sarama (Apache Kafka driver)

Kết Thúc

Trong bài này, chúng ta discussed some essential aspects of building web applications with Go. You should now be able to understand why many Go programmers swear by the standard library. It's very comprehensive và provides most of the tools necessary for a production-ready service.

Nếu bạn require clarification on anything we've covered here, please send me a message on Twitter. In the next and final article in this series, we'll discuss the go tool và how to use it to tackle common tasks in the course of developing with Go.

Thanks for reading, and happy coding!