
Go 语言里面提供了一个完善的 net/http 包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很便利地对 Web 的路由,静态文件,模版,cookie 等数据进行操作。
package main
import (
"fmt"
"log"
"net/http"
"strings"
)
func home(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // 解析参数,默认是不会解析的
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
fmt.Println("path : ", r.URL.Path)
fmt.Println("scheme : ", r.URL.Scheme)
fmt.Println(r.Form["url_long"])
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
fmt.Fprintf(w, "网站首页") //这个写入到w的是输出到客户端的
}
func main() {
http.HandleFunc("/", home) // 设置访问的路由 如首页
err := http.ListenAndServe(":800", nil) //设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
就是这么方便, 一个 web server 创建好了, 通过访问 : http://localhost:800/ 即可体验 ~
Request 用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息
Response 服务器需要反馈给客户端的信息
Conn 用户的每次请求链接
Handler 处理请求和生成返回信息的处理逻辑1 创建Listen Socket, 监听指定的端口, 等待客户端请求到来。
2 监听并接收客户端的请求, 接下来通过Client Socket与客户端通信。
3 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。
1. 通过 http.ListenAndServe(":800", nil) 监听请求
2. 通过 http.HandleFunc("/", home) 处理请求
3. 通过 http.ResponseWriter() 输出响应结果Request 用户请求的信息,用来解析用户的请求信息,包括post、get、cookie、url等信息
Response 服务器需要反馈给客户端的信息
Conn 用户的每次请求链接
Handler 处理请求和生成返回信息的处理逻辑1 创建Listen Socket, 监听指定的端口, 等待客户端请求到来。
2 监听并接收客户端的请求, 接下来通过Client Socket与客户端通信。
3 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。go 语言实现 web server 的关键点 :
1. 通过 http.ListenAndServe(":800", nil) 监听请求
2. 通过 http.HandleFunc("/", home) 处理请求
3. 通过 http.ResponseWriter() 输出响应结果go 语言中一个web 请求会对应创建一个 goroutine 来处理整个请求. Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。
ListenAndServe 是创建web 服务器的关键函数 :
第一个参数为 addr 及网络地址, 为空代表 当前主机 80 端口
第二个参数为 http.handler, 如果为 nil 代表使用 DefaultServeMuxhandler 为 http.Handler 接口的实现.
package main
import (
"net/http"
)
func main() {
server := http.Server{
Addr: ":8080",
Handler: nil,
}
server.ListenAndServe()
}Handler 是一个接口, 作为创建web 服务时的第二个参数, 如果为 nil 则为一个 DefaultSeveMax [ 多路复用器 作为路由 ] .

2种模式的路由注册
可以使用 http.HandleFunc 和 http.Handle 注册路由, 2个函数第一个参数都是路径,第2个有所不同 :
http.HandleFunc 的第2个参数为一个复合格式的函数;
http.Handle 的第2个参数为一个具体 handler 的指针地址;
package main
import (
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
}
type aboutHandler struct{}
func (ah *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("About"))
}
func main() {
ah := aboutHandler{}
server := http.Server{
Addr: ":88",
// 使用 defaultServerMux
Handler: nil,
}
http.HandleFunc("/", home)
http.Handle("/about", &ah)
server.ListenAndServe()
}func NotFoundHandler() Handler
返回一个 handler, 响应一个 404;
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home"))
})
http.Handle("/test", http.NotFoundHandler())
http.ListenAndServe(":80", nil)
}func Redirect(w ResponseWriter, r *Request, url string, code int)
http.Redirect 可以进行重定向 :
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home"))
})
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
})
http.ListenAndServe(":80", nil)
}func StripPrefix(prefix string, h Handler) Handler
StripPrefix将URL中的前缀中的prefix字符串删除,然后再交给后面的Handler处理,一般是http.FileServer()的返回值。
如果URL不是以prefix开始,或者prefix包含转移字符,最后结果都会返回404,因此要精确匹配URL和prefix;
http.StripPrefix 更像一个中间件 ~
func TimeoutHandler(h Handler dt time.Duration, msg string Handler
返回一个 handler,它用来在指定时间内运行传入的 handler。也相当于是一个修饰器
h,将要被修饰的 handler
dt,第一个 handler 允许的处理时间
msg,如果超时,那么就把 msg 返回给请求,表示响应时间过长示例
package main
import (
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home"))
})
http.Handle("/slow", http.TimeoutHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 3)
w.Write([]byte("slow"))
}), time.Second*2, "Timeout..."))
http.ListenAndServe(":80", nil)
}func FileServer(root FileSystem) Handler
http.FileServer 方法属于标准库 nettp,返回一个使用 FileSystem 接口 root 提供文件访问服务的 HTTP 处理器。可以方便的实现静态文件服务器。
使用时需要用到操作系统的文件系统,所以还需要委托给 :
type Dir string func (d Dir) Open(name string) (File, error)
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
println(r.URL.Path)
http.ServeFile(w, r, "html"+r.URL.Path)
})
http.ListenAndServe(":80", nil)
}package main
import (
"net/http"
)
func main() {
http.ListenAndServe(":80", http.FileServer(http.Dir("html")))
}在 go 语言中 net/http 包用于表述 http 的消息结构,其中 request 用于表述客户端发送的 HTTP 请求消息(请求数据)。
重要的字段 :
URL
Header Body
Form、PostForm、MultipartFormurl 数据格式为一个结构体 :
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Path 请求路径
println(r.URL.Path)
// RawQuery get 请求参数 ? 后面部分
println(r.URL.RawQuery)
w.Write([]byte("hi.."))
})
http.ListenAndServe(":80", nil)
}
header 数据格式为一个 map :
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.Header)
w.Write([]byte("hi.."))
})
http.ListenAndServe(":80", nil)
}[
Accept:[text/html,application/xhtml+xml,...]
Accept-Encoding:[gzip, deflate, br]
Accept-Language:[zh-CN,zh;q=0.9]
Cache-Control:[max-age=0]
Connection:[keep-alive]
Sec-Ch-Ua:[" Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102"]
Sec-Ch-Ua-Mobile:[?0]
Sec-Ch-Ua-Platform:["Windows"]
Sec-Fetch-Dest:[document]
Sec-Fetch-Mode:[navigate]
Sec-Fetch-Site:[none]
Sec-Fetch-User:[?1]
Upgrade-Insecure-Requests:[1]
User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64...]
]说明 : 可以使用 vscode REST Client 插件 模拟请求
POST http://localhost/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
{
"name" : "lesscode",
"age" : 19
}package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
length := r.ContentLength
body := make([]byte, length)
r.Body.Read(body)
fmt.Fprintln(w, string(body))
w.Write([]byte("hi.."))
})
http.ListenAndServe(":80", nil)
}
如 : http://www.***.com/?id=100&name=lesscode
r.URL.RawQuery 会提供实际查询的原始字符串
r.URL.Query(),会提供查询字符串对应的 map[string][]string
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Index ..."))
fmt.Fprintln(w, r.URL.Query())
})
http.ListenAndServe(":80", nil)
}golang 提供 Form、PostForm、MultipartForm 三种方式来获取表单及POST 数据
Form 获取的数据保护 POST 数据和 URL 数据
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Index ..."))
r.ParseForm()
fmt.Fprintln(w, r.Form)
})
http.ListenAndServe(":80", nil)
}POST http://localhost/?a=909 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
name=hhhh&age=9r.PostForm 只接收 POST 数据
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Index ..."))
r.ParseForm()
fmt.Fprintln(w, r.PostForm)
})
http.ListenAndServe(":80", nil)
}package main
import (
"io"
"log"
"net/http"
"os"
)
func uploadOne(w http.ResponseWriter, r *http.Request) {
//判断请求方式
if r.Method == "POST" {
//设置内存大小
r.ParseMultipartForm(32 << 20)
w.Write([]byte("ok."))
//获取上传的第一个文件
file, header, err := r.FormFile("file")
// 判断文件有效性
if err != nil {
w.Write([]byte("no..."))
} else {
defer file.Close()
//创建上传目录
os.Mkdir("./upload", os.ModePerm)
//创建上传文件
cur, err := os.Create("./upload/" + header.Filename)
if err != nil {
println(err.Error())
} else {
defer cur.Close()
//把上传文件数据拷贝到我们新建的文件
io.Copy(cur, file)
w.Write([]byte("ok"))
}
}
}
}
func uploadMore(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
//设置内存大小
r.ParseMultipartForm(32 << 20)
//获取上传的文件组
files := r.MultipartForm.File["file"]
len := len(files)
for i := 0; i < len; i++ {
//打开上传文件
file, err := files[i].Open()
if err != nil {
log.Fatal(err)
} else {
defer file.Close()
//创建上传目录
os.Mkdir("./upload", os.ModePerm)
//创建上传文件
cur, err := os.Create("./upload/" + files[i].Filename)
if err != nil {
log.Fatal(err)
} else {
defer cur.Close()
io.Copy(cur, file)
}
}
}
}
}
func main() {
http.HandleFunc("/uploadMore", uploadMore)
http.HandleFunc("/uploadOne", uploadOne)
err := http.ListenAndServe(":9090", nil)
if err != nil {
log.Fatal(err)
}
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<form
action="http://127.0.0.1:9090/uploadMore"
method="post"
enctype="multipart/form-data">
<div>
文件:<input type="file" name="file" value="">
</div>
<div>
文件:<input type="file" name="file" value="">
</div>
<input type="submit" value="提交">
<div>提交</div>
</form>
</body>
</html>前面的章节我们讲解了 go web server 相关的知识,我们已经可以对路由进行解析,然后匹配对应的句柄处理路由,实现网站页面的输出。在网站开发过程中静态文件处理也是一个必须的功能,您可以使用 http.Handle 或者 http.HandleFunc 通过静态文件或者静态文件夹解析函数来完成这个功能。
package main
import (
"log"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
html := `<html>
<body>
<img src="static/demo.png" />
<div>hello</div>
</body>
<html>`
w.Write([]byte(html))
}
func staticFunc(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./favicon.ico")
}
func main() {
// 首页路由
http.HandleFunc("/", home)
// 静态文件夹解析
// StripPrefix将URL中的前缀中的prefix字符串删除,
// 然后再交给后面的Handler处理,一般是http.FileServer()的返回值。
// 如果URL不是以prefix开始,或者prefix包含转移字符,最后结果都会返回404,因此要精确匹配URL和prefix
// http.FileServer 函数返回一个Handler,
// 这个 Handler 向h ttp request 提供文件系统内容,一般使用http.Dir(“yourFilePath”)
http.Handle(
"/static/",
http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))),
)
// 单独解析一个静态文件示例
// favicon.ico 文件
http.HandleFunc("/favicon.ico", staticFunc)
err := http.ListenAndServe(":800", nil) //设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}Web 模板就是预先设计好的 HTML 页面,它可以被模板引擎反复的使用,来产生 HTML 页面
go 语言的标准库提供了 text/template,html/template两个模板库,大多数 Go 的 Web 框架都使用这些库作为 默认的模板引擎。
go 主要使用的是 text/template,HTML 相关的部分使用了 html/template,是个混合体。
模板可以完全无逻辑,但又具有足够的嵌入特性,和大多数模板引擎一样,Go Web 的模板位于无逻辑和嵌入逻辑之间的某个地方。
模板必须是可读的文本格式,扩展名任意。对于 Web 应用通常就是 HTML。里面会内嵌一些命令(叫做 action)
text/template 是通用模板引擎,html/template 是HTML 模板引擎 action 位于双层花括号之间:{{.}}这里的,就是一个action,它可以命令模板引擎将其替换成一个值。
package main
import (
"net/http"
"text/template"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
template, _ := template.ParseFiles("./templates/index.html")
template.Execute(w, "hi ...")
})
http.ListenAndServe(":80", nil)
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<span style="color: red;">{{.}}</span>
</body>
</html>go 语言通过 parseFiles、parseGlob、parse 三个函数来解析模板 :
func ParseFiles(filenames ...string) (*Template, error)
功能 : 解析指定的一个或者多个模板,返回一个模板结构体,后续可被执行。
使用模式匹配来解析匹配的模板文件 :
package main
import (
"net/http"
"text/template"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
template, _ := template.ParseGlob("./templates/*.html")
template.Execute(w, "hi ...")
})
http.ListenAndServe(":80", nil)
}可以解析字符串模板,上面2个函数最终调用 parse()。
Must 函数可以包裹一个函数,返回到一个模板的指针 和 一个错误。如果错误不为 nil,那么就 panic。
参数是 ResponseWriter、数据,适用于 : 单模板
参数是 ResponseWriter、模板名称、数据,适用于 : 多模板
action 是嵌入 html 模板的命令,位于 {{}} 中间,例如 . 就是一个 action 代表了传入模板的数据。
语法 :
{{ if arg }}
逻辑
{{ end }}
{{ if arg }}
......
{{ else }}
......
{{ end }}
语法 :
{{ with "ok" }}
{{.}}
{{ end }}语法 :
{{template 模板名称 .}}利用 . 传递模板变量
main.go
package main
import (
"net/http"
"text/template"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmp, _ := template.ParseFiles("./tmpls/index.html", "./tmpls/header.html")
// 定义数据结构体
type Data struct {
Name string
List []string
}
data := Data{
Name: "lesscode",
List: []string{"a", "b", "c"},
}
// 传入数据
tmp.Execute(w, data)
})
http.ListenAndServe(":80", nil)
}index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go</title>
</head>
<body>
{{template "header.html" .}}
{{if .Name}}
<h2>hi.. {{.Name}}</h2>
{{end}}
{{ range .List }}
<div>- {{.}} -</div>
{{ end }}
<div>
{{ with "ok" }}
{{.}}
{{ end }}
</div>
</body>
</html>header.html
<div style="height:50px; background:red;">
header..{{.Name}}
</div>您可以在 go 内定义自定义函数并在模板中调用。
使用 template.FuncMap{ } 定义一个函数, 注意函数只能返回一个值,或者一个值 + 错误;然后通过 template.New() 将函数传递到模板调用。
package main
import (
"net/http"
"text/template"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 定义函数
say := template.FuncMap{"say": func(message string) string {
return "say : " + message
}}
t := template.New("index.html")
t.Funcs(say)
t, _ = t.ParseFiles("tmpls/index.html")
// 传入数据
err := t.Execute(w, "test..")
if err != nil {
println(err.Error())
}
})
http.ListenAndServe(":80", nil)
}<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div>hi...{{say .}}</div>
</body>
</html>管道就是按顺序连接在一起的一串参数和方法。会把管道的输出传递给下一个管道。
语法 : {{ p1 | p2 | p3 }}
<div>{{1.12356 | printf "%.2f"}}</div>在一般网站开发中,我们经常遇到站点的头部、底部或者左侧菜单是一样的,我们可以使用组合模板来实现这样的功能。
实现组合模板的步骤
{{define "layout"}}
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<h1>公共头部</h1>
{{template "body" .}}
<h1>公共底部</h1>
</body>
</html>
{{end}}如 home.html
{{ define "body" }}
<h2>{{.}}</h2>
{{ end }}和 about.html
{{ define "body" }}
<h2>{{.}}</h2>
<p>关于 ......</p>
{{ end }}
package main
import (
"html/template"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("./tmpls/layout.html", "./tmpls/home.html")
if err != nil {
println(err.Error())
return
}
t.ExecuteTemplate(w, "layout", "首页")
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("./tmpls/layout.html", "./tmpls/about.html")
if err != nil {
println(err.Error())
return
}
t.ExecuteTemplate(w, "layout", "关于我们")
})
http.ListenAndServe(":80", nil)
}html 语法:
{{range $key 或 索引, $遍历元素}}
HTML 循环元素
{{end}}go 示例
package main
import (
"net/http"
"text/template"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
template, _ := template.ParseFiles("./templates/index.html")
// 注册多个数据
var data = map[string]any{
"name": "lesscode.work",
"persons": []string{
"小张", "小王", "小李",
},
}
template.Execute(w, data)
})
http.ListenAndServe(":80", nil)
}
html 模板文件示例
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
{{range $idx, $item := .persons}}
<li>{{$idx}} : {{$item}}</li>
{{end}}
</ul>
</body>
</html>在上面的示例中我们注册了 2个变量 : name 和 persons ,然后我们在 HTML 模板中对 persons 变量进行遍历,在遍历过程中如果我们需要直接使用 name 变量是无法做到的,如 :
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
{{range $idx, $item := .persons}}
<li>{{$idx}} : {{$item}} - {{.name}}</li>
{{end}}
</ul>
</body>
</html>原因是 : . ( 英文点 ) 的作用域在遍历过程中被重置,如何解决呢?
定义一个模板变量可以有效解决上面的变量作用域问题,模板变量定于语法 :
{{$变量名称 := .原变量名称}}示例 :
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<ul>
{{$name := .name}}
{{range $idx, $item := .persons}}
<li>{{$idx}} : {{$item}} - {{$name}}</li>
{{end}}
</ul>
</body>
</html>Gin 是 Go 语言写的一个 web 框架,它具有运行速度快,分组的路由器,良好的崩溃捕获和错误处理,非常好的支持中间件和 json。一款值得好好学习并应用的 Web 框架。
官网 : https://gin-gonic.com/zh-cn/docs/
// 初始化项目 mod
go mod init gotest
// 初始化依赖
go mod tidy
// 安装 Gin
go get -u github.com/gin-gonic/gin在 main.go 文件编写如下代码 :
package main
import "github.com/gin-gonic/gin"
func main() {
// 初始化 Gin 引擎
r := gin.Default()
// 定义路由
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello",
})
})
// 启动 web 服务
r.Run(":80")
}
在 main.go 文件编写如下代码 :
package main
import "github.com/gin-gonic/gin"
func main() {
// 初始化 Gin 引擎
r := gin.Default()
// 定义路由
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello",
})
})
// 启动 web 服务
r.Run(":80")
}
运行 main.go
go run ./main.go打开浏览器访问 localhost 即可看到 web 访问已经启动。
gin 内置提供了三种运行模式 :
const (
// DebugMode indicates gin mode is debug.
DebugMode = "debug"
// ReleaseMode indicates gin mode is release.
ReleaseMode = "release"
// TestMode indicates gin mode is test.
TestMode = "test"
)我们可以在不同阶段设置不同的模式,如开发时使用 DebugMode、上线后使用 ReleaseMode。
开发时我们建议使用 r := gin.Default() 初始化 gin,会在终端内输出详细的运行信息。当项目准备上线时我们推荐使用 release 模式 :
package main
import "github.com/gin-gonic/gin"
func main() {
// 初始化 Gin 引擎
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// 定义路由
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello",
})
})
// 启动 web 服务
r.Run(":80")
}初始化 gin 引擎后获得 gin 引擎对象,通过引擎对象的 GET、POST、PUT、DELETE、Any 方法来定义路由
package main
import "github.com/gin-gonic/gin"
func main() {
// 初始化 Gin 引擎
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// 定义首页路由
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello",
})
})
// 定义关于其他路由
r.GET("/News", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("新闻"))
})
// 启动 web 服务
r.Run(":80")
}Any 代表路由可以接受 GET、POST、PUT、DELETE 任何模式,可以使用 ctx.Request.Method 获取请求类型 :
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
// 初始化 Gin 引擎
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// 定义首页路由
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello",
})
})
// 定义关于其他路由
r.Any("/Any", func(ctx *gin.Context) {
requestMethod := ctx.Request.Method
fmt.Printf("requestMethod: %v\n", requestMethod)
})
// 启动 web 服务
r.Run(":80")
}可以通过 r.Group() 函数来实现路由分组,例如将路由划分为网站前端和后端 :
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 初始化 Gin 引擎
r := gin.Default()
// 网站前台分组
frontGroup := r.Group("/")
frontGroup.GET("/", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("首页"))
})
// 网站后台分组
AdminGroup := r.Group("/Admin")
AdminGroup.GET("/", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("后台首页"))
})
r.Run(":80")
}访问 localhost 匹配网站前端路由;
访问 localhost/Admin 匹配网站后台路由;
可以将路由抽离为一个或者多个文件,使您的项目结构更加合理,下面我们样式将上面的路由抽离为前端、后端2个路由文件:
1. 创建文件 /router 文件夹
2. 创建 /router/front.go 文件,编写如下代码 :
package router
import "github.com/gin-gonic/gin"
// 网站前台分组
func Front(r *gin.Engine) {
frontGroup := r.Group("/")
frontGroup.GET("/", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("首页"))
})
}3. 创建 /router/admin.go 文件,编写如下代码 :
package router
import "github.com/gin-gonic/gin"
// 网站前台分组
func Admin(r *gin.Engine) {
// 网站后台分组
AdminGroup := r.Group("/Admin")
AdminGroup.GET("/", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("后台首页"))
})
}4. 在 main.go 中应用路由
import (
"gotest/router"
"github.com/gin-gonic/gin"
)
func main() {
// 初始化 Gin 引擎
r := gin.Default()
router.Front(r)
router.Admin(r)
r.Run(":80")
}
5. 运行,体验效果,至此路由文件抽离完毕。
Gin 路由参数定义格式 :
r.GET("/:name/:age", func...)实际访问路由格式 :
http://localhost/test/10在函数中可以通过 : ctx.Param(key) 函数获取字符串形式的参数值。
// 网站前台分组
func Front(r *gin.Engine) {
frontGroup := r.Group("/")
frontGroup.GET("/:action", func(ctx *gin.Context) {
action := ctx.Param("action")
fmt.Printf("action: %v\n", action)
ctx.Writer.Write([]byte("首页"))
})
}路由中间件可以方便地实现权限检查、行为追踪等功能。通过 r.use() 函数可以方便地实现路由中间件。
如 在路由分组中实现中间件 :
package router
import (
"fmt"
"github.com/gin-gonic/gin"
)
// 定义中间件函数
func frontMiddleWare(ctx *gin.Context) {
println("我是前台路由中间件")
fmt.Printf("请求路径: %v\n", ctx.Request.URL)
}
// 网站前台分组
func Front(r *gin.Engine) {
frontGroup := r.Group("/")
frontGroup.Use(frontMiddleWare)
frontGroup.GET("/", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("首页"))
})
}以上代码演示了路由中间件的应用,您也可以将中间件抽离为独立的 go 文件,然后在其函数内实现中间件细节功能。
在某些情况下,如会员登陆检查失败,需要从当前路由调整到登陆页面,可以使用 ctx.Redirect() 函数进行重定向。
当某些条件不满足或者需要跳出当前路由时,需要使用 ctx.Abort() 函数终止路由继续运行。
// 跳转到 Login
ctx.Redirect(302, "/Login")
// 跳出当前路由
ctx.Abort()Query 用于接收 GET 数据, 而 postFrom 用于接收 POST 数据。
url /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
POST数据 : name=manu&message=this_is_greatfunc main() {
router := gin.Default()
router.POST("/post", func(c *gin.Context) {
id := c.Query("id")
page := c.DefaultQuery("page", "0")
name := c.PostForm("name")
message := c.PostForm("message")
fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
})
router.Run(":8080")
}
// id: 1234; page: 1; name: manu; message: this_is_great要将请求体绑定到结构体中,使用模型绑定。 Gin目前支持JSON、XML、YAML和标准表单值的绑定(foo=bar&boo=baz)。
Gin使用 go-playground/validator/v10 进行验证。 查看标签用法的全部文档。
使用时,需要在要绑定的所有字段上,设置相应的tag。 例如,使用 JSON 绑定时,设置字段标签为 json:"fieldname"。
Must bindMethods - Bind, BindJSON, BindXML, BindQuery, BindYAML
这些方法属于 MustBindWith 的具体调用。 如果发生绑定错误,则请求终止,并触发 c.AbortWithError(400, err).SetType(ErrorTypeBind)。
Should bindMethods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
这些方法属于 ShouldBindWith 的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。
使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定。 如果你明确知道要绑定什么,可以使用 MustBindWith 或 ShouldBindWith。
你也可以指定必须绑定的字段。 如果一个字段的 tag 加上了 binding:"required",但绑定时是空值, Gin 会报错。
手册地址 : https://pkg.go.dev/gopkg.in/go-playground/validator.v10
1. 安装 validator 包
go get github.com/go-playground/validator/v102. 在代码中引入工具包并使用
2.1 前端数据提交模板代码
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<form action="/" method="post">
<table width="500">
<tr>
<td>姓名</td>
<td><input type="text" name="Name" /></td>
</tr>
<tr>
<td>年龄</td>
<td><input type="text" name="Age" /></td>
</tr>
<tr>
<td>手机号</td>
<td><input type="text" name="Phone" /></td>
</tr>
<tr>
<td>邮箱</td>
<td><input type="text" name="Email" /></td>
</tr>
<tr>
<td></td>
<td>
<button type="submit">提交</button>
</td>
</tr>
</table>
</form>
</body>
</html>2.2 后端数据验证演示代码
package main
import (
"fmt"
"html/template"
"regexp"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// 创建一个结构体用于接收及验证POST数据
type Person struct {
Name string `validate:"min=1,max=10"`
Age int `validate:"lte=100,gte=10"`
Phone string `validate:"PhoneNumberCheck"`
Email string `validate:"email"`
}
// 自定义验证函数 演示
func PhoneNumberCheck(fl validator.FieldLevel) bool {
checkData := fl.Field().String()
reg := regexp.MustCompile(`^1[0-9]{10,10}$`)
return reg.MatchString(checkData)
}
func main() {
// 初始化 Gin 引擎
r := gin.Default()
// 提交页面
r.GET("/", func(ctx *gin.Context) {
template, err := template.ParseFiles("./templates/index.html")
if err != nil {
fmt.Printf("err: %v\n", err)
}
template.Execute(ctx.Writer, nil)
})
// 提交接收路由
r.POST("/", func(ctx *gin.Context) {
formData := &Person{}
// 表单数据映射
ctx.Bind(formData)
fmt.Printf("formData: %v\n", formData)
// 表单数据校验
validate := validator.New()
// 添加自定义规则
validate.RegisterValidation("PhoneNumberCheck", PhoneNumberCheck)
err := validate.Struct(formData)
if err != nil {
validationErrs := err.(validator.ValidationErrors)
fmt.Printf("validationErrs: %v\n", validationErrs)
fieldName := validationErrs[0].Field()
ctx.JSON(200, gin.H{"errcode": 300001, "data": "字段 " + fieldName + " 数据错误"})
return
}
})
r.Run(":80")
}1. 上面代码演示了使用 ctx.Bind() 函数将表单数据映射为结构体;
2. 使用 validate.New() 函数初始化验证对象;
3. 使用 validate.Struct() 来验证结构体;
4. 使用 validate.RegisterValidation() 来添加自定义验证规则,函数定义请参考 :
func PhoneNumberCheck(fl validator.FieldLevel) bool {
checkData := fl.Field().String()
reg := regexp.MustCompile(`^1[0-9]{10,10}$`)
return reg.MatchString(checkData)
}5. 验证失败后返回错误信息切片,可以从错误数据中获取到字段名称及错误信息。
更多规则请查阅官方手册,手册地址 :
https://pkg.go.dev/github.com/go-playground/validator/v10
在 Gin 中您可以通过 text/template 和 html/template 直接实现 html 模板渲染。关于模板引擎的知识请查阅本课程上一个章节。
package main
import (
"fmt"
"html/template"
"github.com/gin-gonic/gin"
)
func main() {
// 初始化 Gin 引擎
r := gin.Default()
r.GET("/", func(ctx *gin.Context) {
template, err := template.ParseFiles("./templates/index.html")
if err != nil {
fmt.Printf("err: %v\n", err)
}
template.Execute(ctx.Writer, gin.H{"name": "lesscode.work"})
})
r.Run(":80")
}可以通过 gin 提供的 html 渲染函数来实现模板渲染 :
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
router.Run(":8080")
}templates/index.tmpl
<html>
<h1>
{{ .title }}
</h1>
</html>func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "Posts",
})
})
router.GET("/users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"title": "Users",
})
})
router.Run(":8080")
}templates/posts/index.tmpl
{{ define "posts/index.tmpl" }}
<html>
<h1>
{{ .title }}
</h1>
<p>Using posts/index.tmpl</p>
</html>
{{ end }}templates/users/index.tmpl
{{ define "users/index.tmpl" }}
<html>
<h1>
{{ .title }}
</h1>
<p>Using users/index.tmpl</p>
</html>
{{ end }}你可以使用自定义的 html 模板渲染
import "html/template"
func main() {
router := gin.Default()
html := template.Must(template.ParseFiles("file1", "file2"))
router.SetHTMLTemplate(html)
router.Run(":8080")
}你可以使用自定义分隔
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates")import (
"fmt"
"html/template"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func formatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}
func main() {
router := gin.Default()
router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
router.Run(":8080")
}直接打包获得对应平台执行文件即可
go build ./main.goCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./main ./main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ./main ./main.go$ENV:CGO_ENABLED=0
$ENV:GOOS="linux"
$ENV:GOARCH="amd64"
go build -o ./main ./main.goset CGO_ENABLED=0
set GOOS=linux
set GOARCH=amd64
go build -o ./main ./main.go1 在 /etc/systemd/system/ 下创建服务, 如 goadmin.service
2 打开 goadmin.service, 编写如下命令 :
[Unit]
Description=Go Admin
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# 程序执行的目录
WorkingDirectory=/webs/***/
# 启动的脚本命令
ExecStart=/webs/***/main
# 重启条件
Restart=always
# 几秒后重启
RestartSec=5
[Install]
WantedBy=multi-user.targetsystemctl enable goadmin.servicesystemctl start goadmin.servicesystemctl status goadmin.servicesystemctl daemon-reload可以使用 systemctl status 命令查看状态及内存占用情况,命令 :
systemctl status ***.service输出
***.service - ***
Loaded: loaded (/etc/systemd/system/***.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2023-03-29 10:29:23 CST; 15min ago
Main PID: 367** (main)
Tasks: 5 (limit: 22696)
Memory: 178.6M
CGroup: /system.slice/***.service
└─367*** /***/main通过 top 命令查看 ( -p 后的进程 ID 通过上面的 status 命令获取 ) :
top -p 367*** 输出 :
3673281 root 20 0 933084 186940 13980 S 0.0 5.1 0:02.47 main1 top 命令执行后,按下 shift + m 可以实现按照内存使用量排序;
2 top 命令执行后,按下 shift + p 可以实现按照cpu使用量排序;当我们想在一个服务器上部署多个 go 站点时,每个go项目运行都需要独立的端口,但80端口只有一个,您可以通过 nginx 或者 apache 服务器的反向代理功能实现在一个服务器上部署多个 go 站点。还可以利用 nginx 反向代理实现负载均衡。
Nginx(“engine x”)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强。
官网 : https://nginx.org/
下载地址 https://nginx.org/en/download.html
选择对应的 Nginx 版本下载并安装。
模拟场景 :
go web server 项目运行端口 11022nginx 反向代理配置 :
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://127.0.0.1:11022;
root html;
index index.html index.htm;
}
# ......
}配置多个 server {} 对应不同的 server_name 及 http://127.0.0.1:不同端口,即可实现基于 nginx 的反向代理功能,从而实现在一个服务器上部署多个 go 站点。
nginx 启动命令
start nginx.exe // 启动 nginx
nginx.exe -s stop //停止 nginx
nginx.exe -s reload //重新加载 nginx
nginx.exe -s quit //退出 nginx<VirtualHost *:80>
ServerAdmin webmaster@example.com
DocumentRoot "/******"
ServerName localhost
ServerAlias localhost
<IfModule mod_proxy.c>
ProxyRequests Off
SSLProxyEngine on
ProxyPass / http://127.0.0.1:11022/
ProxyPassReverse / http://127.0.0.1:11022/
</IfModule>
#PROXY-END
#......
</VirtualHost>