特别说明:本笔记基于 Go 1.16 版本,根据 astaxie 的 web 学习教程:build-web-application-with-golang
golang 中国官网 下载,然后正常安装, cmd
检查是否加入系统环境变量中。
从 Go 1.12 开始,使用 Go Module 替换了 GOPATH 的用法,大大优化了包管理。这里只介绍最新的 Go Module 模式:
默认不开启,查看配置
go env
go env 命令需要 Go版本 >= 1.13
手动开启
go env -w GO111MODULE=on
由于科学上网挺麻烦,所以需要先设置本地代理
命令行设置:
go env -w GOPROXY=https://goproxy.io
其他代理地址:
https://goproxy.cn
https://mirrors.aliyun.com/goproxy/
一定要接项目名
go mod init [module 名称]
go mod tidy
go get -v github/com/go-ego/[email protected]
go get -u
更新指定包依赖:
go get -u github.com/go-ego/gse
指定版本:
go get -u github/com/go-ego/[email protected]
go mod download
go mod vendor
go mod edit
go mod graph
go mod verify
go mod edit -replace github.com/go-ego/gse=/path/to/local/gse
go mod edit -replace github.com/go-ego/gse=github.com/vcaesar/gse
作用是将某些不能访问的包,使用本地包文件或者别的包地址替换
用于安装你获取的包,存放的路径: $GOPATH/pkg
项目文件源码都是放在 $GOPATH/src/
中,以文件的方式存在,允许多个文件嵌套归纳,如 github.com/astaxie/beedb
注意:以上是 GOPATH 模式的传统结构,如果使用 Go Module ,则项目不能放在 GOPATH 路径下,否则编译会报错,Go Module 模式下,项目文件可以随意在哪
go mod init mymath
go mod tidy
这样就创建了一个 Module,会在本地生成 go.mod 依赖文件,可以被其他项目调用。
**重要提醒:**在 Goland 设置中,将Go Module 设置中的 Enable Go Modules integration
打开,这样才会有包补全提示
go mod init mathapp
mathapp/go.mod
本地包导入 replacerequire mymath v0.0.0
replace (
mymath v0.0.0 => ../mymath
)
go mod tidy
go build
mathapp/mathapp.exe
提醒新手,这里一定有main.go 文件,并且注意,package main ,不是默认的文件夹名,不然 go build 不会起作用
编译代码
$GOPATH/pkg
下生成对应文件,请使用 go install
$GOPATH/bin
下生成相应文件,需要使用 go install
.go
文件,可以在 go build
后面接文件名来指定,无则默认编译所有 go 文件_
和 .
开头的 go 文件移除当前源码包和关联源码包里面编生成的文件:
_obj/ 旧的object目录,由Makefiles遗留
_test/ 旧的test目录,由Makefiles遗留
_testmain.go 旧的gotest文件,由Makefiles遗留
test.out 旧的test记录,由Makefiles遗留
build.out 旧的test记录,由Makefiles遗留
*.[568ao] object文件,由Makefiles遗留
DIR(.exe) 由go build产生
DIR.test(.exe) 由go test -c产生
MAINFILE(.exe) 由go build MAINFILE.go产生
*.so 由 SWIG 产生
一般用于版本控制提交前使用,具体参数:
-i
清除关联的安装的包和可运行文件,也就是通过go install安装的文件-n
把需要执行的清楚命令打印出来,但是不执行-r
循环的清除在import中引入的包-x
打印出执行的详细命令,和 -n
的不同是会执行命令格式化,用法 go fmt xx.go
,大部分开发工具都会在保存时自动调用。
动态获取远程代码包,在内部其实分为两步:
go install
具体参数:
-d
只下载,不安装-u
强制使用网络去更新包和它的依赖包-v
显示执行的命令-t
同时也下载需要为运行测试所需要的包-f
只在包含 -u
参数时有效,不去验证 import 中每一个都已经获取,这对本地 fork 的包特别有用在内部分为两步:
.a
包)$GOPATH/pkg
或者 $GOPATH/bin
下参数:
-v
打印具体执行的信息执行这个命令,会自动读取源码目录下的所有 xxx_test.go
文件,生成并运行测试用的可执行文件。
参数:
-v
显示测试的详细命令go tool fix
用于修复以前的老版本代码至新版本go tool vet directory/files
用来分析当前的代码是否是正确代码这个命令是 Go 1.4 之后开始设计的,用于在编译前自动化生成某类代码。
需要安装:
go get golang.org/x/tools/cmd/godoc
使用:
生成本地在线文档:
godoc -http:localhost:6060
命令行查看:
go doc builtin
go doc net.http // 查看包
go doc fmt.Printf // 查看函数
go doc -src fmt.Printf // 查看源码
运行 main 包程序
var num int
var total, num int
var total int = 1000
var total, num int = 1000, 10
var studentNum, className = 52, "高一三班"
studentNum, className := 52, "高一三班"
_
是特殊的变量名,任何赋予它的值都将被丢弃
a, _ := 3, 9
已申明但是未使用的变量,在编译时会报错
不可改变的值,可以定义为数值、布尔值、字符串等类型
const studentName = "Xiao Ming"
const Pi float32 = 3.1415926
布尔值,类型为 bool
,值为 true
或 false
,默认 false
整数类型分为两大类:
所有类型:
rune
int32
int8
byte
字节int16
int64
uint8
uint16
uint32
uint64
注意:
8
16
都是指的位数,即 bit
,一个字节是 1 byte = 8 bit
,所以 int8
和 byte
是同个意思;int
类型,但和 int32
不是同一种类型,所有不同类型的整数之间是不能进行运算的,不然编译会报错,例如:func main() {
var num1 int8 = 12
var num2 int32 = 2333
numSum := num1 + num2
}
// invalid operation: num1 + num2 (mismatched types int8 and int32)
浮点数分为两种:
默认类型是 complex128
,64位实数+64位虚数,其他类型有: complex64
var c complex64 = 5+5i
fmt.Printf("Value is: %v", c)
类型为 string
,可以通过双引号 ""
或者单引号 ''
来定义。
var studentName string = "Xiao Hong"
var className string = '初三二班'
字符串可以像数组一样进行切片获取
hostName := "www.rungolf.com"
fmt.Println(hostName[4:11])
// rungolf
fmt.Println(hostName[4:])
// rungolf.com
fmt.Println(hostName[:11])
// www.rungolf
字符串是不可变值,即使它可以被切片获取,但是不能进行赋值操作
var hostName string = "www.rungolf.com"
fmt.Println(string(hostName[0]))
// w
hostName[0] = 'h'
// cannot assign to hostName[0] (strings are immutable)
如果非要修改,则可以通过转为 []byte
,通过数组修改后再转回来。
字符串可以通过 +
连接,这和大部分语言一致:
firstName := "L"
secondName := "W"
myName := firstName + secondName
多行字符串
rows := `hello
world`
类型为 error
,Go 中专门有一个处理错误的包 errors
func main() {
err := errors.New("This is a customer error defind!")
if err != nil {
fmt.Println(err)
}
}
数组
var arr [n]type
var arr [2]int
arr[0] = 1
arr[1] = 2
数组是不能改变长度的,数组之间的赋值是值的赋值,像整数一样,而不是引用地址赋值,当作为形参传入函数时,是数组的副本,而不是传入指针。
**
初始化方式定义数组:
arr1 := [3]int{1, 2, 3}
// 部分有初始化值
arr2 := [5]int{1, 11}
// 自动根据初始化个数计算数组长度
arr3 := [...]int{1, 12, 113}
多维数组定义:
arr := [2, 3]int{{1, 2, 3}, {11, 22, 33}}
切片,相比较 array
,长度是可变的动态数组,它是引用类型
// 1.
var s []int
// 2.
s := []int{1, 2, 4}
注意 slice
是一个指向底层 array
的引用,即它的值其实是一个指针,当长度动态变化时,他会指向新的一个 array
。
从概念上来说 slice
更像是一个结构体,它的结构由三部分组成:
slice
开始的位置slice
从开始位置到指向的数组末端的长度arr := [6]int{1, 2, 3, 4, 5, 6}
s := arr[1:3]
以上例子 s 的打印值为 [2, 3]
,s 的长度为 2, s的最大长度 cap 为 5。
手动设置最大长度:
s := arr[1:3:4] // 第三位表示最大容量计算结束位置
最大容量为 4-1 = 3
字典,格式 map[indexType]valueType
,示例:
var numbers map[string]int
numbers := make(map[string]int)
注意:
map
是无序的,即每次打印出来都可能不一样,因为他的下标是无序的;map
也是可变长度,即和 slice
一样是引用类型;map
和其他基本类型不同,它不是线程安全,在使用 go-routine
存取时,需要使用 mutex lock
机制;rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2}
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the key list of rating")
}else {
fmt.Println("is not in")
}
// 删除元素
delete(rating, "C")
make
是用于内建类型( map
slice
channel
) 的内存分配, new
用于各种类型的内存分配。
new(T)
分配了一个零值填充额 T
类型的空间,并返回其地址,即一个 *T
类型的值,也是通常意义上说的指针。
make(T, args)
是返回一个有初始值(非零)的 T
类型,原因是指向数据结构的引用在使用前必须初始化,返回一个初始化后的(非零)的值。
if x < 10 {
fmt.Println("x is less than 10")
}
附带局部变量定义
if x := computeVaule(); x < 10 {
fmt.Println("x is less than 10")
}
fmt.Println(x)
// 最后的打印会报错,因为x只在流程控制局部内有效,是一个局部变量
这里的局部变量声明可以减少内存的消耗,流程结束局部变量占用的内存就会被销毁
谨慎使用,会导致执行顺序变得很复杂
func myFunc() {
i := 0
Here:
println(i)
i++
goto Here
}
标签名大小写敏感
Go 的for非常灵活,可以简略到和 while
一样的写法
sum := 0
for index := 0; index < 10; index ++ {
sum += index
}
fmt.Println("index sum is ", sum)
// index sum is 45
index := 0
for ; index < 10; {
index ++
}
index := 0
for index < 10 {
index ++
}
配合 range
读取 slice
和 map
s := []int{1, 2, 3, 4}
for k, v := range s {
fmt.Println(k, v)
}
// 0 1
// 1 2
// 2 3
// 3 4
i := 10
switch i {
case 1:
fmt.Println("i is 1")
case 2, 4, 10:
fmt.Println("i is in 1, 2, 10")
default:
fmt.Println("i is not know")
}
注意几个不同的地方:
case
语句结束后,默认会 break
不会往下执行,如果需要往下,则使用 fallthrough
case
后面可以用逗号分隔,同时判断几个值通过 func
开头来定义
需要注意的几点:
return
具体值,而定义了返回变量,只需要 return
关键字就行func helloWorld() (output string) {
output = "hello world"
return
}
()
func helloWorld() string {
return "string"
}
func helloWorld(a, b int) string {
}
func helloWorld(a string, b, c int) string {
}
// 这里 b 为 int
即个数不定的参数,接收到的是一个 slice
func helloWorld(arg ...int) string {
return reflect.TypeOf(arg).Kind().String()
}
// slice
上面有介绍到一种变量类型的判断,再介绍一种用法 var.(type)
func helloWorld(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
}
注意:这里的 args
必须使用 interface{}
类型,因为 arg.(type)
对变量 arg
的要求必须是 interface{}
类型。这种使用方法仅限于配合 switch
使用,其他地方不起作用。
和上方这种方法相似的还有另一种单个判断类型的:
value, ok := element.(T)
注意:这里的 element
必须是 interface{}
类型,例如:
func main() {
var element interface{}
element = []int{1, 2, 3}
sValue, ok := element.([]int)
fmt.Println(sValue, ok)
}
&x
*x
slice map channel
的实现机制类似指针,所以在传递时可以不用进行取指针操作,但是注意 slice
,如果想在函数中修改 slice
的长度,依然需要取指针传递当函数执行到最后结束前,会按从后往前的顺序执行 defer
,常用于资源的关闭。
func hasRecords() bool {
f := file.Open("file Path")
defer f.Close()
if xxxx {
return false
}
return true
}
函数也可以通过 type
用变量来表示
func main() {
type funcName func(input int)(output string)
}
当其他定义的函数输入和输出类型、数量和 funcName
一致时,则默认它属于 funcName
类型,则在可根据情况传递不同的 funcName
类型变量,不用在传递后再判断一次情况。
package main
import (
"fmt"
"reflect"
)
type funcName func(x interface{}) bool
func main() {
userName := "lw"
if checkFormat(userName, isString) {
fmt.Println("user name pass")
} else {
fmt.Println("user name is wrong format")
}
verifyCode := 212123
if checkFormat(verifyCode, isInt) {
fmt.Println("verify code pass")
} else {
fmt.Println("verify code is wrong format, only number")
}
}
func isInt(x interface{}) bool {
return reflect.TypeOf(x).Kind().String() == "int"
}
func isString(x interface{}) bool {
return reflect.TypeOf(x).Kind().String() == "string"
}
func checkFormat(x interface{}, f funcName) bool {
return f(x)
}
这个举例不是很好,但可以参考使用方法
注意:你应当把这当做最后的手段,应该尽可能的减少使用它,虽然它很强大
panic
是一个内建函数,可以中断原有的控制流程,当函数发生 panic
中断,会继续调用延迟函数,然后返回至上一级函数,这个过程不断重复,直至整个程序退出。 panic
除了手动调用,也能是编译错误时触发,例如数组越界访问。recover
是一个内建函数,可以恢复 panic
中断的 goroutine
,并获得 panic
时的输入值。注意:仅在延迟函数中有效,程序正常执行时调用返回 nil
,没有其他任何效果。defer func(){
if x := recover(); x != nill {
// ...
}
}()
// 注意匿名函数最后需要()来表示执行
区别
main()
只能应用于 package main
,必须init()
可以应用于所有 package
,非必须共同点
注意:
package
可以存在多个init()
,但是为了代码可阅读性,建议只写一个一个正常的程序执行过程:
main
程序开始由上至下执行,导入依赖的包,同一个包只会导入一次,不断嵌套init()
,如果有的话。main
程序的常量和变量初始化,执行 init()
,最后执行 main()
一些省略写法:
import (
. "fmt"
)
Println("省略包名的写法")
import(
f "fmt"
)
f.Println("包的别名")
import(
"fmt"
_ "database/sql"
)
引入包,但是不加载里面的函数,即主程序中无法使用该包的函数,但是会执行包的初始化函数 init()
结构体
type Person struct {
name string
age int
}
var p Person
p.name = "Xiao Ming"
p.age = 23
fmt.Println("The person`s name is", p.name)
其他声明使用方式:
:=
p := Person{name: "Tom", age: 22}
new
p := new(Person)
这里返回的是指针类型 *Person
,具体和 make
区别查看 【基础数据类型】中两者专题的具体说明。
可以理解为隐式的继承关系,有点类似 php
中的 trait
用法,但实际关系是继承关系。
type Human struct {
name string
age int
birthday string // 农历生日
}
type Student struct {
Human
school string
birthday string // 阳历生日
}
xm := Student{Human: Human{name: "XiaoMing", age: 14}, school: "一中"}
fmt.Println(xm.name, xm.age, xm.school)
xm.birthday = "2001/8/2"
xm.Human.birthday = "2001/6/23"
fmt.Println(xm.birthday, xm.Human.birthday)
结构体除了有常量和变量之外,也有方法
func (r ReceiverType) funcName(parameters) (results)
使用例子:
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width float64
height float64
}
func (r Rectangle) area() float64 {
return r.width * r.height
}
type Circle struct {
radius float64
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r := Rectangle{width: 23.2, height: 13.5}
fmt.Println("the area of rectangle is", r.area())
c := Circle{radius: 2.5}
fmt.Println("the area of circle is", c.area())
}
并不像其他语言一样,都会用大括号括起来,所以我个人认为阅读时其实会造成一定的困难
注意:这里的 (c Circle)
都是使用的值传递,传递的都是结构体的复制副本,修改它的字段值,并不会影响实例的,如果需要修改,则需要传递指针,例如:
package main
import (
"fmt"
"math"
)
type Circle struct {
radius float64
backgroundColor string
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func (c *Circle) hover() {
c.backgroundColor = "#0062B7"
}
func main() {
c := Circle{radius: 2.5, backgroundColor: "#ffffff"}
c.hover()
fmt.Println("circle background color at hover status is", c.backgroundColor)
}
method
也可以和常量变量一样被继承后调用
如果函数名、参数、返回值都一样,就默认重写了,和大部分语言一样
接口,是一组 method
签名的组合,定义对象的一组行动。
任意的类型都实现了空 interface{}
注意: interface
可以被定义变量,但是不能实例化,它能存实现了它本身的类的对象:
type Human struct {
name string
age int
}
func (h Human) speak() string {
fmt.Println("speaking")
}
type Communicate interface {
speak() string
}
interface
也可以像 struct
一样,直接被另一个对象隐式继承
type Communicate interface {
speak() string
}
type Student struct {
Communicate
}
反射,一般分为三步:
reflect
对象,有两种对象:t := reflect.TypeOf(i) // 得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) // 得到实际的值,通过v我们获取存储在里面的值,还可以去改变值
// 获取定义在 struct 里的标签
tag := t.Elem().Field(0).Tag
// 存储在第一个字段里的值
name := t.Elem().Field(0).String()
获取值返回相应的类型和数值
var x float64 = 3.456
rxv := reflect.ValueOf(x)
fmt.Println("reflect x Type is", rxv.Type())
fmt.Println("reflect x is float64:", rxv.Kind() == reflect.Float64)
fmt.Println("reflect x value is:", rxv.Float())
var x float64 = 3.44
rxv := reflect.ValueOf(&x)
rxv.Elem().SetFloat(5.55)
fmt.Println(x)
这里只是介绍了 reflect
的基本使用
它是协程,比线程更小,是通过 Go 的 runtime
管理的一个线程管理器,通过 go
关键字实现。
go sayHello(somebody)
runtime.Gosched()
表示把 CPU 计算资源让给别人,下次某时候继续恢复执行。
Go 1.15 之后,并发线程个数 runtime.GOMAXPROCs
,已经由原来的默认 1 变为 CPU 核心数
通道,用于 多个goroutine
之间的通信,每个通道只可以定义一种数据格式,可以发送和接收数据, 必须使用 make
创建
ci := make(chan int)
发送和接收 <-
// 发送v到ch通道
ch <- v
// 接收通道数据并赋值给v
v := <- ch
channel
是阻塞型的,即当有数据在通道中传送时,通道不能再发送另一份数据,必须等到第一份数据被接收,通道空闲后才能使用
func sum(arr []int, c chan int) {
total := 0
for _, v := range arr {
total += v
}
c <- total
}
func main() {
i := []int{1, 2, 3, 4, 5, 56, 6, 6, 3}
c := make(chan int)
go sum(i[:4], c)
go sum(i[4:], c)
x, y := <-c, <-c
fmt.Println(x, y, x+y)
}
上面介绍的是阻塞型的通道,也有缓存型的通道,允许同时传递多组数据
make(chan type, n)
c := make(chan int, 2)
c <- 1
c <- 2
fmt.Println(<- c, <- c)
通过循环直接读取缓存型通道的多个数据
func split(n int, c chan int) {
for i := 0; i < n; i++ {
c <- i
}
close(c)
}
func main() {
c := make(chan int, 10)
go split(11, c)
for i := range c {
fmt.Println(i)
}
}
注意: channel
并不需要像文件一样一定关闭,除非你确定不再需要发送数据了,或者想要显示的关闭通道,关闭通道应该在数据生产的地方关闭,而不应该在接受端,即应用数据端关闭。
v, ok := <- c
可以通过这种方式判断 ok
来测试通道是否关闭
用于监听多通道,阻塞型,只有当有数据时才会调用执行,随机选择准备好的通道进行执行
package main
import "fmt"
func producer(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println("c data:", <-c)
}
quit <- 0
}()
producer(c, quit)
}
注意:
send c: 1
receive c: 1
receive c: 1
send c: 1
send c: 2
receive c: 2
receive c: 3
send c: 3
send c: 5
receive c: 5
receive c: 8
send c: 8
send c: 13
receive c: 13
receive c: 21
send c: 21
send c: 34
receive c: 34
receive c: 55
send c: 55
quit
注意看这里的运行结果,会非常有意思的是,他并不是我们想的那种,一发一收,而是很可能两发或者两收,我们定义的却并不是 buffered channel
,所以应该是和多线程调用有关,普通通道肯定是阻塞等待的,但是打印可能会因为多线程切换和有先后。
当所有线程都不动等待时,为了防止假死,会通过 select
case
设置 time.After
c := make(chan int)
to := make(chan bool)
go func() {
for {
select {
case v := <- c:
fmt.Println(v)
case <- time.After(5* time.Second):
fmt.Println("timeout after 5 second")
to <- true
}
}
}()
<- to
退出当前 goroutine
, defer
函数还是会执行
让出当前 goroutine
执行权,调度器安排其他任务,并在下次某个时候从该位置恢复执行
返回 CPU 核心数量
返回正在执行和排队的任务总数
设置可以并行计算的 CPU 最大核数,并返回之前的值