golang采坑

2023年4月8日12:00:28

选择golang开发的几个理由

  1. 语法简单,类C语法,但是又不是全c风格,刚开始会有些不适应
  2. 部署简单,直接编译成二进制文件,直接部署
  3. 高性能,很多互联网项目需要考虑的
  4. 近些年,go的社区基金会都是大厂,未来发展问题不大
  5. 国内外很多项目开始采用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{}是所有类型的类似基类的存在

posted on 2023-05-20 16:49  zh7314  阅读(234)  评论(0编辑  收藏  举报