[12-21]
截至目前为止,前端是基于react+ts+tailwind的架构。
开个坑,写个项目。
Leaning doc 📝
初始化
首先我们创建初始化项目
这里的hello可以替换成任何喜欢的模块名的。
然后创建main.go
package main </br> func main() { </br> }
|
一般来说不要先想着导入库,goland会自动优化掉的。同理,如果用到了外部库,编译器会第一时间把包导入。
这时第一时间想到
fmt.Println("Hello, World!")
|
但是会发现输入print的话,会跳出很多函数,引起了我的好奇心。
我列出几个看到第一眼就感觉比较常见的。
内置的打印函数
打印后换行
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
|
我么看到这个函数
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPost, relativePath, handlers) }
|
它是一个RouterGroup类的方法,输入为一个相对路径和任意数量的HandlerFunc。返回一个 `IRoutes` 类型的对象。在 Gin 中,`IRoutes` 是一个接口,提供了链式路由的能力。那么如果不需要链式路由,我们是可以忽略这个返回值的。
type HandlerFunc func(*Context)
|
`gin.Context` 是 Gin 框架中的一个核心结构体。它封装了一个 HTTP 请求的所有细节,并提供了许多方法来操作 HTTP 请求和响应。
func addTodo(r *gin.RouterGroup, db sql.DB) { r.POST("/todo", func(c *gin.Context) { </br> }) }
|
写下如上代码,创建了一个路由,处理器是一个匿名函数。匿名函数没有名字,一般一次性的,但是很方便。
完善后:
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绑定到结构体实例,再执行数据写入数据库,返回成功状态。
此处解释一下
`map[string]any`:这是定义 `H` 类型实际表示的数据结构。`map` 是 Go 语言中的一种内置数据类型,它是一个键值对的集合。这里:
- `string` 表示键的类型,即这个映射的每个键都是一个字符串。
- `any` 表示值的类型,这是 Go 1.18 引入的一个新特性,代表任意类型的值。`any` 实际上是 `interface{}` 的一个别名,意味着可以存储任何类型的值。
加上错误处理的完善后:
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) } return db, err } func main() { 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
|
返回
当然,我们也可以测试其他的错误情况,会返回不同的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) </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';
interface DbItem { id: number; content: string; done: boolean; }
|
函数设置
const KanbanBoard = () => { const [tasks, setTasks] = useState([]); const [newTaskContent, setNewTaskContent] = useState("");
const [dbData, setDbData] = useState<DbItem[]>([]);
const handleAddTask = () => { if (!newTaskContent) return; const newTask = { id: Date.now(), content: newTaskContent, }; 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);
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);
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")
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"}) }
|