Skip to content

Go Web 编程学习

Updated: at 04:58 AM

用 Golang 编写 Restful Api

启动一个 http 服务

通过闭包注册 HandleFunc

package main
import (
    "fmt"
	"io"
	"log"
	"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)
    })

    err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe:", err.Error())
	}
}

通过函数名注册 HandleFunc

import (
	"io"
	"log"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	io.WriteString(w, "Hello,World!")
}

func main() {
	http.HandleFunc("/hello", helloHandler)

	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe:", err.Error())
	}
}

通过对比得出结论

不管是不是匿名函数,每个 Handler 函数有两个参数,分别是 w http.ResponseWriter 和 r *http.Request,前者用来处理返回值,后者用来处理请求。 http 服务是由 http.ListenAndServe(“:8080”, nil) 启动的。 路由是由 http.HandleFunc(“/hello”, helloHandler) 控制的。 每个请求会按 path 分配到http.HandleFunc 注册的对应 Handler 中去处理。

处理 Rest 风格的请求

Go 的 net/http 包提供了很多功能,但是无法处理复杂的请求,例如将请求 url 分割为多个参数、区分不同 method 的请求。 gorilla/mux 刚好用来弥补 Golang 路由的不足,所以下边用 gorilla/mux 来实现高级的路由功能。

URL Parameters

package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
)

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

Methods

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

Hostnames & Subdomains

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

Schemes

r.HandleFunc("/secure", SecureHandler).Schemes("https")
r.HandleFunc("/insecure", InsecureHandler).Schemes("http")

Path Prefixes & Subrouters

bookrouter := r.PathPrefix("/books").Subrouter()
bookrouter.HandleFunc("/", AllBooks)
bookrouter.HandleFunc("/{title}", GetBook)

Json Encode & Json Decode

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type User struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Password string `json:"password"`
	Status   int    `json:"status"`
}

func main() {
    // 从 request 中获取一个 json
    http.HandleFunc("/decode", func(w http.ResponseWriter, r *http.Request) {
        var user User

        // 解码成 Go 对象
        json.NewDecoder(r.Body).Decode(&user)

        fmt.Fprintf(w, "Id: %s,Name: %s,Password: %s,Status: %d", user.ID, user.Name, user.Password, user.Status)
    })

    // 向 response 返回一个 json
    http.HandleFunc("/encode", func(w http.ResponseWriter, r *http.Request) {
        user := User{
            ID:       "123",
            Name:     "Doe",
            Password: "123456",
            Status:   1,
		}

        // 编码成json
        json.NewEncoder(w).Encode(user)
    })

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

用数据库为接口提供数据

常见的数据库操作

package model

import (
	"database/sql"
	"fmt"
	"log"

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

func do() {
	db, err := sql.Open("mysql", "root:toor@tcp(localhost:3306)/dev?tls=skip-verify&autocommit=true")
	if err != nil {
		log.Fatal(err)
	}
	if err := db.Ping(); err != nil {
		log.Fatal(err)
	}

	{ // Create a new table
		query := `
			CREATE TABLE IF NOT EXISTS users (
                id INT NOT NULL AUTO_INCREMENT,
                name VARCHAR(45) NOT NULL COMMENT '用户名',
                password VARCHAR(45) NOT NULL COMMENT '密码',
                status INT NOT NULL DEFAULT 0 COMMENT '0:有效帐户 1:无效帐户',
                PRIMARY KEY (id),
                UNIQUE INDEX idx_user_01 (name ASC))
                ENGINE = InnoDB
                DEFAULT CHARACTER SET = utf8
                COMMENT = '用户表'`

		if _, err := db.Exec(query); err != nil {
			log.Fatal(err)
		}
	}

	{ // Insert a new user
		name := "charles"
		password := "123456"
		status := 1

		result, err := db.Exec(`INSERT INTO users (name, password, status) VALUES (?, ?, ?)`, name, password, status)
		if err != nil {
			log.Fatal(err)
		}

		id, err := result.LastInsertId()
		fmt.Println(id)
	}

	{ // Query a single user
		var (
			id       int
			name     string
			password string
			status   int
		)

		query := "SELECT id, name, password, status FROM users WHERE id = ?"
		if err := db.QueryRow(query, 3).Scan(&id, &name, &password, &status); err != nil {
			log.Fatal(err)
		}

		fmt.Println(id, name, password, status)
	}

	{ // Query all users
		type user struct {
			id       int
			name     string
			password string
			status   int
		}

		rows, err := db.Query(`SELECT id, name, password, status FROM users`)
		if err != nil {
			log.Fatal(err)
		}
		defer rows.Close()

		var users []user
		for rows.Next() {
			var u user

			err := rows.Scan(&u.id, &u.name, &u.password, &u.status)
			if err != nil {
				log.Fatal(err)
			}
			users = append(users, u)
		}
		if err := rows.Err(); err != nil {
			log.Fatal(err)
		}

		fmt.Printf("%#v", users)
	}

    { // Update user
        result, err := db.Exec(`UPDATE users set name = ?, password =?, status=? where id=?`, user.Name, user.Password, user.Status, user.ID)
        if err != nil {
            log.Println(err)
            return
        }
    }

	{ // Delete user
		_, err := db.Exec(`DELETE FROM users WHERE id = ?`, 1)
		if err != nil {
			log.Fatal(err)
		}
	}
}

简单的数据库增删改查

package main

import (
	"database/sql"
	"encoding/json"
	_ "github.com/go-sql-driver/mysql"
	"io"
	"log"
	"net/http"
	"strconv"

	"github.com/gorilla/mux"
)

var db *sql.DB

type User struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Password string `json:"password"`
	Status   int    `json:"status"`
}

func main() {
	r := mux.NewRouter()

	r.HandleFunc("/user/list", GetUserList).Methods("GET")
	r.HandleFunc("/user/{id}", GetUser).Methods("GET")
	r.HandleFunc("/user", CreateUser).Methods("POST")
	r.HandleFunc("/user", UpdateUser).Methods("PUT")
	r.HandleFunc("/user/{id}", DeleteUser).Methods("DELETE")

	err := http.ListenAndServe(":80", r)
	if err != nil {
		log.Fatal(err)
	}
}

func GetUserList(w http.ResponseWriter, r *http.Request) {
	rows, err := db.Query(`SELECT id, name, password, status FROM users`)
	if err != nil {
		log.Println(err)
	}
	defer func(rows *sql.Rows) {
		err := rows.Close()
		if err != nil {
			log.Println(err)
		}
	}(rows)
	// 将查询结果转换成数组
	var users []User
	for rows.Next() {
		var u User

		err := rows.Scan(&u.ID, &u.Name, &u.Password, &u.Status)
		if err != nil {
			log.Println(err)
		}
		users = append(users, u)
	}
	if err := rows.Err(); err != nil {
		log.Println(err)
	}

	if len(users) == 0 {
		_, err := io.WriteString(w, "No users")
		if err != nil {
			log.Println("[GetUserList] No users")
			return
		}
		return
	}
	err = json.NewEncoder(w).Encode(users)
	if err != nil {
		log.Println("[GetUserList][Encode(users)]", err)
		return
	}
}

func GetUser(w http.ResponseWriter, r *http.Request) {
	// 获取路由参数
	vars := mux.Vars(r)
	var id = vars["id"]
	log.Println("[GetUser][id]", id)

	// 接收查询结果
	var (
		name     string
		password string
		status   int
	)

	query := "SELECT id, name, password, status FROM users WHERE id = ?"
	if err := db.QueryRow(query, id).Scan(&id, &name, &password, &status); err != nil {
		if _, err := io.WriteString(w, "User not found"); err != nil {
			log.Println("[GetUser][w.Write]", err)
			return
		}
		log.Println("[GetUser][db.QueryRow]", err)
		return
	}

	if err := json.NewEncoder(w).Encode(User{
		ID:       id,
		Name:     name,
		Password: password,
		Status:   status,
	}); err != nil {
		log.Println("[GetUser][Encode(User)]", err)
		return
	}
}

func CreateUser(w http.ResponseWriter, r *http.Request) {
	var user User
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		log.Println("[CreateUser][Decode(&user)]", err)
		return
	}

	if user.Name == "" || user.Password == "" {
		if _, err := io.WriteString(w, "Name/Password is empty"); err != nil {
			log.Printf("[CreateUser][io.WriteString] Name:{%s} Password:{%s} 最少有一个是空的", user.Name, user.Password)
			return
		}
		log.Printf("[CreateUser] Name:{%s} Password:{%s} 最少有一个是空的", user.Name, user.Password)
		return
	}

	result, err := db.Exec(`INSERT INTO users (name, password, status) VALUES (?, ?, ?)`, user.Name, user.Password, user.Status)
	if err != nil {
		if _, err := io.WriteString(w, user.Name+" already exists"); err != nil {
			log.Println("[CreateUser][db.Exec][w.Write]", err)
			return
		}
		log.Println("[CreateUser][db.Exec]", err)
		return
	}

	id, err := result.LastInsertId()
	if err != nil {
		log.Println("[CreateUser][result.LastInsertId]", err)
	}
	_, err = io.WriteString(w, "created user: "+strconv.FormatInt(id, 10))
	if err != nil {
		log.Println("[CreateUser][w.Write]", err)
		return
	}
}
func UpdateUser(w http.ResponseWriter, r *http.Request) {
	var user User
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		log.Println("[CreateUser][Decode(&user)]", err)
		return
	}

	if user.ID == "" || user.Name == "" || user.Password == "" {
		if _, err := io.WriteString(w, "ID/Name/Password is empty"); err != nil {
			log.Printf("[UpdateUser][io.WriteString] ID:{%s} Name:{%s} Password:{%s} 最少有一个是空的", user.ID, user.Name, user.Password)
			return
		}
		log.Printf("[UpdateUser] ID:{%s} Name:{%s} Password:{%s} 最少有一个是空的", user.ID, user.Name, user.Password)
		return
	}

	result, err := db.Exec(`UPDATE users set name = ?, password =?, status=? where id=?`, user.Name, user.Password, user.Status, user.ID)
	if err != nil {
		if _, err := io.WriteString(w, user.Name+" update failed"); err != nil {
			log.Println("[UpdateUser][db.Exec][w.Write]", err)
			return
		}
		log.Println("[UpdateUser][db.Exec]", err)
		return
	}
	rowsAffected, _ := result.RowsAffected()
	println("[UpdateUser][rowsAffected]", rowsAffected)
	if _, err := io.WriteString(w, "rowsAffected: "+strconv.FormatInt(rowsAffected, 10)); err != nil {
		log.Println("[UpdateUser][w.Write]", err)
		return
	}
}
func DeleteUser(w http.ResponseWriter, r *http.Request) {
	// 获取路由参数
	vars := mux.Vars(r)
	var id = vars["id"]
	result, err := db.Exec(`DELETE FROM users WHERE id = ?`, id)
	if err != nil {
		if _, err := io.WriteString(w, "delete failed"+id); err != nil {
			log.Println("[DeleteUser][db.Exec][w.Write]", err)
			return
		}
		log.Println("[DeleteUser][db.Exec]", err)
		return
	}
	rowsAffected, _ := result.RowsAffected()
	println("[DeleteUser][rowsAffected]", rowsAffected)
	if _, err := io.WriteString(w, "rowsAffected: "+strconv.FormatInt(rowsAffected, 10)); err != nil {
		log.Println("[DeleteUser][w.Write]", err)
		return
	}
}

func init() {
	var err error
	db, err = sql.Open("mysql", "root:toor@tcp(localhost:3306)/dev")
	if err != nil {
		log.Fatal(err)
	}
}

中间件

中间件是洋葱模型,链式调用的,一层一层进入然后一层一层返回。

简单中间件

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

复杂中间件

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

type Middleware func(http.HandlerFunc) http.HandlerFunc

// 日志记录所有请求及其路径和处理时间
func Logging() Middleware {

    // 创建一个新的中间件
    return func(f http.HandlerFunc) http.HandlerFunc {

        // 定义 http.HandlerFunc
        return func(w http.ResponseWriter, r *http.Request) {

            // Do middleware things
            start := time.Now()
            defer func() { log.Println(r.URL.Path, time.Since(start)) }()

            // 调用链中的下一个中间件/处理程序
            f(w, r)
        }
    }
}

// 确保只能使用特定的方法请求url,否则返回错误的请求
func Method(m string) Middleware {

    // 创建一个新的中间件
    return func(f http.HandlerFunc) http.HandlerFunc {

        // 定义 http.HandlerFunc
        return func(w http.ResponseWriter, r *http.Request) {

            // Do middleware things
            if r.Method != m {
                http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
                return
            }

            // 调用链中的下一个中间件/处理程序
            f(w, r)
        }
    }
}

// Chain 将中间件应用到 http.HandlerFunc
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
    for _, m := range middlewares {
        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("GET"), Logging()))
    http.ListenAndServe(":8080", nil)
}

用 Golang 渲染网页

简单渲染一个网页

启动一个 http 服务

package main

import (
	"html/template"
	"net/http"
)

type Todo struct {
	Title string
	Done  bool
}

type TodoPageData struct {
	PageTitle string
	Todos     []Todo
}

func main() {
    // template.ParseFiles 的路径是项目根目录算起,在另一篇文章中强调过这个事情,不管启动文件在什么路径,这里都从根目录算起。
	tmpl := template.Must(template.ParseFiles("./build/book.html"))
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		data := TodoPageData{
			PageTitle: "My TODO list",
			Todos: []Todo{
				{Title: "Task 1", Done: false},
				{Title: "Task 2", Done: true},
				{Title: "Task 3", Done: true},
			},
		}
		tmpl.Execute(w, data)
	})
	http.ListenAndServe(":80", nil)
}

创建一个模版文件

创建一个 build 文件夹,然后在 build 中创建一个 book.html,写入如下代码:

<h1>{{.PageTitle}}</h1>
<ul>
    {{range .Todos}}
        {{if .Done}}
            <li class="done">{{.Title}}</li>
        {{else}}
            <li>{{.Title}}</li>
        {{end}}
    {{end}}
</ul>

渲染一个表单并接收返回值

启动一个 http 服务

package main

import (
    "html/template"
    "net/http"
)

type ContactDetails struct {
    Email   string
    Subject string
    Message string
}

func main() {
    tmpl := template.Must(template.ParseFiles("./build/forms.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 如果当前请求不是 post 则渲染表单让用户填写
        if r.Method != http.MethodPost {
            tmpl.Execute(w, nil)
            return
        }

        // 从请求中获取表单的值
        details := ContactDetails{
            Email:   r.FormValue("email"),
            Subject: r.FormValue("subject"),
            Message: r.FormValue("message"),
        }

        // 用表单的值做一些处理,比如登陆
        _ = details

        // 如果当前请求是 post 则渲染提交成功页面
        tmpl.Execute(w, struct{ Success bool }{true})
    })

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

创建一个模版文件

创建一个 build 文件夹,然后在 build 中创建一个 forms.html,写入如下代码:

{{if .Success}}
    <h1>Thanks for your message!</h1>
{{else}}
    <h1>Contact</h1>
    <form method="POST">
        <label>Email:</label><br />
        <input type="text" name="email"><br />
        <label>Subject:</label><br />
        <input type="text" name="subject"><br />
        <label>Message:</label><br />
        <textarea name="message"></textarea><br />
        <input type="submit">
    </form>
{{end}}

总结

由于 go 还是用来做 api 多一些,渲染网页就到这里就行了。 从代码中可以看到,依然是注册一个 http.HandleFunc,和 api 不同的是,这里要渲染模版。

载入模版

tmpl := template.Must(template.ParseFiles(”./build/book.html”))

渲染模版

tmpl.Execute(w, data) 调用刚刚创建的模版的 Execute 方法,同时传入当前 HandleFunc 的 http.ResponseWriter 和要渲染的数据。

根据不同 Method 作出不同返回

if r.Method != http.MethodPost {
	tmpl.Execute(w, nil)
	return
}

获取 post 表单的值

details := ContactDetails{
    Email:   r.FormValue("email"),
    Subject: r.FormValue("subject"),
    Message: r.FormValue("message"),
}

用 Golang 分发文件

package main

import (
    "fmt"
    "net/http"
)

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

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

一些问题

ResponseWriter.Write 和 io.WriteString 的区别

ResponseWriter.Write 的参数是字节码,io.WriteString 的参数是 ResponseWriter 和 string,如果你要写出 string 使用 io.WriteString 可以少写字节码到 string 的转换。 io 包下的方法除了 io.WriteString 还有 io.Writer,这个用法更多,用到了可以详细看看。 详细去看 https://stackoverflow.com/questions/37863374/whats-the-difference-between-responsewriter-write-and-io-writestring

fmt.Printf 和 log.Printf 的区别

h := "world"
fmt.Printf("hello = %v\n", h)
log.Printf("halo = %v\n", h)

/*
输出结果:
hello = world
2016/12/30 09:13:12 halo = world
*/

从输出结果可以看出 log.Printf 是带有时间的,fmt.Printf 仅打印你要打印的东西。还涉及到线程安全什么的。 详细去看 https://stackoverflow.com/questions/41389933/when-to-use-log-over-fmt-for-debugging-and-printing-error

runtime.Caller() 和 debug.Stack() 的区别

debug.Stack() 是打印完整的调用堆栈,详细且多。runtime.Caller() 是可以控制只打印自己想要的内容的,比如行号或者文件名。 详细去看 https://www.orztu.com/post/golang-trace/