技术架构选型
Gin + GORM + go‑redis + zap日志 + validator + JWT + MySQL8 + Redis + go‑pay/gopay
| 组件 | 是什么 | 作用 |
|---|---|---|
| Gin | Web http 框架 | 接收 http 接口请求,路由、中间件 |
| GORM | ORM 库 | 操作 MySQL8,写数据库不用原生 SQL |
| go‑redis | Go 客户端 SDK,github.com/redis/go-redis/v9 |
代码层面,用来连接、读写 Redis 服务的库 |
| zap | 日志库 | 打印结构化日志,输出文件 /stdout |
| validator | 参数校验库 | 校验 http 入参结构体 |
| JWT | 鉴权组件 | 生成 / 解析 token,登录鉴权 |
| MySQL8 | 数据库服务 | 主业务存储(订单、用户) |
| Redis | 独立缓存服务(docker 部署) | 缓存、计数器、限流、黑名单、会话存储 |
Go
// 支持支付宝、微信支付;支持**公钥模式、证书模式**,回调通知验签、退款、查询、转账等功能
go get github.com/go-pay/gopay
安装本地开发环境

- 查询golang版本号
Go
go version
// go version go1.27.1 windows/amd64
运行你的第一个golang程序
main.go
Go
package main
import ("fmt")
func init() {
// 使用 fmt 包的 Println 函数输出 "init" 字符串
fmt.Println("init")
}
// main 是程序的入口函数
func main() {
// 使用 fmt 包的 Println 函数输出 "Hello, Services!" 字符串
fmt.Println("Hello, Services!")
}
运行....
bash
PS C:\src\go\src\bases> go build
PS C:\src\go\src\bases> .\bases.exe
init
Hello, Services!
PS C:\src\go\src\bases> go clean
PS C:\src\go\src\bases> go build
PS C:\src\go\src\bases> go clean
PS C:\src\go\src\bases> go run .\main.go
init
Hello, Services!
go env用于打印Go语言的环境信息。
go run命令可以编译并运行命令源码文件。
go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。
go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。go install用于编译并安装指定的代码包及它们的依赖包。
go clean命令会删除掉执行其它命令时产生的一些文件和目录。
报错
bash
PS C:\src\go\src\bases> go run .\main.go
main.go:7:2: cannot find package "bases/hello" in:
C:\src\go\src\bases\hello
main.go:4:2: found packages hello (hello.go) and main (main.go) in C:\src\go\src\bases
报错原因分析
-
同一个目录下不能有不同的包:
报错信息
found packages hello (hello.go) and main (main.go) in C:\src\go\src\bases明确指出,在bases目录下,hello.go声明了包名为hello,而main.go声明了包名为main。在 Go 中,同一个目录下的所有.go文件必须属于同一个包。Go 编译器在加载包时发现冲突,直接报错。 -
相对路径导入和重复导入:
你的代码中import hello "bases"和_ "./hello"试图导入同一个逻辑包,且使用了相对路径./hello,这在现代 Go(Go 1.16+)中是不被推荐的,且容易引发路径解析混乱。
解决方案
将 hello 包移到子目录中(推荐,符合 Go 工程规范)
如果你希望 hello 是一个独立的包供 main 调用,它必须拥有自己独立的目录。
基础知识
main 和 init 函数的区别
相同点:
两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
不同点:
init可以应用于任意包中,且可以重复定义多个。
main函数只能用于main包中,且只能定义一个。
下划线的使用场景
- 不需要把整个包都导入进来,仅仅是是希望它执行init()函数。
*2.*占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值。
Go
f, _ := os.Open("test.txt")
defer f.Close()
buf := make([]byte, 1024)
for {
n, _ := f.Read(buf)
if n == 0 {
break
}
os.Stdout.Write(buf[:n])
}
定义变量方式
Go
func main() {
hello.Print()
var name string = "ms"// 规范命名
var age = "18"// 类型推导
sex := "man"// 短变量声明
fmt.Println(name, age, sex)
}
基本类型
-
数字:整型(byte/rune/int32/int64)、浮点型(float32/float64)
-
bool布尔值、string字符串、array数组、struct结构体、引用类型(slice/map/channel)、interface接口、function函数
注意:在 Go 语言中,不推荐使用下划线命名法 (如 struct_name)。官方推荐使用驼峰命名法(CamelCase),并且首字母小写表示该变量仅在当前包内可见
替换字符串
Go
func main() {
hello.Print()
s1 := "ii"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'H'
fmt.Println(string(byteS1))
s2 := "博客"
// s1 := s2[1:]
s4 := string([]rune(s2)[1:])
s3 := "狗狗" + s4
fmt.Println(s3)
}
关于赋值
Go
func main() {
// := 用于声明并赋值,左边必须有新变量。
// = 用于单纯赋值,左边必须全是已声明的变量。
s1 := "ii"
s2 := "博客"
s1 = s2
fmt.Println(s1)
}
结构体Struct
Go
func main() {
structName := struct {
name string
age int
}{name: "李四", age: 20}
fmt.Println(structName.name, structName.age)
dd := [...]struct {
name string
age int
}{
{"张三", 18},
{"李四", 20},// 别忘了最后一行的逗号。
}
for _, v := range dd {
fmt.Println(v.name, v.age)
}
}
数组Array
Go
func main() {
// 一维数组
// var s1 = [...]int{1, 2, 3}
s1 := [...]int{1, 2, 3}
for _, v := range s1 {
fmt.Println(v)
}
// 二维数组
s2 := [...][2]int{{1, 2}, {3, 4}, {5, 6}}
for _, v := range s2 {
fmt.Println(v)
}
var struct_name = [2]struct {
name string
age int
}{{name: "张三", age: 18}, {name: "李四", age: 20}}
for key, item := range struct_name {
fmt.Println(key, item.name, item.age)
}
}
切片Slice
- \[\] T 切片;N T 数组
- slice 三要素:指针、len、cap
- 切片只是窗口,底层数组才存数据
- 同数组切出来的切片,共享内存
- append:满了就扩容,换新数组,返回新切片
- copy:复制元素,不自动扩容目标
- 传参:值拷贝 slice 头;修改原有元素外部可见;扩容后外部不可见
- nil 切片 len cap 都是 0,可以 append
Go
func main() {
// [] T 切片;[N] T 数组
// 要理解 Go 语言的切片,最直观的方法就是把它想象成餐厅的桌子:
// 长度:当前已经坐在桌子上吃饭的人数。
// 容量:这张桌子最多能坐下的总人数(包含正在吃和空着的位子)。
// s := make([]int, len, cap) // len 表示长度,cap 表示容量
s := make([]int, 5, 6)
s[0] = 1
s[1] = 1
s[2] = 1
s[3] = 1
s[4] = 1
// // 如果你一开始只确定了部分长度,后续需要动态增加元素,必须使用内置函数 append,而不能直接用索引赋值。
// // 当 append 发现原来的容量不够用时,它会自动在内存中寻找一块更大的空间,
// // 把原来桌子上的所有人(旧数据)复制过去,然后再把新客人加进去。这就是所谓的扩容。
s = append(s, 3, 4, 5)
fmt.Println(s)// [1 1 1 1 1 3 4 5]
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:3] // [2,3] len=2 cap=4
s2 := append(s1, 99) // 同数组切出来的切片,共享内存,修改一个会影响另一个。
s3 := make([]int, 10)
copy(s3, s2) //copy 用来切断共享底层数组,单独一份数据。
s3 = append(s3, 88)
fmt.Println(s1, s2, s3, arr) // [2 3] [2 3 99] [2 3 99 0 0 0 0 0 0 0 88] [1 2 3 99 5]
}
指针
Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。
&:取地址符。&a→ 获取变量a的内存地址*:解引用符。*p→ 根据指针 p 里存的地址,取出对应内存的值
Go
func main() {
var a int = 20 // 声明实际变量
var ip *int // 声明指针变量
ip = &a // 指针变量的存储地址
fmt.Printf("a 变量的地址是: %x\n", &a)
// 指针变量的存储地址
fmt.Printf("ip 变量储存的指针地址: %x\n", ip)
// 使用指针访问值
fmt.Printf("*ip 变量的值: %d\n", *ip)
b := 10
p := new(int)
p = &b
*p = 100
fmt.Println(*p, b)
}