三天上手Go以及实战Gin+Gorm

2022-12-20 14:40:00 浏览数 (2)

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

语法特性
变量
  1. 命名一般是名称在前, 类型在后
  2. 匿名变量使用_标记忽略
  3. 常量使用 const, 例const c_name1, c_name2 = value1, value2. 关键字: iota, 索引自增进行初始化常量
代码语言:javascript复制
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)
}

4. 支持指针

5. 支持结构体

6. 切片/slice: 可以看作动态数组, 且go提供一些内置方法

7. 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中使用, 用于类型匹配
代码语言:javascript复制
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("未知型")
}

3. x.(type)在if中的形式

代码语言:javascript复制
type test struct {
}

func main() {
	var x interface{}
    // 判断x是否为指定类型
	if _, ok := x.(test); ok {
		
	}
}

4. switch不再需要break, 执行了一个分支后自动退出, fallthrough该关键字会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true. 类型匹配中不能使用fallthrough

5. select是一种go可以处理多个通道之间的机制, 每个 case 必须是一个通信操作,要么是发送要么是接收. 当多个case可以执行时, 随机选取一个case执行, 当没有case可执行也没有default时, 发生阻塞. 即select可以同时监控多个通道的情况,只处理未阻塞的case. 对于没有case的select{}会一直等待, 可用于阻塞main函数

代码语言:javascript复制
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)
		}
	}
}

6. 支持goto关键字

7. range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素

函数
  1. 函数支持多返回值, 甚至a, b = b, a进行a/b值的交换
  2. 函数可以绑定到某一个方法上, 例: func (b* math) ParseToken(a*int) error, 即为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, 则异常会继续外抛, 直至程序退出
代码语言:javascript复制
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(&amp;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()
}

运行输出

代码语言:javascript复制
手动宕机前
error: &amp;{手动触发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("%sn", string(48))
语法综合例子
例子一
代码语言:javascript复制
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 %dn", *a, b.add)
    *a  
    return nil
}

// 全局函数
func test() error {
	fmt.Printf("test: %dn", 1)
	return nil
}

func main() {
    // 初始化结构体, := 类型推导
	m := math{add: 2}
	a := 1
    // 等同于
    // 1. var err = m.ParseToken(&amp;a)
    // 2. err := m.ParseToken(&amp;a)
	var err error = m.ParseToken(&amp;a)
    // 错误处理
	if err != nil {
		return
	}
	err = test()
	if err != nil {
		return
	}
	fmt.Printf("ss: %dn", a)
}
例子二
代码语言:javascript复制
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 = %vn", tc)
}
例子三
代码语言:javascript复制
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) &amp;&amp; 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 = %vn", 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内的依赖

代码语言:javascript复制
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地址
代码语言:javascript复制
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
}

3. 比如登录接口url是/base/login

4. server/initialize/router.go line: 52, 使用jwt和casbin对接口进行鉴权

5. 最终请求到达对应方法进行处理

写一个新接口

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

  1. api/v1/system/sys_user中实现方法
代码语言:javascript复制
func (b *BaseApi) GetUserById(c *gin.Context) {
	var id systemReq.GetUserInfoById

	if err := c.ShouldBind(&amp;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)
}

2. service实现如下

代码语言:javascript复制
func (userService *UserService) GetUserInfoById(id int) (data interface{}, err error) {
	db := global.GVA_DB.Model(&amp;system.SysUser{})
	user := new(system.SysUser)
	err = db.Where("id = ?", id).First(&amp;user).Error
	return user, err
}

3. 在router/system/sys_user:InitUserRouter中, 添加一行userRouterWithoutRecord.GET("getUserInfoById", baseApi.GetUserById)

4. 数据库添加一行权限

代码语言:javascript复制
INSERT INTO `gva`.`casbin_rule`(`ptype`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`, `v6`, `v7`) VALUES ('p', '888', '/user/getUserInfoById', 'GET', '', '', '', '', '');

5. 根据路由映射, 找到/base/login对应的处理代码, 删除掉验证码校验的逻辑

6. 调用登录接口获取token

代码语言:javascript复制
POST /base/login
{
    "username": "admin",
    "password": "123456"
}

7. 使用该token调用/user/getUserInfoById?id=1就可以看到响应信息了

代码语言:javascript复制
GET /user/getUserInfoById?id=1
headers: 
{
    "x-token": "xxxxxxxxxxxxxxxxxxx"
}

0 人点赞