Golang 小项目(2)


前言

本项目适合 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。

正文

项目结构

.
└─src
    ├─go-basic-server
    ├─go-code
    ├─go-keword-scraper
    ├─go-movies-curd
    │      go.mod
    │      go.sum
    │      main.go
    │
    ├─go-rest
    ├─go-server
    └─go-todo

项目结构图

为降低难度,本项目未使用 DATEBASE ,仅使用本地 json 文件存储数据。

  • /movieGET 方法获取所有电影信息,调用 getMovies() 函数获取所有数据

  • /movies/idGET 方法获取指定电影信息, 调用 getMovie() 函数获取单条数据

  • /moviesPOST 方法新增电影信息, 调用 createMovie() 函数新增单条数据

  • /movies/idPUT 方法更新指定电影信息, 调用 updateMovie() 函数更新单条数据

  • /movies/idDELETE 方法删除指定电影信息, 调用 deleteMovie() 函数删除单条数据

最后运用 postman 测试接口,验证项目功能是否正常。

项目初始化

  1. 创建项目文件夹 go-curd02

  2. 在项目文件夹下创建 src 文件夹,并在 src 文件夹下创建 go-basic-servergo-codego-keword-scrapergo-movies-curdgo-restgo-servergo-todo 共六个文件夹

  3. go-movies-curd 文件夹下使用 cmd 命令下载 github.com/gorilla/mux 包,并在 go.mod 文件中添加依赖

// 依次使用以下命令初始化 mod 文件并下载依赖包
go mod init go-movies-curd
go get github.com/gorilla/mux@latest

// cmd 命令打开 VSCode
code .
  1. go-movies-curd 文件夹下创建 main.go 文件

项目实现

  1. 导入依赖包
package main

import (
	"fmt"      // 导入 fmt 包,用于格式化输入输出
	"log"      // 导入 log 包,用于日志记录和错误处理 -> 用于记录日志
	"encoding/json" // 导入 encoding/json 包,用于处理 JSON 编码和解码 -> 用于处理JSON数据(postman测试)
	"math/rand" // 导入 math/rand 包,用于生成随机数 -> 用于生成id
	"net/http" // 导入 net/http 包,用于创建 HTTP 服务器和处理 HTTP 请求 -> 用于创建HTTP服务器
	"strconv"  // 导入 strconv 包,用于字符串和其他基本数据类型的转换 -> 便于将id转化为string类型
	"github.com/gorilla/mux" // 导入 Gorilla Mux 包,用于处理 HTTP 路由 -> 用于处理API接口
)
  1. 定义数据结构体

定义电影数据结构体,用于存储电影信息

type Movie struct {
    ID string `json:"id"`
    Isbn string `json:"isbn"`
    Title string `json:"title"`
    Director *Director `json:"director"`
}

定义导演数据结构体,用于存储导演信息

type Director struct {
    Firstname string `json:"firstname"`
    Lastname string `json:"lastname"`
}

今日小知识: 结构体标签(struct tags)

  1. 定义:结构体标签是一个字符串,用反引号括起来,通常位于字段定义的后面。
  2. 主要用途:JSON 编码和解码、数据库字段映射、表单解析和 XML 编码和解码
    示例:
//结构体标签最常见的用途是与 JSON 编码和解码相关。通过指定 JSON 标签,可以控制 JSON 数据中的字段名称和行为。
type Person struct {
 Name  string `json:"name"`
 Age   int    `json:"age"`
 Email string `json:"email"`
}
// 当你使用 encoding/json 包对这个结构体进行编码时,json:"name" 标签指定了在 JSON 数据中应该使用 "name" 作为字段名。这使得 JSON 数据与 Go 结构体字段之间能够正确映射。
import (
 "encoding/json"
 "fmt"
)

func main() {
 p := Person{Name: "Alice", Age: 30, Email: "alice@example.com"}
 jsonData, _ := json.Marshal(p)
 fmt.Println(string(jsonData)) // 输出: {"name":"Alice","age":30,>>"email":"alice@example.com"}
}
// 结构体标签也可以用于数据库操作,通过标签指定字段名与数据库表中的列名之间的映射。
// 这里,gorm:"column:user_name" 标签指定了结构体字段 Name 对应数据库表中的 user_name 列。
type User struct {
 ID    int    `gorm:"primary_key"`
 Name  string `gorm:"column:user_name"`
 Email string `gorm:"column:user_email"`
}
// 在 Web 开发中,结构体标签可以用于表单解析,指定表单字段的名称与结构体字段之间的映射关系。例如,使用 gin 框架处理表单数据:
// 在这个例子中,form:"username" 标签表示表单字段 username 将被映射到结构体字段 Username。
type LoginForm struct {
 Username string `form:"username"`
 Password string `form:"password"`
}
// 除了 JSON,结构体标签也可以用于 XML 编码和解码。例如,使用 encoding/xml 包:
// 这里,xml:"title" 标签指定了 XML 数据中字段名为 title,这在 XML 编码和解码时起作用。
type Book struct {
 Title  string `xml:"title"`
 Author string `xml:"author"`
}
  1. 定义全局变量
var movies []Movie
  1. 定义函数

根据项目结构图创建相应函数

func main(){
    // 创建路由实例
    r := mux.NewRouter()

    // 创建用于测试的电影数据
    movies = append(movies, Movie{ID: "1",Isbn: "438227", Title: "Movie One", Director: &Director{Firstname: "John", Lastname: "Doe"}})
    movies = append(movies, Movie{ID: "2",Isbn: "45455", Title: "Movie Two", Director: &Director{Firstname: "Steve", Lastname: "Smith"}})

    // 定义路由以及视图函数
    r.HandleFunc("/movies", getMovies).Methods("GET")
    r.HandleFunc("/movies/{id}", getMovie).Methods("GET")
    r.HandleFunc("/movies", createMovie).Methods("POST")
    r.HandleFunc("/movies/{id}", updateMovie).Methods("PUT")
    r.HandleFunc("/movies/{id}", deleteMovie).Methods("DELETE")

    fmt.Printf("Starting server at port 8000\n")
    // 监听 8000 端口和路由器 r, 如果启动服务器过程中出现任何错误(例如端口已被占用或其他问题),log.Fatal 会记录错误信息并退出程序。
    log.Fatal(http.ListenAndServe(":8000",r))
}

*& 傻傻分不清?
在 Go 语言中,& 是一个运算符,它用于获取(或查找)变量的地址。你可以把这看作是“给变量取一个标记”的过程。实际上,& 就像在说“这个变量的房子在哪里”。
举个栗子:

var x int = 10
var p *int
p = &x

上面这段代码中,我们声明了一个名为 x 的整型变量,并且给它取了一个值为 10。然后我们声明了一个名为 p 的指针类型的整型变量(指针变量),你会注意到它的类型 *int 里面有星号。
这个星号 * 表示这个变量 p 是一个指针,它可以持有地址。然后我们使用 &x,这是什么意思呢?就是这个整型变量 x 的 “地址标记”,我们把这个“地址标记”赋给了 p
总结& 就是给变量取一个标记,也就是获取变量的地址;* 表示“我想要地址所指向位置的值”,用于解引用指针。
浅显一点就是,* 用于定义指针变量: 就像定义一个“指针容器”,它准备好来存放指向某种类型的地址。例如,_int 表示一个“整数指针容器”,它可以存放一个整数的地址。
& 用于确定地址: 当你有一个具体的变量,并且想知道它在内存中的地址时,你使用 &。这个地址将被存放到你之前定义好的“指针容器”里。
速记:_ 从地址取值,& 找地址。

  1. 定义路由函数
// 定义用于获取所有电影数据的函数
func getMovies(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(movies)
}

// 定义用于删除指定电影数据的函数
func deleteMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    // 从 HTTP 请求 r 中提取路径参数,并将它们以 map[string]string 的形式返回
    params := mux.Vars(r)
    // index 是当前元素的索引(即位置),它是一个整数值; item 是当前索引位置上的元素本身
    for index, item := range movies {

        if item.ID == params["id"] {
            // append(movies[:index], movies[index+1:]...) 实际上创建了一个新的切片,其中包含了删除指定元素后的所有元素。
            movies = append(movies[:index], movies[index+1:]...)
            // break 语句用于退出 for 循环
            break
        }
    }
    json.NewEncoder(w).Encode(movies)
}

func getMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type","application/json")
    params := mux.Vars(r)
    for _, item := range movies {

        if item.ID == params["id"] {
            json.NewEncoder(w).Encode(item)
            return
        }
    }
}

func createMovie(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    var movie Movie
    // Decode(&movie):传入 movie 的指针,以便解码器可以直接修改 movie 变量的内容。
    // _ 表示我们忽略 Decode 方法的返回值。如果解码过程发生错误,通常会记录错误信息而不是简单地忽略。
    _ = json.NewDecoder(r.Body).Decode(&movie)
    // rand.Intn(100000000):生成一个随机整数,范围是 0 到 99999999(不包括 100000000)。
    movie.ID = strconv.Itoa(rand.Intn(100000000))
    // 将 movie 添加到 movies 切片的末尾。append 函数会创建一个新的切片(如果原切片空间不足),并将原切片和新元素合并到一起。
    movies = append(movies, movie)
    json.NewEncoder(w).Encode(movie)
}

func updateMovie(w http.ResponseWriter, r *http.Request) {

    // 设定响应头 Content-Type 为 application/json
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)

    for index, item := range movies {
        if item.ID == params["id"] {
            // 删除原有电影
            movies = append(movies[:index], movies[index+1:]...)
            // 创建新电影
            var movie Movie
            // 创建一个 JSON 解码器,并从 HTTP 请求的主体中解码 JSON 数据到 `movie` 变量。
            _ = json.NewDecoder(r.Body).Decode(&movie)
            movie.ID = params["id"]
            movies = append(movies, movie)
            json.NewEncoder(w).Encode(movies)
            return
        }
    }
    
}

  1. 运行项目
go build main.go
go run main.go
  1. 测试接口

进入Postman测试接口,验证项目功能是否正常。

GET ALL 接口地址:http://localhost:8000/movies
GET BY ID 接口地址:http://localhost:8000/movies/1
CREATE 接口地址:http://localhost:8000/movies
UPDATE 接口地址:http://localhost:8000/movies/1
DELETE 接口地址:http://localhost:8000/movies/1

接口图例