效果图
后端
前期准备
取一个随便点的名字
1 | mkdir goquestionapi |
这里和数据库相关的操作会使用两个工具,migrate
和 sqlc
,可以用 go 的方式安装,但这边习惯用 pacman,因为包升级好管理
1 | sudo pacman -S sqlc |
sqlc 是一个代码生成工具,生成的代码中连接数据库的部分用的是 Golang 原生 sql 库,性能极高(官方是这样说的)
为什么不用 GORM,因为由于多年 Java 开发经验先入为主,看到 GORM 的 API 好多时候会觉得莫名其妙,尝试过几遍,都弃坑了,并且据说有性能问题
sqlc 可以通过编写简单的 SQL 语句,来自动生成模板代码,那么只需要关注具体的业务逻辑即可,并且可以通过 sqlc.arg() 的方式给占位符起别名,使用起来比较顺手
数据库用的是 Postgres
对于这种一个问题多个答案,一对多的结构,其实我自己比较喜欢用 JSON 字段
为什么不直接用 MongoDB?其实工作多年接触的项目,只用过一次,不是很熟悉
为什么不用 MySQL?
1 | CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS |
如果开了 performance_schema 呢(默认是开)
1 | CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS |
练手而已
目前所在公司,新项目也纷纷上 Postgres,不涉及到那些储存过程函数之类,转换下 database schema 概念,其实用起来都没什么区别
准备数据,数据是从网上拿的,主要是一些问题,然后对应的选项写死了分值,最后根据总分来判断性格(性格测试这东西我本人觉得极为扯淡,有些公司面试还要做,莫名其妙,如果我所在公司也要做,那我前面什么都没说,这个测试好得很,必须做)
数据准备好,就可以导入数据库,这里用的是 migrate,所以先准备第一版的 SQL
sqlc 配置文件
1 | version: "2" |
schema 是数据库建表、删表语句,queries 是业务 SQL 的编写地方,然后 package 是生成的 go 代码的包名,out 就是代码生成的文件夹
1 | migrate create -ext sql -dir sqlc/migration -seq init |
migrate 参数:
- -ext: 数据脚本扩展名(后缀名)
- -dir: 脚本文件所在地,配合上面 sqlc 的配置文件就是在 sqlc/migration 里
- -seq: 就是一个 sequence,自增
- 最后是文件名字
上面的命令就会生成 000001_init.down.sql 和 000001_init.up.sql 这两个文件,up 就是新改动,down 就是回滚,和别的数据库 migration 一样
首先需要先创建 schema,这里 schema 叫 question,表也叫 question(表则可以在 migration sql 里创建)
创建好 schema 之后 migrate 和 sqlc 才会好使,不然会有先有鸡还是先有蛋的问题
1 | CREATE SCHEMA question; |
准备 up sql
1 | CREATE TABLE |
1 | migrate -path sqlc/migration -database "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable&search_path=question" -verbose up |
down sql
1 | DROP TABLE question; |
同理
1 | migrate -path sqlc/migration -database "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable&search_path=question" -verbose down |
接着就是编写获取问题列表的 SQL
1 | -- name: GetQuestions :many |
根据 sqlc 的规范,-- name: 方法名
,这些空格都是必须的,如果返回一条数据就 :one
,多条则 :many
,如果只是增删,可以用 :exec
执行 sqlc generate
这样就会生成好几个 go 文件,其中生成的方法类似
1 | func (q *Queries) GetQuestions(ctx context.Context) ([]GetQuestionsRow, error) { |
但是每次都要输入这么长的命令,虽然可以键盘上下,但是有更好的方法,就是 Makefile
1 | up: |
.PHONY
的作用就是确保执行的是这里定义的命令,比如 Makefile 里定义个 ls,如果不加到 .PHONY 里,则执行的是系统的 ls
编写业务逻辑
定义配置类,用到了 viper
,有一个好处就是 AutomaticEnv()
能够将环境变量覆盖掉代码配置,这样在部署的时候非常方便,日志则用了 logrus,可以设置不同的级别,还有颜色输出,Java 过来表示很亲切
1 | package util |
然后就可以开启调试了,可以 build 或者直接 run,或者用其他热加载的工具,这里用到了 gowatch
直接执行 gowatch 就可以将终端晾在一边了,和前端开发的体验没什么区别,Java 就要重启,没用过 JRebel
此时访问本地 8080 就能得到 404 的正确响应了
接着加一个 CORS
的中间件,这里不加,前端就要 proxy
1 | package middleware |
给 server 加上
1 | server.Use(middleware.CORS()) |
API
1 | package api |
路由配置
1 | package router |
给 server 加上,并且让其具有查询 db 的功能
1 | sqlDB, err := sql.Open(config.DBDriver, config.DBSource) |
需要注意一点就是,要让 db 的驱动执行初始化动作
1 | import _ "github.com/lib/pq" |
接着再访问 http://localhost:8080/questions
,就能看到数据
1 | { |
但是 json 的 key 全部大写了,可以在 sqlc 上配置 emit_json_tags
,修改后的配置
1 | version: "2" |
再重新跑一遍 make gen
,再刷新页面,就能拿到小写的 key,比较符合习惯
1 | { |
至此获取问题列表 API 完成,接着是答题然后计算分数
这里用到下面的数据结构进行提交,思路还是逐条拿分数出来再叠加,没有太多花里胡哨的 SQL
1 | { |
于是根据题目拿分数的 SQL 就会是
1 | -- name: GetQuestionScoreByID :one |
这里可以用 $1
,$2
等的占位符,不过用 sqlc.arg()
就会更加顾名思义一些
再执行 make gen
,生成新代码
接着编写完答题 API (练手而已,没有太多封装)
1 | type answersRequest struct { |
配置下路由
1 | group.POST("", router.api.HandleAnswers) |
简单测试能得到结果
1 | { |
此时 API 就完成了,接着就是部署
部署
如果按照其他语言,一般都是 FROM
一个官方的基础镜像,然后在上面再进行构建,这样就有第一种
1 | FROM FROM golang:alpine |
这样出来的镜像是很大的,因为 golang 的 alpine 镜像本身就很大了
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
而由于 go 本身特点,可以编译出来一个摆脱其他束缚的二进制文件,因此可以直接上 alpine
1 | FROM alpine |
1 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build . |
这样 build 出来的程序有 13M,还可以再优化下,-s
可以去掉符号表,-w
可以去掉调试信息
1 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" . |
这样 build 出来的程序只有 8.5M
跑下镜像测试下
1 | docker run --rm -p 8080:8080 -e DB_SOURCE="postgresql://postgres:postgres@192.168.31.60:5432/postgres?sslmode=disable&search_path=question" -d goquestionapi |
访问正常拿到数据
此时 docker 镜像大小为 14.4M
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
上面说了,由于 go 自身的特性,如果没有额外的需求,还可以用 scratch
镜像,只是这样就不能 sh 进去容器,如果有访问外网的需求,也会报 SSL 证书错误,又或者时区的问题,因为里面确实什么都没有
这个 scratch 感觉更像是一个概念,不是实际可以摸到的东西,因为如果像其他镜像那样 pull 的话,会报错
1 | docker pull scratch |
这样 build 出来的镜像大小就又小了些
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
但是为了后续方便 sh 进去,还是习惯用 alpine,也不差那几兆硬盘
现如今都在谈 DevOps,谈云原生,一般 cicd 都在云端了,因此会有第二种,分阶段构建
这个时候就需要一个 go 环境,所以要利用 golang 镜像来做编译,完了再把编译出来的程序,再复制到基础的 alpine 镜像上
1 | FROM golang:alpine AS builder |
此时编译正常,镜像小,也能正常运行
docker build
的方式会产生很多 <none>
的没用的镜像,每次还要跑一大串命令,这有点不方便,因此可以用 Docker Compose
1 | services: |
只要 docker-compose build
就可以把镜像 build 出来,并且可以灵活修改配置,命令不变,和 Makefile 的思想一样
还有一个好处就是没有 <none>
镜像,代码洁癖+强迫症患者表示很舒服
此时跑 docker-compose up
,也能正常看到数据
以上是本地部署,线上大多是 Kubernetes 环境
1 | apiVersion: apps/v1 |
由于我本地 K8s 环境是 kind
,所以要把镜像 load 进去
1 | kind load docker-image goquestionapi |
有两点需要注意,第一就是确保能拿到镜像,要么给个 tag 不用 latest 再通过加载镜像的方式,要么修改 imagePullPolicy 为 IfNotPresent 或者 Never,要么正确配置仓库地址,无论用 Docker Hub 还是自建
还有一点就是 postgres 要用 K8s 内部的连接,因为我的 postgres 是用 helm 安装的,所以可以通过 helm status postgres
查看到集群内部链接
apply 后访问 http://localhost/api/questions
,也能正常看到数据
VSCode 或者 Goland 装好 Kubernetes 插件后,都能很方便直接 apply
VSCode 的话,配置写在一起会有提示无法对比,只能对比单个的,但是单个的话,已经 apply 了的配置会自动带上时间之类的额外的东西,对比起来也不是很直接,所以这里还是写在一起
至此,API 部分完
前端
前期准备
利用 create-react-app
来创建 React 项目
1 | yarn create react-app reactquestionweb |
接着集成第三方组件,axios
刚发布了 1.0.0
版本,会有兼容问题,因此还是用回老版本 0.27.2
版本
1 | yarn add antd |
编写业务逻辑
这里没什么好说的了,用函数式组件 + hook 完成网络请求、条件渲染,其他的就按照后端接口约定的参数提交
不会 CSS,所以没有太多华丽的辞藻显示在页面上
部署
部署好多人都喜欢用 node 的镜像然后用 npm start,但其实我这边习惯用 nginx,除了 yarn build
会有优化外,你都 build 出来静态文件了,还要 node 干啥
Dockerfile
1 | FROM node:alpine AS builder |
compose.yaml
1 | services: |
k8s.yaml
1 | apiVersion: apps/v1 |
然后访问 http://localhost
,一切正常
到这里就完了,附上项目地址
纯属抛砖玉玉,如果有不同的意见,以你的为准