快速上手Go以及实战Gin+Gorm

kyaa111 1年前 ⋅ 429 阅读

阅读本文需要一定的java开发经验以及一点c/c++的基础

语法特性

变量
  1. 命名一般是名称在前, 类型在后
  2. 匿名变量使用_标记忽略
  3. 常量使用 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)
}

  1. 支持指针
  2. 支持结构体
  3. 切片/slice: 可以看作动态数组, 且go提供一些内置方法
  4. channel: 通道, 例: ch := make(chan int), 声明一个int的通道, channel是引用类型, 只能使用make初始化. 通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯. 通道类似一个先入先出队列. channel有发送/接收/关闭三种动作
    1. ch = make(chan int, 2) 带缓冲区通道, 异步通道
    2. ch = make(chan int) 无缓冲区通道, 又称为同步通道, 必须要同步接受
    3. 可以限定通道只读或只写, 例只读: ch = make(<-chan int)
    4. 向一个nil channel发送消息, 会一直阻塞
    5. 向一个已经关闭的channel发送消息, 会引发运行时恐慌(panic)
    6. channel关闭后不可以继续向channel发送消息,但可以继续从channel接收消息
    7. 当channel关闭并且缓冲区为空时, 继续从channel接收消息会得到一个对应类型的零值
    8. 通道是可以被垃圾回收机制回收的, 关闭通道不是必须的
    9. 当不确定channel状态时, 可以用v, ok := <- ch中的ok判断是否读取到了值
流程控制
  1. 没有三目运算符,不支持 ? :形式的条件判断
  2. 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("未知型")
}
  1. x.(type)在if中的形式
type test struct {
}

func main() {
	var x interface{}
    // 判断x是否为指定类型
	if _, ok := x.(test); ok {
		
	}
}
  1. switch不再需要break, 执行了一个分支后自动退出, fallthrough该关键字会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true. 类型匹配中不能使用fallthrough
  2. 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)
		}
	}
}

  1. 支持goto关键字
  2. range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素
函数
  1. 函数支持多返回值, 甚至a, b = b, a进行a/b值的交换
  2. 函数可以绑定到某一个方法上, 例: func (b* math) ParseToken(a*int) error, 即为math结构体添加了一个方法. 注意 (b* math)(b math) 的区别在于: 前者相当于传递了指向当前对象的指针, 可以修改该结构体内的属性, 后者传递了当前对象的复制体, 修改属性不能影响原对象.
  3. 方法首字母大写 即包外public方法 小写为private方法
  4. 接口不需要显式实现, 如implement. 方法签名相同即认为是实现了接口. 所以基于此, 有一种特殊的空接口, type name interface{}, 每个类型都实现了空接口. 在函数的参数以及返回, 都可以用空接口做定义, 表示该函数接收或返回任意类型
  5. 错误处理 函数通过多返回值中返回error表示当前函数调用发生了错误
异常
  1. 一个关键字 defer, 两个函数panic, recover
  2. panic类似throw, 抛出一个异常, go自身也会抛出异常, 如npe
  3. defer类似finally, 在当前函数退出前必定执行, 多个defer倒序执行
  4. 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
其他
  1. 关键字go开启协程
  2. new()和make()的区别:
    1. new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
    2. make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型: slice/map/channel
  3. 提供类型转换函数: 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

下载依赖
  1. go env -w GOPROXY=https://goproxy.cn,direct
  2. go mod tidy
  3. go mod download
  4. 启动main方法
  5. 启动前端, 按官方文档初始化
依赖分析

查看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启动流程
  1. 初始化viper加载配置
  2. 初始化日志库zap
  3. 初始化orm
  4. 初始化定时任务
  5. RunWindowsServer中初始redis/从数据库加载jwt黑名单/初始gin路由/
接口流程
  1. server/core/server.go line:28初始化总路由
  2. 一路跳转可以看到这里定义了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
}

  1. 比如登录接口url是/base/login
  2. server/initialize/router.go line: 52, 使用jwt和casbin对接口进行鉴权
  3. 最终请求到达对应方法进行处理
写一个新接口

根据id获取用户信息的接口, 并加上权限校验

  1. 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)
}
  1. 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
}
  1. router/system/sys_user:InitUserRouter中, 添加一行userRouterWithoutRecord.GET("getUserInfoById", baseApi.GetUserById)
  2. 数据库添加一行权限
INSERT INTO `gva`.`casbin_rule`(`ptype`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`, `v6`, `v7`) VALUES ('p', '888', '/user/getUserInfoById', 'GET', '', '', '', '', '');
  1. 根据路由映射, 找到/base/login对应的处理代码, 删除掉验证码校验的逻辑
  2. 调用登录接口获取token
POST /base/login
{
    "username": "admin",
    "password": "123456"
}
  1. 使用该token调用/user/getUserInfoById?id=1就可以看到响应信息了
GET /user/getUserInfoById?id=1
headers: 
{
    "x-token": "xxxxxxxxxxxxxxxxxxx"
}