x
初始化项目
1 | mkdir echo-practice |
连接postgresql和redis
1 | if databaseURL == "" { |
echo负责路由和接口,pg用pgxpool连接池,redis用go-redis客户端,然后把连接对象注入到handler里面
Redis 存的是临时状态,比如匹配队列、在线用户、房间临时状态。
安装依赖
1 | go mod init echo-demo |
拉取失败,在当前powershell里面临时改代理环境变量
1 | $env:GOPROXY="https://goproxy.cn,direct" |
测试和连接redis
1 | var ctx = context.Background() |
1 | PONG <nil> |
连接postgresql
1 | var db *sql.DB |
1 | -- ========================================= |
1 | -- ========================================= |
1 | -- ========================================= |
1 | -- ========================================= |
以后
1 | -- ========================================= |
外键
链接两个表的字段,在一个表中存储的值必须匹配另一个表的主键或唯一键
1 | REFERENCES words(id) 外键约束:word_id 的值必须是 words 表中 id 列已有的值 |
id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
user表用uuid作为主键
sslmode=disable – 禁用 SSL(仅适用于本地开发,生产环境建议启用)
return &UserRepository{db: db}
???
已理解
1 | if err != nil { // 1. 发生了错误 |
ORDER BY RANDOM():
PostgreSQL 的随机排序函数,结果顺序随机。
COALESCE(meaning, ''):
如果 meaning 字段为 NULL,则返回空字符串,避免 Go 中扫描到 nil 字符串时出错。同样处理 phonetic 字段。
go引入自建包名报错 package XXX is not in std
这个错误消息表明Go代码尝试导入一个位于项目目录之外的包,并且Go无法找到这个包。首先看了我的包名对应的路径是没有问题的。
检查配置环境变量
1 | go run server\main.go |
为什么winnerID是指针地址
1 | func (contest *ConflictRepostiory) FinishBattle( |
允许没有胜者,
sync.Once
保证某个操作在全局范围内只执行一次
1 | var once sync.Once |
协程
1 | defer rows.Close() |
延迟执行close,在当前函数返回结果前自动关闭结果集,防止连接泄露,defer保障无论返回panic还是正常都会执行关闭
持有数据库连接资源的都defer
1 | for rows.Next() { |
遍历结果集的每一行
1 | JOIN synonyms s ON s.word_id = w.id |
内联查询,on后面跟查询条件
只保留那些在 synonyms 表中至少有一条关联记录的单词。如果一个单词在 synonyms 表中没有对应的行,则这个单词不会出现在查询结果中。
1 | group by w.id ,w.word |
按单词的id和单词本身分组
1 | having count(*) filter (where s.is_correct = true) >=1 |
filter语法,计算分组后四个单词里面is correct的数量,然后要求它大于等于1
1 | order by |
排序
1 | result := make([]wordRow, 0, count) |
创建一个result的切片,类型是wordRow,初始长度是0,容量是count
思路
有一个想法后先分析它是业务逻辑复杂还是主要数据库的查询
DDD
将业务逻辑作为软件的核心,通过建立与业务专家一致的“领域模型”来解决复杂业务问题。
BDD
一种敏捷开发实践,强调用自然语言描述软件的行为,从用户需求出发,让业务、开发和测试三方协作并自动验证。
ORM自动映射
对象关系映射 的一种高级功能,它能自动将数据库表结构映射到程序中的对象,并自动处理对象状态与数据库记录的同步。
首先确定
对战形式:实时,所以用到websocket
然后设计数据存储,游戏流程,前端需要展示题目,选项,用户点击选项后判断对错,更新得分,加载下一题
WebSocket 用于实时同步双方状态(得分、当前题目、谁答对等)。HTTP 用于页面加载、获取初始数据等。
没有思路就频繁切换到用户视角
用户a和b对战,一局有十轮,一轮中从word表里面取出一个单词,然后根据word id查synonyms
多行数据从数据库到业务层的转换
如果是十轮,就在查完一个单词的基础上加limit 10,把查询到的结果返回到rows里面,初始化一个result切片,长度是10,遍历rows,把每一行word id 和word扫描到wordRow里面,然后追加到result,这就实现了多行数据从数据库到业务层的转换
1 | func (r *QuestionRepository) getRandomWords(ctx context.Context) ([]wordRow, error) { |
根据word id查synonyms
取出来的单词和单词id放在业务层的wordRow后利用它查synonym,这个场景是已知word id,求synonyms
首先确定实体
前端需要通过ws向后端发送指令,所以定义客户端到服务端的消息格式
1 | type ClientMessage struct { |
服务端需要接收用户的回答,需要确定哪个玩家在哪一回合选择什么
1 | type Answer struct { |
服务端需要向用户发送信息
设计函数时要分析在什么场景,做什么动作,已知什么,要返回什么
服务端在游戏中向所有用户发送信息且不影响其他操作,所以需要先复制再发送
1 | // 错误做法:直接使用 map |
1 | Broadcast("hello") |
复制操作是写入操作,加锁保证获取某个时间点干净快照
需要定义一个游戏运行所需要的状态和依赖
用户输入需要写入泵,前端展示需要读取泵
两个用户在同一个场景需要房间
一个用户加入需要加入操作
如果前端int被访问即判断加入,通过int记录client信息,(创建客户端对象,然后读取赋值),如果两个人同时访问一个int,把先进入房间的通道关闭
什么情况下加锁
至少有两个go线程同时操作同一个变量,至少有一个是写操作
是map,go的map不是并发安全的,并发读写会panic
然后启动读写泵开始交互
ON CONFLICT (username)
冲突检测
完善注册功能
先确定数据模型struct实体,请求参数,注册响应
然后确定数据访问层,也就是repository,再确定service层,再确定handler层,最后配置路由
什么时候写defer
defer用于确保某个函数调用在当前函数返回前,无论正常返回还是返回panic,都要执行
只要获得了一个需要手动关闭或释放的资源,就在获取成功的下一行写defer释放他
常见情景有
文件操作
open后立即defer close
http响应体
get或post后立马defer close
互斥锁
mu.lock后立即defer unlock
数据库连接或事物
什么时候写err报错判断
飞书实际返回json后需要定义对应的go结构体去承接它
1 | -- 一次性添加两个列 |
1 | app_id 请求不合法 |
授权链接里的 client_id / app_id 不合法、为空、写错、或者不是飞书开放平台认可的应用 ID。飞书 OAuth 获取授权码接口文档说明,授权链接需要传 client_id、redirect_uri、response_type=code、state 等参数;重定向 URL 也需要在开放平台配置通过安全校验。
ngrok内网穿透
1 | ngrok http 8080 |
如有错误,多多指教