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

2022-12-20 14:24:49 567

阅读本文需要一定的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. 没有三目运算符,不支持 ? : 形式的条件判断
  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()的区别:
  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"
}


Gin框架集成Zap日志库

在go语言gin框架中,日志是默认输出到终端的,但是我们在实际工作中,一般来说是需要记录服务器日志的。而最常用的日志库就是zap日志库,我们需要将gin在终端输出的内容通过zap日志库记录到文件中假设你已配置好了Gin/ZapginDefault := gin.New() ginDefault.Us
2023-03-22

Ubuntu Golang 编译 + Docker部署

配置Golang编译环境下载go环境 https://studygolang.com/dl 选择go1.20.2.linux-amd64.tar.gztar -zxvf go1.20.2.linux-amd64.tar.gz 解压到 /usr/local/go创建 /usr/local/go-pat
2023-03-22

Go的方法接收: 值接收与指针接收

package main import "fmt" type Circle struct { r uint } func (c *Circle) add() uint { c.r++ return c.r } func (c Circle) show() uint { c.r++
2023-03-04

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

阅读本文需要一定的java开发经验以及一点c/c++的基础语法特性变量命名一般是名称在前, 类型在后匿名变量使用_标记忽略常量使用 const, 例const c_name1, c_name2 = value1, value2. 关键字: iota, 索引自增进行初始化常量package main
2022-12-20

解决GoLand无法Debug

go 1.20rc1goland 2022.2.3无法进行debug控制台提示WARNING: undefined behavior - version of Delve is too old for Go version 1.20.-1 (maximum supported version 1.1
2022-12-14

freemarker 时间显示不正常 设置时区

项目在本地开发的时候显示正常,部署上服务器就一直差8个小时,最后发现freemarker官方文档有这样的说明time_zone:时区的名称来显示并格式化时间。 默认情况下,使用JVM的时区。 也可以是 Java 时区 API 接受的值,或者 "JVM default" (从 FreeMarker 2
2020-03-28
IDEA 2019.1 xml 不高亮

IDEA 2019.1 xml 不高亮

前几天更新了idea后,发现xml里的代码都没有了高亮,变得跟记事本一个德性了打开setting ,搜索 File Types,找到xml项, 查看下方的匹配格式,果然没有xml,(idea真是厉害)点击右方的+,输入*.xml,点击ok,解决问题
2020-03-28

npm install 淘宝镜像

npm install --registry=https://registry.npm.taobao.org
2020-03-28
Java中方法的参数传递机制

Java中方法的参数传递机制

来看一段代码 public class Man { private String name; private Integer age; public String getName() { return name; } publi
2020-03-28
基于自定义注解手写权限控制

基于自定义注解手写权限控制

方法一: AOP 方法二: 拦截器项目结构项目依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-w
2020-03-28

Docker 部署 详细全过程 附代码

Docker 部署本站 全过程环境:CentOS7.61. 安装Docker其他版本CentOS可以参考这个https://help.aliyun.com/document_detail/187598.html查看本机内核版本,内核版本需高于 3.10uname -r 确保 yum 包最新yum u
2020-03-28

SpringBoot 启动普通java工程

引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.0.9</version> </dependency>
2020-03-28

Vue.js DOM操作

<template> <input type="button" @click="reply($event)" value="回复"> </template> export default { methods: { replyFun(e) {
2020-03-29
CentOS7编译调试OpenJDK12

CentOS7编译调试OpenJDK12

1. 下载源码https://hg.openjdk.java.net/jdk/jdk12点击左侧的browse,再点击zip,就可以下载zip格式的源码压缩包。unzip xxx.zip 解压文件2. 安装jdkyum install java-11-openjdk-devel -y3. 运行con
2020-04-23
编写自己的Spring Boot Starter

编写自己的Spring Boot Starter

1.新建一个maven项目命名规则统一是xxx-spring-boot-starter完整pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"
2020-06-29