golang采坑
2023年4月8日12:00:28
选择golang开发的几个理由
- 语法简单,类C语法,但是又不是全c风格,刚开始会有些不适应
- 部署简单,直接编译成二进制文件,直接部署
- 高性能,很多互联网项目需要考虑的
- 近些年,go的社区基金会都是大厂,未来发展问题不大
- 国内外很多项目开始采用go重写java,php,python等,需要高性能,部署方便,低系统消耗的项目更新迭代
开发环境基础配置
下载 golang
https://studygolang.com/dl
包网站
https://pkg.go.dev/
windows配置go的环境变量
windows的环境变量配置方式:
我的电脑-属性-高级系统设置-环境变量-系统变量
在PATH
里加入 C:\Program Files\Go\bin
这个是安装目录
GOROOT
变量名:GOROOT
变量值:代码项目目录,如比 D:\go
建立 src
,pkg
,bin
# 查看go的环境变量
go env
还需要再在系统环境变量PATH里面追加一条记录为
%GOROOT%\bin
保存成功后打开windows的命令行,输入以下命令可以显示go语言版本
go version
GOPATH
GOPATH 通常是有默认值的,删除或者重新配置就可以。
变量名:GOPATH
变量值:Go语言程序的工作目录
常规编译
go build .
静态编译
常规编译后的包还需要依赖部署环境的支持,静态编译出的文件可以任意放到指定平台下运行,不需要环境配置。
go build --ldflags "-extldflags -static" -o main .
交叉编译
编译有平台区分,需要根据部署情况,选择匹配的编译方式。
// 编译 Linux 环境
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build .
// 编译 Windows 环境
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build .
// 编译 Mac 环境
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build .
设置代理
类 Unix
在 Linux 或 macOS 上面,需要运行下面命令(或者,可以把以下命令写到 .bashrc 或 .bash_profile 文件中):
# 启用 Go Modules 功能
go env -w GO111MODULE=on
# 配置 GOPROXY 环境变量,以下三选一
# 1. 七牛 CDN
go env -w GOPROXY=https://goproxy.cn,direct
# 2. 阿里云
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
# 3. 官方
go env -w GOPROXY=https://goproxy.io,direct
确认一下:
$ go env | grep GOPROXY
GOPROXY="https://goproxy.cn"
Windows
在 Windows 上,需要运行下面命令:
# 启用 Go Modules 功能
$env:GO111MODULE="on"
# 配置 GOPROXY 环境变量,以下三选一
# 1. 七牛 CDN
$env:GOPROXY="https://goproxy.cn,direct"
# 2. 阿里云
$env:GOPROXY="https://mirrors.aliyun.com/goproxy/,direct"
# 3. 官方
$env:GOPROXY="https://goproxy.io,direct"
测试一下
$ time go get golang.org/x/tour
# windows命令行修改
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct
windows环境变量
读取特殊的环境变量
通过环境变量读取Windows操作系统的安装路径,和默认应用程序的安装路径。
PS> $env:windir
C:\Windows
PS> $env:ProgramFiles
C:\Program Files
通过$env:,这就提示powershell忽略基本的variable:驱动器,而是去环境变量env:驱动器中寻找变量。为了和其它变量保持一致,powershell环境变量也可以象其它变量那样使用。比如你可以把它插入到文本中。
PS> "My computer name $env:COMPUTERNAME"
My computer name MYHome-test-01
查找环境变量
Powershell把所有环境变量的记录保存在env: 虚拟驱动中,因此可以列出所有环境变量 。一旦查出环境变量的名字就可以使用$env:name 访问了。
PS> ls env:
Name Value
---- -----
ALLUSERSPROFILE C:\ProgramData
APPDATA C:\User\sv-test\Home\AppData\Roaming
CommonProgramFiles C:\Program Files\Common Files
COMPUTERNAME MYHome-test-01
ComSpec C:\Windows\system32\cmd.exe
FP_NO_HOST_CHECK NO
HOMEDRIVE C:
HOMEPATH Users\v-test\Home
创建新的环境变量
创建新环境变量的方法和创建其它变量一样,只需要指定env:虚拟驱动器即可
PS> $env:TestVar1="This is my environment variable"
PS> $env:TestVar2="Hollow, environment variable"
PS> ls env:Test*
Name Value
---- -----
TestVar1 This is my environment variable
TestVar2 Hollow, environment variable
删除和更新环境变量
在powershell删除和更新环境变量和常规变量一样。例如要删除环境变量中的 windir,
PS> del env:windir
PS> $env:windir
PS>
可以更新环境变量$env:OS 为linux redhat。
PS> $env:OS
Windows_NT
PS> $env:OS="Redhat Linux"
PS> $env:OS
Redhat Linux
这样直接操作环境变量,会不会不安全?事实上很安全,因为$env:中的环境变量只是机器环境变量的一个副本,即使你更改了它,下一次重新打开时,又会恢复如初。(.NET方法更新环境变量除外)
我们可以将受信任的文件夹列表追加到环境变量的末尾,这样就可以直接通过相对路径执行这些文件下的文件或者脚本,甚至省略扩展名都可以。
PS> md .myscript
Directory:
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2011/11/29 18:20 myscript
PS> cd .myscript
PSmyscript> "write-host 'Hollow , Powershell'" > hollow.ps1
PSmyscript> .hollow.ps1
Hollow , Powershell
PSmyscript> cd ..
PS> $env:Path+=";C:PowerShellmyscript"
PS> hollow.ps1
Hollow , Powershell
PS> hollow
Hollow , Powershell
环境变量更新生效
上述对于环境变量的操作只会影响当前powershell会话,并没有更新在机器上。
.NET方法[environment]::SetEnvironmentvariable操作可以立刻生效。
下面的例子对当前用户设置环境变量,经测试,重新打开powershell仍然存在
PS> [environment]::SetEnvironmentvariable("Path", ";c:\powershellscript", "User")
PS> [environment]::GetEnvironmentvariable("Path", "User")
;c:\powershellscript
一些常用命令
go help: 查看帮助文档。
go help build
go build: 对源代码和依赖的文件进行打包,生成可执行文件。
go build -o my_first_go_exe entrance_class/demo.go
go install: 编译并安装包或依赖,安装到$GOPATH/bin下。
go install entrance_class/demo.go
go get: 把依赖库添加到当前module中,如果本机之前从未下载过则先下载。
go get github.com/tinylib/msgp
以上命令会在$GOPATH/pkg/mod目录下会生成github.com/tinylib/msgp目录。
go install github.com/tinylib/msgp@latest
以上命令会在$GOPATH/bin下生成msgp可执行文件。
go mod init module_name
初始化一个Go项目。
go mod tidy通过扫描当前项目中的所有代码来添加未被记录的依赖至go.mod文件或从go.mod文件中删除不再被使用的依赖。
go run: 编译并运行程序。
go test: 执行测试代码。
go tool: 执行go自带的工具。go tool pprof对cpu、内存和协程进行监控;go tool trace跟踪协程的执行过程。
go vet: 检查代码中的静态错误。
go fmt: 对代码文件进行格式化,如果用了IDE这个命令就不需要了。
go fmt entrance_class/demo.go
go doc: 查看go标准库或第三方库的帮助文档。
go doc fmt
go doc gonum.org/v1/gonum/stat
go version: 查看go版本号。
go env: 查看go环境信息。
go mod download 下载当前项目的依赖包
go mod edit 编辑go.mod文件
go mod graph 输出模块依赖图
go mod init 在当前目录初始化go mod
go mod tidy 清理项目模块
go mod verify 验证项目的依赖合法性
go mod why 解释项目哪些地方用到了依赖
go clean -modcache 用于删除项目模块依赖缓存
go list -m 列出模块
web框架选择
框架名称 | 热度 | 推荐理由 |
---|---|---|
gin | 很高 | 很强大,性能不错,使用人很多,文档很完善 |
beego | 不错 | 国内布道go的框架,使用人很多,文档很完善 |
echo | 不错 | 类beego框架,使用人很多,文档很完善 |
Iris | 不错 | 高性能框架,使用人很多,文档很完善 |
goravel | 一般 | 借鉴PHP laravel框架设计,适合php转golang,文档很完善 |
goframe | 一般 | 借鉴JAVA spring框架设计,适合java转golang,文档很完善 |
其他框架微服务的框架后续补充 |
一些常见问题
标签(Tag)
结构体类型(Struct type)
Struct 是一系列字段。每个字段由可选名称和所需类型(源代码)组成:
package main
import "fmt"
type T1 struct {
f1 string
}
type T2 struct {
T1
f2 int64
f3, f4 float64
}
func main() {
t := T2{T1{"foo"}, 1, 2, 3}
fmt.Println(t.f1) // foo
fmt.Println(t.T1.f1) // foo
fmt.Println(t.f2) // 1
}
T1 域被称为嵌入字段,因为它是用类型声明但没有名称。
字段声明可以在 T2 中指定来自第 3 个字段声明的 f3 和 f4 之类的多个标识符。
语言规范声明每个字段声明后面跟着分号,但正如我们上面所见,它可以省略。如果需要将多个字段声明放入同一行(源代码),分号可能很有用(源代码):
package main
import "fmt"
type T struct {
f1 int64; f2 float64
}
func main() {
t := T{1, 2}
fmt.Println(t.f1, t.f2) // 1 2
}
标签(Tag)
字段声明后面可以跟一个可选的字符串文字(标记),它称为相应字段声明中所有字段的属性(单字段声明可以指定多个标识符)。让我们看看它的实际应用(源代码):
type T struct {
f1 string "f one"
f2 string
f3 string `f three`
f4, f5 int64 `f four and five`
}
可以使用原始字符串文字或解释的字符串文字,但下面描述的传统格式需要原始字符串文字。规范 中描述了原始字符串文字和解释字符串文字之间的差异。
如果字段声明包含多个标识符,则标记将附加到字段声明的所有字段(如上面的字段 f4 和 f5)。
反射(Reflection)
标签可通过 reflect 包访问,允许运行时反射(源代码):
package main
import (
"fmt"
"reflect"
)
type T struct {
f1 string "f one"
f2 string
f3 string `f three`
f4, f5 int64 `f four and five`
}
func main() {
t := reflect.TypeOf(T{})
f1, _ := t.FieldByName("f1")
fmt.Println(f1.Tag) // f one
f4, _ := t.FieldByName("f4")
fmt.Println(f4.Tag) // f four and five
f5, _ := t.FieldByName("f5")
fmt.Println(f5.Tag) // f four and five
}
设置空标记与完全不使用标记的效果相同(源代码):
type T struct {
f1 string ``
f2 string
}
func main() {
t := reflect.TypeOf(T{})
f1, _ := t.FieldByName("f1")
fmt.Printf("%q\n", f1.Tag) // ""
f2, _ := t.FieldByName("f2")
fmt.Printf("%q\n", f2.Tag) // ""
}
总结:使用反引号,包括的字符串成为标签tag,可以使用反射reflect获取到,在根据标签内容处理对应的代码逻辑
单引号
单引号在go语言中表示golang中的 rune(int32) 类型,单引号里面是单个字符,对应的值为改字符的 ASCII 值。
双引号
在go语言中双引号里面可以是单个字符也可以是字符串,双引号里面可以有转义字符,如 \n、\r 等,对应go语言中的 string 类型。
反引号
反引号中的字符表示其原生的意思,在单引号中的内容可以是多行内容,不支持转义。
=
和 :=
有什么区别
=
是赋值, :=
是声明变量并赋值
:=
是用于未被定义过的变量,编译器自动进行右值推导定义并赋值
=
是用于给变量赋值,这个被赋值的变量一定要是一个已经被定义过的变量,否则会报错
mystr := "hello world"
它等同于:
var mystr string
mystr = "hello world"
零值
官方文档中零值称为zero value,零值并不仅仅只是字面上的数字零,而是一个类型的空值或者说默认值更为准确。
类型 | 零值 |
---|---|
数字类型 | 0 |
布尔类型 | false |
字符串类型 | "" |
数组 | 固定长度的对应类型的零值集合 |
结构体 | 内部字段都是零值的结构体 |
切片,映射表,函数,接口,通道,指针 | nil |
nil
源代码中的nil,可以看出nil仅仅只是一个变量。
var nil Type
Go中的nil并不等同于其他语言的null,nil仅仅只是一些类型的零值,并且不属于任何类型,所以nil == nil这样的语句是无法通过编译的。
iota
iota是一个内置的常量标识符,通常用于表示一个常量声明中的无类型整数序数,一般都是在括号中使用。
const iota = 0
先看几个例子,看看规律。
const (
Num = iota // 0
Num1 // 1
Num2 // 2
Num3 // 3
Num4 // 4
)
也可以这么写
const (
Num = iota*2 // 0
Num1 // 2
Num2 // 4
Num3 // 6
Num4 // 8
)
包 package module
这个和java的包概念类似,又有一点不一样
包的根名称是在 go mod init module_name 的module_name在go.mod里面的 module module_name决定的
module shop
go 1.20
module关键字声明了当前项目的模块名,一个go.mod文件中只能出现一个module关键字。例子中的
module golearn
代表着当前模块名为golearn,例如打开Gin依赖的go.mod文件可以发现它的module名
module github.com/gin-gonic/gin
Gin的模块名就是下载依赖时使用的地址,这也是通常而言推荐模块名格式,域名/用户/仓库名
公共方法和私有方法
对外暴露的函数和变量可以被包外的调用者导入和访问,如果是不对外暴露的话,那么仅包内的调用者可以访问,外部将无法导入和访问,该规则适用于整个Go语言,例如后续会学到的结构体及其字段,方法,自定义类型,接口等等。
错误范返回
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0不能作为被除数")
}
return a / b, nil
}
go常用的参数
Go 测试有着非常多的标志参数,下面只会介绍常用的参数,想要了解更多细节建议使用go help testflag命令自行查阅。
参数 | 释义 |
---|---|
-o file | 指定编译后的二进制文件名称 |
-c | 只编译测试文件,但不运行 |
-json | 以json格式输出测试日志 |
-exec xprog | 使用xprog运行测试,等价于go run |
-bench regexp | 选中regexp匹配的基准测试 |
-fuzz regexp | 选中regexp匹配的模糊测试 |
-fuzztime t | 模糊测试自动结束的时间,t为时间间隔,当单位为x时,表示次数,例如200x |
-fuzzminimizetime t | 模式测试运行的最小时间,规则同上 |
-count n | 运行测试n次,默认1次 |
-cover | 开启测试覆盖率分析 |
-covermode set,count,atomic | 设置覆盖率分析的模式 |
-cpu | 为测试执行GOMAXPROCS |
-failfast | 第一次测试失败后,不会开始新的测试 |
-list regexp | 列出regexp匹配的测试用例 |
-parallel n | 允许调用了t.Parallel的测试用例并行运行,n值为并行的最大数量 |
-run regexp | 只运行regexp匹配的测试用例 |
-skip regexp | 跳过regexp匹配的测试用例 |
-timeout d | 如果单次测试执行时间超过了时间间隔d,就会panic。d为时间间隔,例1s,1ms,1ns等 |
-shuffle off,on,N | 打乱测试的执行顺序,N为随机种子,默认种子为系统时间 |
-v | 输出更详细的测试日志 |
-benchmem | 统计基准测试的内存分配 |
-blockprofile block.out | 统计测试中协程阻塞情况并写入文件 |
-blockprofilerate n | 控制协程阻塞统计频率,通过命令go doc runtime.SetBlockProfileRate查看更多细节 |
-coverprofile cover.out | 统计覆盖率测试的情况并写入文件 |
-cpuprofile cpu.out | 统计cpu情况并写入文件 |
-memprofile mem.out | 统计内存分配情况并写入文件 |
-memprofilerate n | 控制内存分配统计的频率,通过命令go doc runtime.MemProfileRate查看更多细节 |
-mutexprofile mutex.out | 统计锁竞争情况并写入文件 |
-mutexprofilefraction n | 设置统计n个协程竞争一个互斥锁的情况 |
-trace trace.out | 将执行追踪情况写入文件 |
-outputdir directory | 指定上述的统计文件的输出目录,默认为go test的运行目录 |
needs pointer receiver ide有时候提示的信息不太正确 比如调用另一个struct的方法的时候会出现错误提示
name := "ssssss"
controllers.UserController.Zx(name)
提示错误是:
Not enough arguments in call to 'controllers.UserController.Zx'
Cannot use 'name' (type string) as the type (controllers.UserController, string) or controllers.UserController
但是运行 的错误是
.\main.go:16:29: invalid method expression controllers.UserController.Zx (needs pointer receiver (*controllers.UserController).Zx)
这个错误就比较明显是调用需要指针调用,下面这两种调用方法都可以
var userController *controllers.UserController
//userController := new(controllers.UserController)
userController.Zx(name)
new和make的区别
在前面的几节已经很多次提到过内置函数new和make,两者有点类似,但也有不同,下面复习下。
func new(Type) *Type
返回值是类型指针
接收参数是类型
专用于给指针分配内存空间
func make(t Type, size ...IntegerType) Type
返回值是值,不是指针
接收的第一个参数是类型,不定长参数根据传入类型的不同而不同
专用于给切片,映射表,通道分配内存。
下面是一些例子:
new(int) // int指针
new(string) // string指针
new([]int) // 整型切片指针
make([]int, 10, 100) // 长度为10,容量100的整型切片
make(map[string]int, 10) // 容量为10的映射表
make(chan int, 10) // 缓冲区大小为10的通道
new (T) 为每个新的类型 T 分配一片内存,初始化为 0 并且返回类型为 * T 的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体(参见第 10 章);它相当于 &T{}。
make(T) 返回一个类型为 T 的初始值,它只适用于 3 种内建的引用类型:切片、map 和 channel
换言之,new 函数分配内存,make 函数初始化;
- 切片、映射和通道,使用 make
- 数组、结构体和所有的值类型,使用 new
出于性能考虑的最佳实践和建议
(1)尽可能的使用 := 去初始化声明一个变量(在函数内部);
(2)尽可能的使用字符代替字符串;
(3)尽可能的使用切片代替数组;
(4)尽可能的使用数组和切片代替映射(详见参考文献 15);
(5)如果只想获取切片中某项值,不需要值的索引,尽可能的使用 for range 去遍历切片,这比必须查询切片中的每个元素要快一些;
(6)当数组元素是稀疏的(例如有很多 0 值或者空值 nil),使用映射会降低内存消耗;
(7)初始化映射时指定其容量;
(8)当定义一个方法时,使用指针类型作为方法的接受者;
(9)在代码中使用常量或者标志提取常量的值;
(10)尽可能在需要分配大量内存时使用缓存;
(11)使用缓存模板(参考 章节 15.7)。
获取变量类型
var a int
typeOfA := reflect.TypeOf(a)
fmt.Println(typeOfA.Name(), typeOfA.Kind())
类型强制转换
go中的强制类型转换为:
type(v)
var a int32 = 10
var b int64 = int64(a)
var c float32 = 12.3
var d float64 =float64(c)
类型断言
类型断言就是将接口类型的值(x),转换成类型(T)。格式为:x.(T);
类型断言的必要条件就x是接口类型,非接口类型的x不能做类型断言;
T可以是非接口类型,如果想断言合法,则T必须实现x的接口;
T也可以是接口,则x的动态类型也应该是接口T;
类型断言如果非法,运行时会导致错误,为了避免这种错误,应该总是使用下面的方式来进行类型断言
引发的错误 cannot use res (variable of type any) as string value in argument to url.QueryUnescape: need type assertion
// 进行类型的断言的变量必须是空接口
var x interface{}
x =100
value1,ok :=x.(int)
if ok {
fmt.Println(value1)
}
init基本概念
Go语言里的init函数有如下特点:
- init函数没有参数,没有返回值。如果加了参数或返回值,会编译报错。
- 一个package下面的每个.go源文件都可以有自己的init函数。当这个package被import时,就会执行该package下的init函数。
- 一个.go源文件里可以有一个或者多个init函数,虽然函数签名完全一样,但是Go允许这么做。
- .go源文件里的全局常量和变量会先被编译器解析,然后再执行init函数。
我们来看如下的代码示例:
package main
import "fmt"
func init() {
fmt.Println("init")
}
func init() {
fmt.Println(a)
}
func main() {
fmt.Println("main")
}
var a = func() int {
fmt.Println("var")
return 0
}()
go run main.go执行这段程序的结果是:
var
init
0
main
golang windows多版本管理
下载地址: https://github.com/voidint/g/releases
如果已经安装golang,先去系统环境变量PATH去掉 go的bin目录
下载g1.5.0.windows-arm64.zip 解压到 D:\gobin
\g.exe
在 D:\gobin
或者你的目录
系统环境变量PATH 追加上 D:\gobin
查询当前可供安装的stable状态的go版本
$ g ls-remote stable
1.13.15
1.14.7
安装目标go版本1.14.7
$ g install 1.14.7
Downloading 100% |███████████████| (119/119 MB, 9.939 MB/s) [12s:0s]
Computing checksum with SHA256
Checksums matched
Now using go1.14.7
查询已安装的go版本
$ g ls
1.7.6
1.11.13
1.12.17
1.13.15
1.14.6
* 1.14.7
gorm工具 Gen Tool
官方地址: https://gorm.io/zh_CN/gen/gen_tool.html
安装:go install gorm.io/gen/tools/gentool@latest
gentool -dsn "root:root@tcp(localhost:3306)/jump?charset=utf8mb4&parseTime=True&loc=Local" -outPath "./" fieldNullable "true" fieldWithIndexTag "true" fieldWithTypeTag "true"
gentool -dsn "root:root@tcp(localhost:3306)/jump?charset=utf8mb4&parseTime=True&loc=Local" fieldNullable "true" fieldWithIndexTag "true" fieldWithTypeTag "true"
jump
是数据库名称,注意:生成的时候,注意是双层目录,会在当前目录 xxxx.gen.go 在上一层目录会生产model
在上一层目录 model/xxxx.gen.go
模型代码如下:
package model
import (
"time"
)
const TableNameAPILog = "api_log"
// APILog mapped from table <api_log>
type APILog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
CreateAt time.Time `gorm:"column:create_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"create_at"`
UpdateAt time.Time `gorm:"column:update_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"update_at"`
Method string `gorm:"column:method;comment:请求方式" json:"method"`
RequestIP string `gorm:"column:request_ip;comment:请求ip" json:"request_ip"`
RequestURL string `gorm:"column:request_url;comment:请求url" json:"request_url"`
QueryParams string `gorm:"column:query_params;comment:请求参数" json:"query_params"`
ResponseData string `gorm:"column:response_data;comment:返回的数据 不包含data" json:"response_data"`
}
// TableName APILog's table name
func (*APILog) TableName() string {
return TableNameAPILog
}
变量覆盖
结论是会覆盖
func test1(name string) (res string, err error) {
return name, errors.New("test1 error")
}
func test2(name string) (res string, err error) {
return name, errors.New("test2 error")
}
func main() {
name1, err := test1("name1 error")
fmt.Println(name1)
fmt.Println(err)
name2, err := test2("name2 error")
fmt.Println(name2)
fmt.Println(err)
}
//结果
PS D:\go\src\shop> go run .
name1 error
test1 error
name2 error
test2 error
获取获取函数名、文件名、行号
func rr() {
funcName, file, line, ok := runtime.Caller(0)
if ok {
fmt.Println("func name: " + runtime.FuncForPC(funcName).Name())
fmt.Printf("file: %s\n", file)
fmt.Printf("line: %d\n", line)
}
}
any类型
type any = interface{}
注意: any类型返回的数据是需要说过类型断言转换数据,建议使用 interface{} 因为interface{}是所有类型的类似基类的存在
QQ二群 166427999
博客文件如果不能下载请进群下载
如果公司项目有技术瓶颈问题,请联系↓↓
如果需要定制系统开发服务,请联系↓↓
技术服务QQ: 903464207