[12-21]

截至目前为止,前端是基于react+ts+tailwind的架构。


开个坑,写个项目。

Leaning doc 📝

初始化


首先我们创建初始化项目
go mod init hello

这里的hello可以替换成任何喜欢的模块名的。
然后创建main.go
package main
</br>
func main() {
</br>
}

一般来说不要先想着导入库,goland会自动优化掉的。同理,如果用到了外部库,编译器会第一时间把包导入。




这时第一时间想到
fmt.Println("Hello, World!")

但是会发现输入print的话,会跳出很多函数,引起了我的好奇心。
我列出几个看到第一眼就感觉比较常见的。


print("Hello, World!")

内置的打印函数
println("Hello, World!")

打印后换行
fmt.Println("Hello, World!")

属于fmt包,最常用,因为可以处理多种类型的参数,功能比较强大。
fmt.Printf("%s", "Hello, World!")

格式化输出,类似于C语言中的打印函数
log.Printf("Hello, World!")

属于log包,打印的同时,前面会带上时间戳
pretty.Print("Hello, World!")

美化输出对象,尤其对于复杂数据结构如结构体或切片。


接下来,我们需要解决todos的逻辑,因此创建funcs文件夹,在里面创建todos.go文件。

设计crud中的“c“


首先我们可以先尝试做一个“增”功能。


设计数据结构,
type TODO struct {
Id int `json:"id"`
Content string `json:"content"`
Done bool `json:"done"`
}

这里一个简单的todolist,我们需要三个成员:
1.id。用于标识每一条消息是独一无二的。
2.content。内容,一般是一串文字,所以是字符串。
3.done。标识是否完成,那就是二元的状态,因此用bool类型。当然,也可以用int的,看心情。


每一个成员有一个标签,用于JSON包的便捷操作。
注意,结构体的名字是要大写的,在这里四个字母都大写,所以表现不明显。而类型名在go中都是后置的,在这里的表现为struct后置。
结构体里成员的名字并没有规则要求。但是此处,我们后边用JSON包处理它,因此我们要大写公开。

gin框架的基本使用


接下来创建一个函数
func TodoService(r *gin.RouterGroup, db *sql.DB) {
</br>
}

函数名的话开头大写,这样代表public,而其他方法如添加todo的函数,我们开头小写,代表private,然后通过这个TodoService进行调用,对外开放。
为什么使用指针呢?
对于routergroup,是因为这样可以保证用的框架的一致性,并且不需要反复复制这个框架,造成资源浪费。
对于DB,是因为我们链接数据库,就是要改值的。


在导入包后,我们可以进去看看routergroup.go
我的路径如下:
myGo\mypath\pkg\mod\github.com\gin-gonic\gin@v1.9.1



我么看到这个函数
// POST is a shortcut for router.Handle("POST", path, handlers).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}

它是一个RouterGroup类的方法,输入为一个相对路径和任意数量的HandlerFunc。返回一个 `IRoutes` 类型的对象。在 Gin 中,`IRoutes` 是一个接口,提供了链式路由的能力。那么如果不需要链式路由,我们是可以忽略这个返回值的。
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

`gin.Context` 是 Gin 框架中的一个核心结构体。它封装了一个 HTTP 请求的所有细节,并提供了许多方法来操作 HTTP 请求和响应。


// 增加todo
func addTodo(r *gin.RouterGroup, db sql.DB) {
r.POST("/todo", func(c *gin.Context) {
</br>
})
}

写下如上代码,创建了一个路由,处理器是一个匿名函数。匿名函数没有名字,一般一次性的,但是很方便。


完善后:
// 增加todo
func addTodo(r *gin.RouterGroup, db *sql.DB) {
r.POST("/todo", func(c *gin.Context) {
var todo TODO
c.BindJSON(&todo)
db.Exec("insert into todos(content,done) values(?,?)", todo.Content, todo.Done)
c.JSON(200, gin.H{"status": "OK"})
})
}

这里先把json绑定到结构体实例,再执行数据写入数据库,返回成功状态。
此处解释一下
type H map[string]any

`map[string]any`:这是定义 `H` 类型实际表示的数据结构。`map` 是 Go 语言中的一种内置数据类型,它是一个键值对的集合。这里:
- `string` 表示键的类型,即这个映射的每个键都是一个字符串。 - `any` 表示值的类型,这是 Go 1.18 引入的一个新特性,代表任意类型的值。`any` 实际上是 `interface{}` 的一个别名,意味着可以存储任何类型的值。
加上错误处理的完善后:
// 增加todo
func addTodo(r *gin.RouterGroup, db *sql.DB) {
r.POST("/todo", func(c *gin.Context) {
var todo TODO
</br>
if err := c.BindJSON(&todo); err != nil {
fmt.Println("Error:", err)
c.JSON(400, gin.H{
"status": "BadRequest", "error": err.Error(),
})
return
}
</br>
_, err := db.Exec("insert into todos(content,done) values(?,?)", todo.Content, todo.Done)
if err != nil {
c.JSON(500, gin.H{"status": "InternalServerError"})
return
}
c.JSON(200, gin.H{"status": "OK"})
})
}

更改主函数
func TodoService(r *gin.RouterGroup, db *sql.DB) {
addTodo(r,db)
}

回到main.go,进行如下建议配置:
package main
</br>
import (
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"hello/funcs"
"log"
)
</br>
func initDB() (db *sql.DB, err error) {
db, err = sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/tododb")
</br>
if err != nil {
log.Fatalf("Error opening database: %q", err)
//%q代表带引号的字符串
//log.Fatalf 是一个便捷的函数,用于打印错误信息并结束程序。
//它的行为类似于先调用 log.Printf 打印信息,
//然后调用 os.Exit(1) 来终止程序。
}
return db, err
}
func main() {
//var db *sql.DB
db, err := initDB()
if err != nil {
log.Fatalf("Error initializing database:%q", err)
}
</br>
defer db.Close()
r := gin.Default()
</br>
funcs.TodoService(r, db)
</br>
fmt.Println("Starting sever at http://localhost:8080")
</br>
r.Run(":8080")
</br>
}
</br>

post方法发送json
{"content":"1","done":true}


http://localhost:8080/todo

返回
{
"status": "OK"
}

当然,我们也可以测试其他的错误情况,会返回不同的json,可以加深对它的理解。


注意几点:
`r := gin.Default()`
- 这行代码创建了一个 Gin 框架的默认路由器。`gin.Default()` 返回一个新的 `gin.Engine` 实例,它包含了 Gin 的默认中间件(例如日志和恢复)。
如果遇到`Error opening database: "sql: unknown driver "mysql" (forgotten import?)"`,问题看起来是 Go 语言的 `database/sql` 包无法识别 "mysql" 数据库驱动。这通常发生在尚未导入相应的 MySQL 驱动包的情况下。
在 Go 中,使用数据库通常涉及两个步骤:
1. **导入 `database/sql` 包**:这是 Go 标准库的一部分,提供了与数据库交互的通用接口。 2. **导入特定数据库的驱动包**:例如,对于 MySQL,你通常会使用 `github.com/go-sql-driver/mysql`。
解决这个问题的方法是导入相应的 MySQL 驱动包。在你的 Go 文件的开头,添加以下导入语句:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)

请注意 `_` 符号的使用。这被称为**匿名导入**,在 Go 中用于导入一个包仅为了确保其初始化过程被执行,而不直接使用该包中的任何函数或方法。在这种情况下,它用于确保 MySQL 驱动注册到了 `database/sql`。
导入后,你的 `database/sql` 包就能够识别 "mysql" 作为数据库驱动了,应该能够解决你的问题。
如果你还没安装 `github.com/go-sql-driver/mysql`,你需要先通过命令 `go get -u github.com/go-sql-driver/mysql` 来安装它。

grom的导入


我们导入gorm,一种与数据库交互的框架:
package main
</br>
import (
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"hello/funcs"
"log"
)
</br>
func initDB() (db *gorm.DB, err error) {
dsn := "root:1234@tcp(127.0.0.1:3306)/tododb?charset=utf8mb4&parseTime=True&loc=Local"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
</br>
if err != nil {
log.Fatalf("Error opening database: %q", err)
}
return db, err
}
</br>
func main() {
db, err := initDB()
if err != nil {
log.Fatalf("Error initializing database: %q", err)
}
</br>
sqlDB, err := db.DB()
if err != nil {
log.Fatalf("Error getting generic database object: %q", err)
}
defer sqlDB.Close()
</br>
r := gin.Default()
</br>
funcs.TodoService(r, db) // 确保 funcs.TodoService 函数可以处理 *gorm.DB 类型的参数
</br>
fmt.Println("Starting server at http://localhost:8080")
</br>
r.Run(":8080")
}

解释一下这一句:
"root:1234@tcp(127.0.0.1:3306)/tododb?charset=utf8mb4&parseTime=True&loc=Local"

从前往后:数据库的用户名,密码,地址,数据库名,字符编码标准(utf8mb4还包括emoji),gotime与mysql时间保持一致,设置时区为本地。

项目功能实现

done值更改

前端如下:

{/* 显示数据库数据 */}
{dbData.map((item) => (
<div key={item.id} className={`card mb-2 ${item.done ? 'bg-green-100' : 'bg-red-100'}`} onClick={() => handleTaskToggle(item.id)}>
<div className="card-body flex justify-between items-center">
<div>{item.content}</div> {/* 左侧内容 */}
<div>{item.done ? "完成" : "未完成"}</div> {/* 右侧状态 */}
</div>
</div>
))}

导入部分

import {SetStateAction, useEffect, useState} from 'react';
// 导入Bootstrap样式

interface DbItem {
id: number;
content: string;
done: boolean;
// 这里可以添加其他属性,例如 id, title 等
}

函数设置

const KanbanBoard = () => {
const [tasks, setTasks] = useState([]); // 假设的tasks状态
const [newTaskContent, setNewTaskContent] = useState(""); // 新任务的内容

// 新增状态来存储从数据库获取的数据
// 使用定义的类型来指定状态类型
const [dbData, setDbData] = useState<DbItem[]>([]);
// 处理添加任务的事件
const handleAddTask = () => {
if (!newTaskContent) return; // 如果没有内容则不添加
const newTask = {
id: Date.now(),
content: newTaskContent,
};
// @ts-ignore
setTasks([...tasks, newTask]);
setNewTaskContent(""); // 清空输入框
};
const handleTaskToggle = async (taskId: number) => {
// 找到被点击的任务
const updatedTasks = dbData.map(task => {
if (task.id === taskId) {
return { ...task, done: !task.done }; // 切换任务的完成状态
}
return task;
});
setDbData(updatedTasks); // 更新状态

// 发送更新到后端
// 这里你需要根据你的API适配
await fetch(`http://localhost:8080/todo/${taskId}/toggle`, { method: 'POST' });
};


// 处理输入框内容变化的事件
const handleNewTaskChange = (event: { target: { value: SetStateAction<string>; }; }) => {
setNewTaskContent(event.target.value);
};
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('http://localhost:8080/data');
const data = await response.json();
console.log("Received data:", data); // 打印接收到的数据
setDbData(data);
} catch (error) {
console.error("Error fetching data: ", error);
}
};

fetchData();
// 如果使用轮询,可以在这里设置定时器
const interval = setInterval(fetchData, 5000); // 每5秒钟获取一次数据

return () => clearInterval(interval); // 清理定时器
}, []);

golang的代码:

在main中

r.POST("/todo/:id/toggle", func(c *gin.Context) {
funcs.ToggleTaskStatus(c, db)
})

然后有一个函数

package funcs

import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"net/http"
)

func ToggleTaskStatus(c *gin.Context, db *gorm.DB) {
var todo TODO
id := c.Param("id")

// 根据ID查找任务
if err := db.First(&todo, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}

// 切换任务的完成状态
todo.Done = !todo.Done

// 更新数据库
if err := db.Save(&todo).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update task"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "success"})
}