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{}是所有类型的类似基类的存在

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐