确定需求和目标
需求
新用户注册时提供用户名,密码即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token
请求参数
| 参数 | 类型 | 备注 |
|---|---|---|
| username | string | 注册用户名,最长32个字符 |
| password | string | 密码,最长32个字符 |
返回响应
| 参数 | 类型 | 备注 |
|---|---|---|
| status_code | integer | 状态码,0-成功,其他值-失败 |
| status_msg | string | 返回状态描述 |
| user_id | integer | 用户id |
| token | string | 用户鉴权token |
设计数据库表结构
创建 user 表
定义表的字段和属性id、username 、 password和token
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name_password_index` (username,`password`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表\n';
编写接口代码
-
根据要求,先写好请求和响应两个结构体
type RegisterRequest struct { Username string `json:"username"` Password string `json:"password"` } type RegisterResponse struct { StatusCode int `json:"status_code"` StatusMsg string `json:"status_msg"` UserID int64 `json:"user_id"` Token string `json:"token"` } -
建立数据库连接
- 首先,需要安装
database/sql和mysql驱动包。可以通过以下命令来安装:
go get -u database/sql go get -u github.com/go-sql-driver/mysql- 接下来在代码中创建数据库连接:
dsn := "username:password@tcp(hostname:port)/databasename" db, err := sql.Open("mysql", dsn) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } defer db.Close() // 检查是否连接成功 err = db.Ping() if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return }sql.Open()函数用于打开数据库连接,第一个参数是数据库类型"mysql",第二个参数是连接数据源的字符串。注意要使用defer db.Close()来确保在代码执行完毕后关闭数据库连接。db.Ping()可以用来检验数据库是否连接成功, 避免因为数据库连接失败造成的问题 - 首先,需要安装
-
处理请求数据
-
读取请求数据, 核心是
ioutil.ReadAll()函数body, err := ioutil.ReadAll(request.Body) if err != nil { http.Error(writer, err.Error(), http.StatusBadRequest) return }首先
ioutil.ReadAll()函数接受一个实现了io.Reader接口的参数request.Body,并将其中的数据全部读取到字节切片中然后将读取到的结果赋值给了变量
body, 以便进行后续处理,例如解析 JSON 或表单数据为了检查是否读取主体数据时发生了错误, 可以将错误信息作为 HTTP 响应的内容返回给客户端,并设置响应的状态码为
http.StatusBadRequest(400), 并用return结束函数 -
解析请求数据, 核心是
json.Unmarshal()函数var requestData RegisterRequest err = json.Unmarshal(body, &requestData) if err != nil { http.Error(writer, err.Error(), http.StatusBadRequest) return }首先我们定义一个名为
requestData的变量,其类型为RegisterRequest, 用于存储从请求主体解析出的数据然后调用
json.Unmarshal()函数,将请求主体中的 JSON 数据解析到requestData变量中。body是包含请求主体数据的字节切片,&requestData是对requestData变量的指针,这样可以将解析后的数据直接存储到requestData中如果解析过程中出现错误,则表示请求数据的格式不符合预期或无法解析为
RegisterRequest结构体, 然后将错误信息作为 HTTP 响应的内容返回给客户端,并设置响应的状态码为http.StatusBadRequest(400), 并用return结束函数
-
-
查询数据库检查用户名是否已存在
query := ` SELECT COUNT(*) FROM user WHERE username = ? ` var count int err = db.QueryRow(query, requestData.Username).Scan(&count) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } if count > 0 { http.Error(writer, "用户名已存在", http.StatusBadRequest) return }首先调用
QueryRow()方法来查询表user中与给定用户名匹配的行的数量, 并将结果存储到count变量中。Scan(&count)方法将查询结果扫描并赋值给count变量。如果出现查询错误,将错误信息作为 HTTP 响应的内容返回给客户端,并设置响应的状态码为
http.StatusInternalServerError(500), 并用return结束函数然后检查变量
count的值是否大于 0,即数据库中是否已存在与给定用户名匹配的记录。如果存在匹配的记录,返回一个 HTTP 响应给客户端,内容为 "用户名已存在",状态码为http.StatusBadRequest(400)。 -
将注册信息插入数据库表
token := generateToken() insertQuery := ` INSERT INTO user (username, password, token) VALUES (?, ?, ?) ` result, err := db.Exec(insertQuery, requestData.Username, requestData.Password, token) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return }首先调用
generateToken()的函数,生成令牌值, 然后 调用Exec()方法来执行 SQL 插入操作, 将用户名、密码和令牌插入到名为user的数据库表中然后检查插入操作是否出现错误。如果有错误发生,表示插入操作无法正常执行。如果出现插入错误,将错误信息作为 HTTP 响应的内容返回给客户端,并设置响应的状态码为
http.StatusInternalServerError(500) -
获取刚插入记录的主键id的值
userID, err := result.LastInsertId() if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return }调用
Result对象的LastInsertId()方法,以获取刚插入记录的自增主键的值并作为id。如果出现错误,将错误信息作为 HTTP 响应的内容返回给客户端,并设置响应的状态码为
http.StatusInternalServerError(500) -
处理响应数据
-
构建响应数据
responseData := RegisterResponse{ StatusCode: http.StatusOK, StatusMsg: "注册成功!", UserID: userID, Token: token, }定义了一个名为
responseData的变量,其类型为RegisterResponse, 将相应的值赋予这些字段 -
将响应数据转换为 JSON 格式
responseJSON, err := json.Marshal(responseData) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return }使用
json.Marshal()函数将responseData转换为 JSON 格式的字节切片。如果转换过程中出现错误,我们会返回一个 HTTP 错误响应。 -
设置响应头部
writer.Header().Set("Content-Type", "application/json")设置响应头部的
"Content-Type"为"application/json",表明响应数据的类型为 JSON -
发送响应数据
writer.WriteHeader(http.StatusOK) writer.Write(responseJSON)通过调用
writer.WriteHeader(http.StatusOK)设置响应的状态码为http.StatusOK(200 OK)。最后,我们使用
writer.Write()方法发送 JSON 格式的响应数据。
-
测试和调试
使用单元测试发送模拟请求
func TestRegisterHandler(t *testing.T) {
// 创建一个用于测试的临时数据库
db, err := sql.Open("mysql", "root:123456@tcp(localhost:3306)/tiktok")
if err != nil {
t.Errorf("Failed to connect to the database: %v", err)
}
defer db.Close()
// 检查数据库连接状态
if err := db.Ping(); err != nil {
t.Errorf("Failed to ping the database: %v", err)
}
// 创建一个模拟的 HTTP 请求和响应
reqBody := RegisterRequest{
Username: "test_username",
Password: "test_password",
}
jsonData, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/douyin/user/register/?username=your_username&password=your_password", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
// 调用被测试的处理函数
RegisterHandler(recorder, req)
// 检查响应状态码
if recorder.Code != http.StatusOK {
t.Errorf("Expected status code %d, but got %d", http.StatusOK, recorder.Code)
}
// 从响应中读取内容
body, _ := ioutil.ReadAll(recorder.Body)
// 解析响应数据
var resp RegisterResponse
if err := json.Unmarshal(body, &resp); err != nil {
t.Errorf("Failed to parse response body: %v", err)
}
// 检查响应内容
if resp.UserID == 0 {
t.Error("User ID is not set in the response")
}
if resp.Token == "" {
t.Error("Token is not set in the response")
}
}
开发过程中的问题和解决方案
通过运行以上测试, 得到了以下问题:
sql: unknown driver "mysql" (forgotten import?)表示没有正确导入 MySQL 驱动程序。搜索后应该在代码开头添加_ "github.com/go-sql-driver/mysql"来导入该驱动程序,但是仍然出错。由于注意到这是个代码仓库,因此我将这个仓库下载下来,并放入了config包中,修改路径后成功解决问题。Failed to parse response body: invalid character 's' looking for beginning of value表示解析响应体时遇到了无效的字符。这是因为问题1的存在导致的,解决后成功修复这个问题。User ID is not set in the response和Token is not set in the response表示响应数据中的用户ID和令牌没有正确设置。这同样是因为问题1的存在导致的,解决后成功修复这个问题。
修修补补
后面写用户登录接口时注意到,接收请求和进行响应的代码是可以重复利用的,于是重构了HandleRequest和HandleResponse函数,如下:
userRequest.go
package controller
import (
"encoding/json"
"io/ioutil"
"net/http"
)
type Request struct {
Username string `json:"username"`
Password string `json:"password"`
}
func HandleRequest(writer http.ResponseWriter, request *http.Request) Request {
var requestData Request
// 检查请求方法是否为POST
if request.Method != http.MethodPost {
http.Error(writer, "Method Not Allowed", http.StatusMethodNotAllowed)
return requestData
}
// 读取请求体数据
requestBody, err := ioutil.ReadAll(request.Body)
defer request.Body.Close()
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return requestData
}
// 解析JSON数据
err = json.Unmarshal(requestBody, &requestData)
if err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
return requestData
}
return requestData
}
userResponse.go
package controller
import (
"encoding/json"
"net/http"
)
type Response struct {
StatusCode int `json:"status_code"`
StatusMsg string `json:"status_msg,omitempty"`
UserID int64 `json:"user_id,omitempty"`
Token string `json:"token,omitempty"`
}
func HandleResponse(userID int64, token string, writer http.ResponseWriter, StatusMsg string) {
// 构造响应数据
responseData := Response{
StatusCode: http.StatusOK,
StatusMsg: StatusMsg,
UserID: userID,
Token: token,
}
// 将响应转换为JSON并写入响应体中
responseJSON, err := json.Marshal(responseData)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
// 设置响应头部
writer.Header().Set("Content-Type", "application/json")
// 发送响应数据
writer.WriteHeader(http.StatusOK)
writer.Write(responseJSON)
}
这样,可以减少冗余,提高代码的可维护性和可读性,如果组员的其它请求同样适用,可以直接使用而无需重复造轮子。