操作数据库
- 1 插入数据
- 2 显示文章
- 2.1 修改 articlesShowHandler() 函数
- 2.2 代码解析
- 3 编辑文章
- 3.1 添加路由
- 3.2 编辑articlesEditHandler()
- 3.3 新建 edit 模板
- 3.4 代码重构
- 3.5 完善articlesUpdateHandler()
- 3.6 测试更新
- 3.7 封装表单验证
1 插入数据
.
.
.
func articlesStoreHandler(w http.ResponseWriter, r *http.Request) {
.
.
.
// 检查是否有错误
if len(errors) == 0 {
lastInsertID, err := saveArticleToDB(title, body)
if lastInsertID > 0 {
fmt.Fprint(w, "插入成功,ID 为"+strconv.FormatInt(lastInsertID, 10))
} else {
checkError(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 服务器内部错误")
}
} else {
.
.
.
}
}
func saveArticleToDB(title string, body string) (int64, error) {
// 变量初始化
var (
id int64
err error
rs sql.Result
stmt *sql.Stmt
)
// 1. 获取一个 prepare 声明语句
stmt, err = db.Prepare("INSERT INTO articles (title, body) VALUES(?,?)")
// 例行的错误检测
if err != nil {
return 0, err
}
// 2. 在此函数运行结束后关闭此语句,防止占用 SQL 连接
defer stmt.Close()
// 3. 执行请求,传参进入绑定的内容
rs, err = stmt.Exec(title, body)
if err != nil {
return 0, err
}
// 4. 插入成功的话,会返回自增 ID
if id, err = rs.LastInsertId(); id > 0 {
return id, nil
}
return 0, err
}
.
.
.
在Go语言中,
defer stmt.Close()
是一种常见的用法,用于在函数执行完毕后延迟关闭资源。具体来说,defer
关键字用于延迟执行一个函数调用,这个函数调用通常是用来释放资源或执行清理操作。在这个例子中,stmt.Close()
表示在当前函数执行完毕后,会调用stmt
对象的Close()
方法来关闭资源。
这种用法对于确保资源的正确释放非常有用,因为无论函数是通过正常返回还是发生异常终止,defer
语句都会被执行。这样可以避免资源泄漏和确保程序的稳定性。
fmt.Fprint(w, “插入成功,ID 为”+strconv.FormatInt(lastInsertID, 10))
具体来说,strconv.FormatInt函数的第一个参数是要转换的整数值,即lastInsertID,第二个参数是指定转换的进制。在这里,第二个参数是10,表示要将整数转换为10进制的字符串。
例如,如果lastInsertID的值为123,那么strconv.FormatInt(123, 10)将返回字符串"123"。
rs, err = stmt.Exec(title, body)
返回值是一个 sql.Result 对象rs,定义如下
type Result interface {
// 使用 INSERT 向数据插入记录,数据表有自增 ID 时,该函数有返回值
LastInsertId() (int64, error)
// 表示影响的数据表行数,常用于 UPDATE/DELETE 等 SQL 语句中
RowsAffected() (int64, error)
}
因为我们的 articles 表里有设置 id 字段为自增 ID,故在我们的代码中,使用 rs.LastInsertId() 来判断是否执行成功,成功的话就返回这条新创建数据的 ID:
// 4. 插入成功的话,会返回自增 ID
if id, err = rs.LastInsertId(); id > 0 {
return id, nil
}
访问localhost:3000/articles/create并填入测试数据
2 显示文章
显示文章分两个步骤:
(1)读取数据
(2)渲染模板
2.1 修改 articlesShowHandler() 函数
.
.
.
// Article 对应一条文章数据
type Article struct {
Title, Body string
ID int64
}
func articlesShowHandler(w http.ResponseWriter, r *http.Request) {
// 1. 获取 URL 参数
vars := mux.Vars(r)
id := vars["id"]
// 2. 读取对应的文章数据
article := Article{}
query := "SELECT * FROM articles WHERE id = ?"
err := db.QueryRow(query, id).Scan(&article.ID, &article.Title, &article.Body)
// 3. 如果出现错误
if err != nil {
if err == sql.ErrNoRows {
// 3.1 数据未找到
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "404 文章未找到")
} else {
// 3.2 数据库错误
checkError(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 服务器内部错误")
}
} else {
// 4. 读取成功
fmt.Fprint(w, "读取成功,文章标题 —— "+article.Title)
}
}
.
.
.
2.2 代码解析
QueryRow() 来读取单条数据
// 2. 读取对应的文章数据
article := Article{}
query := “SELECT * FROM articles WHERE id = ?”
err := db.QueryRow(query, id).Scan(&article.ID, &article.Title, &article.Body)
渲染模板
修改代码
.
.
.
func articlesShowHandler(w http.ResponseWriter, r *http.Request) {
.
.
.
// 3. 如果出现错误
if err != nil {
.
.
.
} else {
// 4. 读取成功,显示文章
tmpl, err := template.ParseFiles("resources/views/articles/show.gohtml")
checkError(err)
err = tmpl.Execute(w, article)
checkError(err)
}
}
.
.
.
模板文件resources/views/articles/show.gohtml
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ .Title }} —— 我的技术博客</title>
<style type="text/css">.error {color: red;}</style>
</head>
<body>
<p>ID: {{ .ID }}</p>
<p>标题: {{ .Title }}</p>
<p>内容:{{ .Body }}</p>
</body>
</html>
保存成功后访问 localhost:3000/articles/1
log.Fatal(err):这是一个标准库log包中的函数,用于输出错误信息并终止程序执行。它会将err作为参数输出到标准错误输出,并调用os.Exit(1)来终止程序执行。这样可以确保在遇到严重错误时,程序会立即停止运行。
3 编辑文章
3.1 添加路由
3.2 编辑articlesEditHandler()
.
.
.
func articlesEditHandler(w http.ResponseWriter, r *http.Request) {
// 1. 获取 URL 参数
vars := mux.Vars(r)
id := vars["id"]
// 2. 读取对应的文章数据
article := Article{}
query := "SELECT * FROM articles WHERE id = ?"
err := db.QueryRow(query, id).Scan(&article.ID, &article.Title, &article.Body)
// 3. 如果出现错误
if err != nil {
if err == sql.ErrNoRows {
// 3.1 数据未找到
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "404 文章未找到")
} else {
// 3.2 数据库错误
checkError(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 服务器内部错误")
}
} else {
// 4. 读取成功,显示表单
updateURL, _ := router.Get("articles.update").URL("id", id)
data := ArticlesFormData{
Title: article.Title,
Body: article.Body,
URL: updateURL,
Errors: nil,
}
tmpl, err := template.ParseFiles("resources/views/articles/edit.gohtml")
checkError(err)
err = tmpl.Execute(w, data)
checkError(err)
}
}
func articlesUpdateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "更新文章")
}
func articlesIndexHandler(w http.ResponseWriter, r *http.Request) {
.
.
.
3.3 新建 edit 模板
<!DOCTYPE html>
<html lang="en">
<head>
<title>编辑文章 —— 我的技术博客</title>
<style type="text/css">.error {color: red;}</style>
</head>
<body>
<form action="{{ .URL }}" method="post">
<p><input type="text" name="title" value="{{ .Title }}"></p>
{{ with .Errors.title }}
<p class="error">{{ . }}</p>
{{ end }}
<p><textarea name="body" cols="30" rows="10">{{ .Body }}</textarea></p>
{{ with .Errors.body }}
<p class="error">{{ . }}</p>
{{ end }}
<p><button type="submit">更新</button></p>
</form>
</body>
</html>
测试http://localhost:3000/articles/1/edit
3.4 代码重构
articlesEditHandler()和articlesShowHandler()中有重复的代码,将其提取出来封装成函数。这样做不仅可以少写代码,也可以提高代码的可维护性。
一般不需要封装会影响返回结果的逻辑处理,所以注释 3 中的错误处理与模板渲染部分我们保持不变。
重构articlesEditHandler()和articlesShowHandler () 函数
3.5 完善articlesUpdateHandler()
.
.
.
func articlesUpdateHandler(w http.ResponseWriter, r *http.Request) {
// 1. 获取 URL 参数
id := getRouteVariable("id", r)
// 2. 读取对应的文章数据
_, err := getArticleByID(id)
// 3. 如果出现错误
if err != nil {
if err == sql.ErrNoRows {
// 3.1 数据未找到
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "404 文章未找到")
} else {
// 3.2 数据库错误
checkError(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 服务器内部错误")
}
} else {
// 4. 未出现错误
// 4.1 表单验证
title := r.PostFormValue("title")
body := r.PostFormValue("body")
errors := make(map[string]string)
// 验证标题
if title == "" {
errors["title"] = "标题不能为空"
} else if utf8.RuneCountInString(title) < 3 || utf8.RuneCountInString(title) > 40 {
errors["title"] = "标题长度需介于 3-40"
}
// 验证内容
if body == "" {
errors["body"] = "内容不能为空"
} else if utf8.RuneCountInString(body) < 10 {
errors["body"] = "内容长度需大于或等于 10 个字节"
}
if len(errors) == 0 {
// 4.2 表单验证通过,更新数据
query := "UPDATE articles SET title = ?, body = ? WHERE id = ?"
rs, err := db.Exec(query, title, body, id)
if err != nil {
checkError(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 服务器内部错误")
}
// √ 更新成功,跳转到文章详情页
if n, _ := rs.RowsAffected(); n > 0 {
showURL, _ := router.Get("articles.show").URL("id", id)
http.Redirect(w, r, showURL.String(), http.StatusFound)
} else {
fmt.Fprint(w, "您没有做任何更改!")
}
} else {
// 4.3 表单验证不通过,显示理由
updateURL, _ := router.Get("articles.update").URL("id", id)
data := ArticlesFormData{
Title: title,
Body: body,
URL: updateURL,
Errors: errors,
}
tmpl, err := template.ParseFiles("resources/views/articles/edit.gohtml")
checkError(err)
err = tmpl.Execute(w, data)
checkError(err)
}
}
}
.
.
.
3.6 测试更新
访问 localhost:3000/articles/1/edit ,修改内容:
再次访问 localhost:3000/articles/1/edit ,将标题修改到只有两个字,点击更新:
3.7 封装表单验证
articlesUpdateHandler() 中的表单验证与 articlesStoreHandler() 使用同一套代码,将其抽出来作为单独的函数。
//封装表单验证
func validateArticleFormData(title string, body string) map[string]string {
errors := make(map[string]string)
//验证标题
if title == "" {
errors["title"] = "标题不能为空"
} else if utf8.RuneCountInString(title) < 3 || utf8.RuneCountInString(title) > 40 {
errors["title"] = "标题长度需介于 3-40"
}
//验证内容
if body == "" {
errors["body"] = "内容不能为空"
} else if utf8.RuneCountInString(body) < 10 {
errors["body"] = "内容长度需大于或等于10个字节"
}
return errors
}
validateArticleFormData() 函数应用到上文提到的两个函数中
测试 localhost:3000/articles/1/edit , 都正常。