快速上手Go以及实战Gin+Gorm
2022-12-20 14:24:49 567
阅读本文需要一定的java开发经验以及一点c/c++的基础
语法特性
变量
- 命名一般是名称在前, 类型在后
- 匿名变量使用
_
标记忽略 - 常量使用 const, 例
const c_name1, c_name2 = value1, value2
. 关键字:iota
, 索引自增进行初始化常量
package main
import "fmt"
const (
i = 1 << iota
j = 3 << iota
// 等同于
// k = 3 << iota
k
// l = 3 << iota
l
)
func main() {
fmt.Println("i=", i)
fmt.Println("j=", j)
fmt.Println("k=", k)
fmt.Println("l=", l)
}
- 支持指针
- 支持结构体
- 切片/slice: 可以看作动态数组, 且go提供一些内置方法
- channel: 通道, 例:
ch := make(chan int)
, 声明一个int的通道, channel是引用类型, 只能使用make初始化. 通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯. 通道类似一个先入先出队列. channel有发送/接收/关闭三种动作
流程控制
- 没有三目运算符,不支持
? :
形式的条件判断 - x.(type)在switch中使用, 用于类型匹配
var x interface{}
// 获取x的真实类型
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T", i)
fallthrough
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
- x.(type)在if中的形式
type test struct {
}
func main() {
var x interface{}
// 判断x是否为指定类型
if _, ok := x.(test); ok {
}
}
- switch不再需要break, 执行了一个分支后自动退出,
fallthrough
该关键字会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true. 类型匹配中不能使用fallthrough - select是一种go可以处理多个通道之间的机制, 每个 case 必须是一个通信操作,要么是发送要么是接收. 当多个case可以执行时, 随机选取一个case执行, 当没有case可执行也没有default时, 发生阻塞. 即select可以同时监控多个通道的情况,只处理未阻塞的case. 对于没有case的select{}会一直等待, 可用于阻塞main函数
package main
import (
"fmt"
"time"
)
func main() {
// int类型通道 channel
chan1 := make(chan int)
chan2 := make(chan int)
// 开启goroutine
go func() {
for {
// 向通道输入 1
chan1 <- 1
time.Sleep(time.Second * 3)
}
}()
go func() {
for {
chan2 <- 2
time.Sleep(time.Second * 3)
}
}()
time.Sleep(time.Second)
for {
select {
// 从通道取值, 用a接收
case a := <-chan1:
fmt.Printf("a = <-chan1 %d \n", a)
time.Sleep(time.Second)
case b := <-chan2:
fmt.Printf("b = <-chan2 %d \n", b)
time.Sleep(time.Second)
// time.After返回是通道类型的值 所以可以用作case的表达式
// time.After是go的time包提供的一个定时器的一个函数
// 它返回一个channel,并在指定时间间隔后,向channel发送一条数据
// time.After(time.Microsecond * 500)就是500ms后向这个channel发送一个数据.
case <-time.After(time.Microsecond * 500):
fmt.Println("timeout")
time.Sleep(time.Second)
}
}
}
- 支持
goto
关键字 - range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素
函数
- 函数支持多返回值, 甚至
a, b = b, a
进行a/b值的交换 - 函数可以绑定到某一个方法上, 例:
func (b* math) ParseToken(a*int) error
, 即为math结构体添加了一个方法. 注意(b* math)
和(b math)
的区别在于: 前者相当于传递了指向当前对象的指针, 可以修改该结构体内的属性, 后者传递了当前对象的复制体, 修改属性不能影响原对象. - 方法首字母大写 即包外public方法 小写为private方法
- 接口不需要显式实现, 如implement. 方法签名相同即认为是实现了接口. 所以基于此, 有一种特殊的空接口,
type name interface{}
, 每个类型都实现了空接口. 在函数的参数以及返回, 都可以用空接口做定义, 表示该函数接收或返回任意类型 - 错误处理 函数通过多返回值中返回error表示当前函数调用发生了错误
异常
- 一个关键字
defer
, 两个函数panic
,recover
- panic类似throw, 抛出一个异常, go自身也会抛出异常, 如npe
- defer类似finally, 在当前函数退出前必定执行, 多个defer倒序执行
- recover类似catch, 但仅在延迟函数 defer 中有效, 接住异常并获取异常上下文. 若defer中没有recover, 则异常会继续外抛, 直至程序退出
package main
import (
"fmt"
"runtime"
)
type panicContext struct {
function string
}
func ProtectRun(entry func()) {
defer fmt.Println("exit1")
defer fmt.Println("exit2")
// 延迟处理的函数
defer func() {
// 发生宕机时,获取panic传递的上下文并打印
err := recover()
switch err.(type) {
// 运行时错误
case runtime.Error:
fmt.Println("runtime error:", err)
// 非运行时错误
default:
fmt.Println("error:", err)
}
}()
// 若entry发生异常, 则defer中的函数执行 再然后 输出 exit2 exit1
entry()
}
func test() {
defer fmt.Println("test退出前执行")
fmt.Println("test")
}
func main() {
ProtectRun(func() {
fmt.Println("手动宕机前")
// 手动触发 使用panic传递上下文
panic(&panicContext{
"手动触发panic",
})
})
ProtectRun(func() {
fmt.Println("赋值宕机前")
var a *int
// npe
*a = 1
fmt.Println("赋值宕机后")
})
// main 函数退出前执行这两个defer
defer fmt.Println("无异常延迟运行1")
defer fmt.Println("无异常延迟运行2")
fmt.Println("-----------------------------")
test()
}
运行输出
手动宕机前
error: &{手动触发panic}
exit2
exit1
赋值宕机前
runtime error: runtime error: invalid memory address or nil pointer dereference
exit2
exit1
-----------------------------
test
test退出前执行
无异常延迟运行2
无异常延迟运行1
其他
- 关键字
go
开启协程 - new()和make()的区别:
- 提供类型转换函数:
fmt.Printf("%s\n", string(48))
语法综合例子
例子一
package main
import "fmt"
// 定义一个结构体 类似class
type math struct {
add int
}
// 为结构体添加方法 方法首字母大写 即 public方法 小写为private方法 此时的b相当于this指针
func (b *math) ParseToken(a *int) error {
fmt.Printf("ss: %d %d\n", *a, b.add)
*a++
return nil
}
// 全局函数
func test() error {
fmt.Printf("test: %d\n", 1)
return nil
}
func main() {
// 初始化结构体, := 类型推导
m := math{add: 2}
a := 1
// 等同于
// 1. var err = m.ParseToken(&a)
// 2. err := m.ParseToken(&a)
var err error = m.ParseToken(&a)
// 错误处理
if err != nil {
return
}
err = test()
if err != nil {
return
}
fmt.Printf("ss: %d\n", a)
}
例子二
package main
import (
"fmt"
"math"
"sync"
"time"
)
// isPrime 判断质数
func isPrime(n int) bool {
i := 0
if n <= 3 {
return n > 1
}
for i = 2; float64(i) <= math.Sqrt(float64(n)); {
if n%i == 0 {
return false
}
i++
}
return true
}
func cal() {
// 等待组
var wg sync.WaitGroup
arr := [count]int{}
// 启动10个协程 即等待数量为10
wg.Add(count)
for i := 0; i < count; i++ {
i := i
go func() {
for j := i; j < max; j += count {
if isPrime(j) {
arr[i]++
}
}
// 当前协程完成 计数减一
wg.Done()
}()
}
// 等待结果
wg.Wait()
// 统计结果
sum := 0
for i := range arr {
sum += arr[i]
}
fmt.Println(sum)
}
// 常量 可以用来定义数组长度
const (
count = 10
max = 200000000
)
func main() {
now := time.Now()
cal()
// 统计耗时
tc := time.Since(now)
// 两亿数字耗时1m26.6016808s 共11078937个质数
fmt.Printf("time cost = %v\n", tc)
}
例子三
package main
import (
"fmt"
"math"
"sync"
"time"
)
func isPrime(n int) bool {
i := 0
if n <= 3 {
return n > 1
}
for i = 2; float64(i) <= math.Sqrt(float64(n)); {
if n%i == 0 {
return false
}
i++
}
return true
}
func cal() {
// 声明一个通道 缓冲区大小为1000
ch := make(chan int, count*100)
// 等待组 等待所有协程退出再结束
var wg sync.WaitGroup
wg.Add(count + 1)
// 开启生产者协程
go func() {
for i := 1; i < max; i++ {
ch <- i
}
close(ch)
wg.Done()
}()
arr := [count]int{}
for i := 0; i < count; i++ {
// 避免匿名函数引用外部变量出现非预期的值
i := i
// 消费者协程
go func() {
for v := range ch {
if isPrime(v) && v != 0 {
arr[i]++
}
}
wg.Done()
}()
}
// 等待所有协程结束
wg.Wait()
// 统计结果
sum := 0
for i := range arr {
sum += arr[i]
}
fmt.Println(sum)
}
// 常量 可以用来定义数组长度
const (
count = 10
max = 200000000
)
func main() {
now := time.Now()
cal()
// 统计耗时
tc := time.Since(now)
// 两亿数字耗时54.0657135s 共11078937个质数
fmt.Printf("time cost = %v\n", tc)
}
实战项目
以这个项目的后端工程为例: https://github.com/flipped-aurora/gin-vue-admin
下载依赖
- go env -w GOPROXY=https://goproxy.cn,direct
- go mod tidy
- go mod download
- 启动main方法
- 启动前端, 按官方文档初始化
依赖分析
查看go.mod内的依赖
require (
// 模板引擎
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
// 对象存储相关
github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible
// 对象存储相关
github.com/aws/aws-sdk-go v1.42.27
// 权限框架
github.com/casbin/casbin/v2 v2.51.0
// 使用数据库配置权限
github.com/casbin/gorm-adapter/v3 v3.7.3
// websocket
github.com/flipped-aurora/ws v1.0.2
// 监听配置文件修改 viper进行重新加载
github.com/fsnotify/fsnotify v1.4.9
// 不停机也可以重启服务
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
// web框架
github.com/gin-gonic/gin v1.7.0
// redis支持
github.com/go-redis/redis/v8 v8.11.4
// mysql驱动
github.com/go-sql-driver/mysql v1.6.0
// jwt
github.com/golang-jwt/jwt/v4 v4.3.0
// 用于终端显示颜色
github.com/gookit/color v1.3.1
// 对象存储相关
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.8+incompatible
// 发邮件
github.com/jordan-wright/email v0.0.0-20200824153738-3f5bafa1cd84
// 日志文件归档
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
// base64图像字符串的验证码
github.com/mojocn/base64Captcha v1.3.1
// 递归复制目录
github.com/otiai10/copy v1.7.0
// 更友好的错误处理
github.com/pkg/errors v0.9.1
// 对象存储相关
github.com/qiniu/api.v7/v7 v7.4.1
// 定时任务
github.com/robfig/cron/v3 v3.0.1
// 生成uuid
github.com/satori/go.uuid v1.2.0
// 提供一些机器信息 如内存 cpu核心之类
github.com/shirou/gopsutil/v3 v3.22.5
// 一些工具
github.com/songzhibin97/gkit v1.2.7
// 配置文件支持
github.com/spf13/viper v1.7.0
// 断言
github.com/stretchr/testify v1.7.1
// api文档
github.com/swaggo/gin-swagger v1.3.0
// api文档
github.com/swaggo/swag v1.7.0
// 对象存储相关
github.com/tencentyun/cos-go-sdk-v5 v0.7.19
// https支持
github.com/unrolled/secure v1.0.7
// 日志
go.uber.org/zap v1.16.0
// 加密支持
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
// 并发支持
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
// 文本处理支持 如unicode之类
golang.org/x/text v0.3.7
// mysql驱动
gorm.io/driver/mysql v1.3.3
// postgres驱动
gorm.io/driver/postgres v1.3.4
// orm
gorm.io/gorm v1.23.4
// websocket
nhooyr.io/websocket v1.8.6
)
main启动流程
- 初始化viper加载配置
- 初始化日志库zap
- 初始化orm
- 初始化定时任务
- RunWindowsServer中初始redis/从数据库加载jwt黑名单/初始gin路由/
接口流程
server/core/server.go line:28
初始化总路由- 一路跳转可以看到这里定义了base相关的url地址
package system
import (
v1 "github.com/flipped-aurora/gin-vue-admin/server/api/v1"
"github.com/gin-gonic/gin"
)
type BaseRouter struct{}
func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
baseRouter := Router.Group("base")
baseApi := v1.ApiGroupApp.SystemApiGroup.BaseApi
{
baseRouter.POST("login", baseApi.Login)
baseRouter.POST("captcha", baseApi.Captcha)
}
return baseRouter
}
- 比如登录接口url是
/base/login
server/initialize/router.go line: 52
, 使用jwt和casbin对接口进行鉴权- 最终请求到达对应方法进行处理
写一个新接口
根据id获取用户信息的接口, 并加上权限校验
- 在
api/v1/system/sys_user
中实现方法
func (b *BaseApi) GetUserById(c *gin.Context) {
var id systemReq.GetUserInfoById
if err := c.ShouldBind(&id); err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
return
}
data, err := userService.GetUserInfoById(1)
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
return
}
response.OkWithData(data, c)
}
- service实现如下
func (userService *UserService) GetUserInfoById(id int) (data interface{}, err error) {
db := global.GVA_DB.Model(&system.SysUser{})
user := new(system.SysUser)
err = db.Where("id = ?", id).First(&user).Error
return user, err
}
- 在
router/system/sys_user:InitUserRouter
中, 添加一行userRouterWithoutRecord.GET("getUserInfoById", baseApi.GetUserById)
- 数据库添加一行权限
INSERT INTO `gva`.`casbin_rule`(`ptype`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`, `v6`, `v7`) VALUES ('p', '888', '/user/getUserInfoById', 'GET', '', '', '', '', '');
- 根据路由映射, 找到
/base/login
对应的处理代码, 删除掉验证码校验的逻辑 - 调用登录接口获取token
POST /base/login
{
"username": "admin",
"password": "123456"
}
- 使用该token调用
/user/getUserInfoById?id=1
就可以看到响应信息了
GET /user/getUserInfoById?id=1
headers:
{
"x-token": "xxxxxxxxxxxxxxxxxxx"
}