通过示例学习go Web编程

翻译《通过示例学习go Web编程》

Posted by 排球混子 on October 12, 2023

通过示例学习go web编程

排球混子

来源: https://gowebexamples.com/

Hello World

介绍

Go 是一门内置了许多功能的编程语言,并且已经内置了一个Web服务器。标准库中的net/http包包含了有关HTTP协议的所有功能.

代码

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
    })

    http.ListenAndServe(":80", nil)
}

项目准备

新建一个项目,名称为gowebexamples mysql环境 可使用docker

HTTP Server

介绍

接下来将在Go中创建一个基本的HTTP服务, 第一步先讨论一下HTTP服务应该具备的能力

  • 处理动态请求:处理来自浏览网站、登录帐户或发布图片的用户的传入请求
  • 提供静态资源:向浏览器提供JS、CSS和图像,以创建用户的动态体验
  • 接受连接:HTTP服务必须监听特定的端口以能够接受来自互联网的连接

处理动态请求

nte/http包包含了接受请求和动态处理请求所需要的所有工具, 我们可以通过http.HandleFunc注册一个新的处理函数。第一个参数表示匹配的路径,第二个参数是要执行的方法。

http.HandleFunc("/", func(w, http.ResponseWriter, r *http.Request){
	fmt.Fprint(w, "Welcome to my website")
})

对于动态方面, http.Request包含关于请求及参数的所有信息。 你可以通过r.URL.Query().Get("token")来读取GET的请求参数 或者 通过r.FormValue("email")来读取POST的参数(来自HTML表单的字段)

提供静态资源

为了提供像JS、CSS和图像这样的静态资源, 我们通过内置http.FileServcer指定一个url路径 。为了让服务器正常工作它需要知道从哪里提供文件。

fs := http.FileServer(http.Dir("static/"))

一旦我们的文件就位, 我们只需要指向这个url路径,像我们在处理动态请求时那样。 需要注意的一点是:为了正确的提供文件, 我们需要去掉url路径的一部分。通常是我们文件所在目录的名称。

http.Handle("/static/", http.StripPrefix("/static/", fs))

接受请求

为完成我们的静态服务器最后需要做的事, 去监听一个端口以接受来自互联网的请求。Go也有一个内置的HTTP server, 我们可以快速的启动。一旦启动,你可以通过浏览器查看你的HTTP server

http.ListenAndServer(":80", nil)

代码

// gowebexample/main.go
package main
import (
	"fmt"
	"net/http"
)

func main(){
	http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request){
		 fmt.Fprintf(w, "Welcome to my website!")
    })

    fs := http.FileServer(http.Dir("static/"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    http.ListenAndServe(":80", nil)
}

Routing(使用 gorilla/mux)

介绍

Go的net/http为HTTP协议提供了很多功能,但是它在如何将复杂的路由请求分割成单个参数上做的不是很好。幸运的是, Go社区中有个非常受欢迎的包,以良好的代码质量而闻名。在这个示例中,你会看到如何使用gorilla/mux包来创建携带参数的路由、GET/POST处理和域名限制。

安装gorilla/mux

gorilla/mux是一个适配Go默认HTTP路由器的包,它提供了很多功能以提高编写web的生产力。它还与Go默认的请求签名处理器func (w http.ResponseWriter, r *http.Request)兼容,因此他可以跟其他HTTP库(比如中间件或现有应有)混合使用。

go get -u github.com/gorilla/mux

创建新的路由器

首先创建一个新的请求路由。 路由器是Web应用程序的主要路由器,并将参数传递给服务器。 它会接收所有的HTTP连接并将其传递给你在其上注册的请求处理器。

r := mux.NewRouter()

URL参数

gorilla/mux路由器最大的优势就是可以从请求URL上提取片。例如,这是你应用程序的URL。

/books/go-programming/page/10

URL有两个动态片段 书名 (go-programming) 页数 (10) 要让请求处理器匹配上述的URL匹配,你可以在你的URL模式中替换动态片段为占位符,像这样:

r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {
	// get the book
	// navigate to the page
})

最后一件事是从URL片段中获取数据。 这个包提供了mux.Vars(r)函数,它以http.Request 作为参数并返回一个片段映射

func(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	vars["title"] // the book
	vars["page"] // the page
}

设置HTTP服务的路由器

有想过nilhttp.ListenAndServe(":80", nil)是什么意思吗?这个参数是HTTP服务主路由器的参数。默认情况下是nil。代表着默认使用net/http包。要是用你自己的路由器,替换nil变量为你的路由器r

http.ListenAndServer(":80", r)

代码

// gowebexample/main.go
package main  
  
import (  
    "fmt"  
    "github.com/gorilla/mux"    "net/http")  
  
func main() {  
    r := mux.NewRouter()  
    r.HandleFunc("/books/{title}/page/{page}", func(w http.ResponseWriter, r *http.Request) {  
       vars := mux.Vars(r)  
       title := vars["title"]  
       page := vars["page"]  
       fmt.Fprintf(w, "You've requested the book: %s on page %s\n", title, page)  
    })  
  
    http.ListenAndServe(":80", r)  
}

gorilla/mux路由器的功能

Methods 限制请求处理器为特定的HTTP方法

```go
r.HandleFunc("/books/{title}", CreateBook).Methods("POST")
r.HandleFunc("/books/{title}", ReadBook).Methods("GET")
r.HandleFunc("/books/{title}", UpdateBook).Methods("PUT")
r.HandleFunc("/books/{title}", DeleteBook).Methods("DELETE")

域名 & 子域名

限制请求处理器为特定的域名或子域名

r.HandleFunc("/books/{title}", BookHandler).Host("www.mybookstore.com")

Schemes

限制请求处理器为 http/https

r.HandleFunc("/secure", SecureHandler).Schemes("Https")
r.HandleFunc("/insecure", InsecureHandler).Schemes("Http")

MySQL Database

介绍

懒得介绍,数据库的作用就是为了存储数据。在Web开发中,一个常用的数据库是MySQL数据库。在这个示例,将介绍在Go中与数据库交互的一些操作。

安装

Go语言提供一个了便捷的包,称作database/sql用来查询各种SQL数据库。这对你来说很有用,因为它将所有常见的SQL特性都抽象成一个单一的API供你使用。不过Go没有包含数据库驱动。在Go中,数据库驱动是一个实现特定数据库的底层细节的包。

go get -u github.com/go-sql-driver/mysql

连接MySQL数据库

在安装完所有必须的包之后我们需要检查的第一件事是: 如果我们不能成功连接我们的MySQL数据库,如果你没有已经运行的MySQL数据库服务,你可以用Docker启动一个。 为了检查我们能否连接数据库,引入database/sqlgo-sql-driver/mysql

import (  
    "database/sql"  
    "fmt"    
    _ "github.com/go-sql-driver/mysql"  
)
func main() {  
    db, err := sql.Open("mysql", "user:password@(127.0.0.1:3307)/your_db_name?parseTime=true")  
    if err != nil {  
       fmt.Println("Failed to connect to the database", err)  
       return  
    }  
    err = db.Ping()  
    if err != nil {  
       fmt.Println("Failed to ping the database", err)  
       return  
    }  
    fmt.Println("Connected to the database")
}

代码

package main  
  
import (  
    "database/sql"  
    "fmt"    _ "github.com/go-sql-driver/mysql"  
    "log"    "time")  
  
type User struct {  
    id        int  
    username  string  
    password  string  
    createdAt time.Time  
}  
  
func main() {  
    db, err := sql.Open("mysql", "user:password@(127.0.0.1:3307)/your_db_name?parseTime=true")  
    if err != nil {  
       fmt.Println("Failed to connect to the database", err)  
       return  
    }  
    err = db.Ping()  
    if err != nil {  
       fmt.Println("Failed to ping the database", err)  
       return  
    }  
    fmt.Println("Connected to the database")  
  
    {  // 创建新表
       query := `  
         CREATE TABLE users (         id INT AUTO_INCREMENT,         username TEXT NOT NULL,         password TEXT NOT NULL,         created_at DATETIME,         PRIMARY KEY (id)       );`  
       _, err = db.Exec(query)  
       if err != nil {  
          fmt.Println("Failed to Exec the query")  
       }  
       fmt.Println("Exec the query")  
    }  
    { // 查询单个用户
       username := "Vic"  
       password := "secret"  
       createdAt := time.Now()  
      
       result, err := db.Exec(`INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)`, username, password, createdAt)  
      
       if err != nil {  
          fmt.Println("Failed to Exec the query of the insert")  
       }  
       userId, err := result.LastInsertId()  
      
       if err != nil {  
          fmt.Println("Failed to get the LastInsertId")  
       }  
       fmt.Printf("userId is %s\n", userId)  
    }  
  
    { // 查询全部用户
       var user User  
  
       query := `SELECT id, username, password, created_at FROM users WHERE id = ?`  
  
       err = db.QueryRow(query, 1).Scan(&user.id, &user.username, &user.password, &user.createdAt)  
       if err != nil {  
          fmt.Println("Failed to Exec the query of the Query", err)  
          return  
       }  
       fmt.Println(user, user.createdAt)  
    }  
  
    { // Query all users  
       rows, err := db.Query(`SELECT id, username, password, created_at FROM users`)  
  
       if err != nil {  
          fmt.Println("Failed to Query")  
       }  
       defer rows.Close()  
  
       var users []User  
  
       for rows.Next() {  
          var user User  
          err := rows.Scan(&user.id, &user.username, &user.password, &user.createdAt)  
          if err != nil {  
             log.Fatal(err)  
          }  
          users = append(users, user)  
  
       }  
  
       err = rows.Err()  
       if err != nil {  
          fmt.Println("err is", err)  
       }  
  
       fmt.Printf("%#v", users)  
    }  
}

中间件(基础)

介绍

中间件只需要用http.HandlerFunc作为参数,将其包装然后返回一个新的http.HandlerFunc

// basic-middleware.go
package main  
  
import (  
    "fmt"  
    "log"    "net/http")  
  
func logging(f http.HandlerFunc) http.HandlerFunc {  
    return func(w http.ResponseWriter, r *http.Request) {  
       log.Println(r.URL.Path)  
       f(w, r)  
    }  
}  
  
func foo(w http.ResponseWriter, r *http.Request) {  
    fmt.Fprintln(w, "foo")  
}  
  
func bar(w http.ResponseWriter, r *http.Request) {  
    fmt.Fprintln(w, "bar")  
}  
  
func main() {  
    http.HandleFunc("/foo", logging(foo))  
  
    http.HandleFunc("/bar", logging(bar))  
  
    http.ListenAndServe(":8080", nil)  
}

中间件(高级)

介绍

中间件本身只是将 http.HandlerFunc 作为其参数之一,对其进行包装并返回一个新的 http.HandlerFunc,以便服务器调用。

在这里,我们定义了一个新类型 Middleware,它使链多个中间件在一起变得更容易

package main  
  
import (  
    "fmt"  
    "log"    "net/http"    "time")  
  
//func createNewMiddleware() Middleware {  
//  Middleware := func(next http.HandlerFunc) http.HandlerFunc {  
//     handler := func(w http.ResponseWriter, r *http.Request) {  
//  
//        next(w, r)  
//     }  
//     return handler  
//  }  
//  return Middleware  
//}  
  
type Middleware func(handlerFunc http.HandlerFunc) http.HandlerFunc  
  
func Logging() Middleware {  
    return func(f http.HandlerFunc) http.HandlerFunc {  
       return func(w http.ResponseWriter, r *http.Request) {  
          start := time.Now()  
          defer func() { log.Println(r.URL.Path, time.Since(start)) }()  
  
          f(w, r)  
       }  
    }  
}  
  
func Method(m string) Middleware {  
    return func(f http.HandlerFunc) http.HandlerFunc {  
       return func(w http.ResponseWriter, r *http.Request) {  
          if r.Method != m {  
             http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)  
          }  
  
          f(w, r)  
       }  
    }  
}  
  
func Chain(f http.HandlerFunc, middleware ...Middleware) http.HandlerFunc {  
    for _, m := range middleware {  
       f = m(f)  
    }  
    return f  
}  
  
func Hello(w http.ResponseWriter, r *http.Request) {  
    fmt.Fprintln(w, "hello world")  
}  
  
func main() {  
    http.HandleFunc("/", Chain(Hello, Method("POST"), Logging()))  
    http.ListenAndServe(":8080", nil)  
}

Sessions

介绍

这个示例将展示如何在Go中使用流行的gorilla/sessions包将数据存储在会话cookie中。

Cookie是存储在用户浏览器中的小数据片段,并在每个请求中发送到我们的服务器。在这些cookie中,我们可以存储用户是否已登录到我们的网站,以及在我们的系统中他到底是谁。

// sessions.go
package main

import (
    "fmt"
    "net/http"

    "github.com/gorilla/sessions"
)

var (
    // key must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256)
    key = []byte("super-secret-key")
    store = sessions.NewCookieStore(key)
)

func secret(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "cookie-name")

    // Check if user is authenticated
    if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
        http.Error(w, "Forbidden", http.StatusForbidden)
        return
    }

    // Print secret message
    fmt.Fprintln(w, "The cake is a lie!")
}

func login(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "cookie-name")

    // Authentication goes here
    // ...

    // Set user as authenticated
    session.Values["authenticated"] = true
    session.Save(r, w)
}

func logout(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "cookie-name")

    // Revoke users authentication
    session.Values["authenticated"] = false
    session.Save(r, w)
}

func main() {
    http.HandleFunc("/secret", secret)
    http.HandleFunc("/login", login)
    http.HandleFunc("/logout", logout)

    http.ListenAndServe(":8080", nil)
}
$ go run sessions.go

$ curl -s http://localhost:8080/secret
Forbidden

$ curl -s -I http://localhost:8080/login
Set-Cookie: cookie-name=MTQ4NzE5Mz...

$ curl -s --cookie "cookie-name=MTQ4NzE5Mz..." http://localhost:8080/secret
The cake is a lie!

JSON

// json.go
package main  
  
import (  
    "encoding/json"  
    "fmt"    "net/http")  
  
type JsonUser struct {  
    Firstname string `json:"firstname"`  
    Lastname  string `json:"lastname"`  
    Age       int    `json:"age"`  
}  
  
func main() {  
    http.HandleFunc("/decode", func(w http.ResponseWriter, r *http.Request) {  
       var user JsonUser  
       json.NewDecoder(r.Body).Decode(&user)  
       fmt.Fprintf(w, "%s %s is %d years old!", user.Firstname, user.Lastname, user.Age)  
    })  
  
    http.HandleFunc("/encode", func(w http.ResponseWriter, r *http.Request) {  
       peter := JsonUser{  
          Firstname: "John",  
          Lastname:  "Doe",  
          Age:       32,  
       }  
       json.NewEncoder(w).Encode(peter)  
    })  
  
    http.ListenAndServe(":8080", nil)  
}

Websockets

这个示例将展示Websocket在Go中如何工作,我们会构建一个简单的服务, 它会回显我们发送的一切内容。我们需要安装gorilla/websocket库。

go get github.com/gorilla/websocket

代码

Go

// websockets.go  
package main  
  
import (  
    "fmt"  
    "net/http"  
    "github.com/gorilla/websocket")  
  
var upgrader = websocket.Upgrader{  
    ReadBufferSize:  1024,  
    WriteBufferSize: 1024,  
    CheckOrigin: func(r *http.Request) bool {  
       return true  
    },  
}  
  
func main() {  
    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {  
       conn, err := upgrader.Upgrade(w, r, nil) // error ignored for sake of simplicity  
  
       if err != nil {  
          fmt.Println("Error upgrading connection:", err)  
          return  
       }  
  
       defer conn.Close() // 确保在函数退出时关闭连接  
  
       for {  
          // Read message from browser  
          msgType, msg, err := conn.ReadMessage()  
          if err != nil {  
             return  
          }  
  
          // Print the message to the console  
          fmt.Printf("%v sent: %s\n", conn.RemoteAddr(), string(msg))  
  
          // Write message back to browser  
          if err = conn.WriteMessage(msgType, msg); err != nil {  
             return  
          }  
       }  
    })  
  
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {  
       http.ServeFile(w, r, "websockets.html")  
    })  
  
    err := http.ListenAndServe(":8080", nil)  
    if err != nil {  
       return  
    }  
}

html

<!-- websockets.html -->  
<input id="input" type="text" />  
<button onclick="send()">Send</button>  
<pre id="output"></pre>  
<script>  
    var input = document.getElementById("input");  
    var output = document.getElementById("output");  
    var socket = new WebSocket("ws://localhost:8080/echo");  
  
    socket.onopen = function () {  
        output.innerHTML += "Status: Connected\n";  
    };  
  
    socket.onmessage = function (e) {  
        output.innerHTML += "Server: " + e.data + "\n";  
    };  
  
    function send() {  
        socket.send(input.value);  
        input.value = "";  
    }  
</script>

密码哈希

此示例展示如何使用bcrypt对密码进行哈希处理 下载命令

$ go get golang.org/x/crypto/bcrypt
// passwords.go
package main  
  
import (  
    "fmt"  
    "golang.org/x/crypto/bcrypt")  
  
func HashPassword(password string) (string, error) {  
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)  
    return string(bytes), err  
}  
  
func CheckPasswordHash(password, hash string) bool {  
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))  
    return err == nil  
}  
func main() {  
    password := "KFC-VME50$"  
    hash, err := HashPassword(password)  
    if err != nil {  
       fmt.Println(err)  
    }  
    fmt.Println("Password:", password)  
    fmt.Println("Hash:", hash)  
  
    match := CheckPasswordHash(password, hash)  
    fmt.Println("Match:   ", match)  
}