go语言特性
一、Go语言的由来和特性
Less is exponentially more – Rob Pike, Go Designer
Do Less, Enable More – Russ Cox, Go Tech Lead
1、为什么需要 Go 语言
其他编程语言的弊端
硬件发展速度远远超过软件
C 语言等原生语言缺乏好的依赖管理 (依赖头文件)
Java 和 C++ 等语言过于笨重
系统语言对垃圾回收和并行计算等基础功能缺乏支持
对多核计算机缺乏支持
Go 语言是一个可以编译高效,支持高并发的,面向垃圾回收的全新语言
秒级完成大型程序的单节点编译
依赖管理清晰
不支持继承,程序员无需花费精力定义不同类型之间的关系
支持垃圾回收,支持并发执行,支持多线程通讯
对多核计算机支持友好
2、Go 语言不支持的特性
不支持函数重载和操作符重载
为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
支持接口抽象,不支持继承
不支持动态加载代码
不支持动态链接库
通过 recover 和 panic 来替代异常机制
不支持断言
不支持静态变量
3、Go 语言特性衍生来源
二、语言编译环境设置
(一)安装Go
1、Mac安装
(1)安装 Go
打开下载网站(https://gomirrors.org/ 或者 https://golang.google.cn/dl/),选择安装包,下载完成,双击,一路 next,记住安装路径,安装完成打开命令行,输入命令 “go”,就可以看到 go 的一些常用命令。
输入命令没反应 —— 环境变量配置出错
默认情况下,双击安装安装完成之后,Go 会帮你配置好环境变量,如果出现未找到命令“go”错误,说明 Go 配置环境变量出错了,一般是因为你后面安装某些东西覆盖了。在 Mac 上错误信息一般是:command not found
输入命令没反应——找到自己的SHELL配置文件
确认自己的命令行配置文件是哪个:如果用的是 zsh,那么对应的文件一般是~/.zshrc;如果用的是 bash,那么对应的文件可能是~/.bashrc 或者~/.bash_profile;通过执行 echo $SHELL 可以准确找到是哪个 shell。
输入命令没反应——在文件里面编辑 PATH
修改 PATH 环境变量,加入GOROOT/bin> Tip: 反正看到找不到命令之类的错误,首先确认自己是否真的安装了,其次检查PATH 里面有没有假如安装路径
(2)配置 Go env
GO111MODULE:控制是否启用 go mod,默认不用管;如果要维护一些老项目,可能要开启。
GOROOT:安装路径,一般不需要配置,在 mac 中默认安装到 /User/local/go 目录下
GOPATH:关键,存放源代码的路径,设置为自己的 golang 的项目放置路径
GOPROXY:国内网络问题,推荐使用 “https://goproxy.cn”
GOPRIVATE:指向自己的私有库,比如说自己公司的私有库
其余选项暂时不用管,几乎不改
可以使用 go env 查看go的所有配置项,然后使用 go env -w 设置对应的变量
conglongli@localhost ~ % go env -w GOPATH="/Users/conglongli/Documents/workspace/golang" conglongli@localhost ~ % go env -w GOPROXY="https://goproxy.cn"
2、Windows安装
(1)安装windows terminal
从Microsoft Store 安装:
打开 Microsofe Store,搜索“terminal”,选择“Windows Terminal”,点击 “安装”,安装完成启动就可以
从Github下载安装:
从GitHub页面(https://github.com/microsoft/terminal/releases)下载,双击下载文件,打开 windows terminal,执行命令 ls,验证安装成功
(2)安装 Go
打开下载网站(https://gomirrors.org/)
选择安装包,下载完成,双击,一路 next,记住安装路径• 安装完成打开命令行,输入命令 “go”
输入命令没反应:默认情况下,双击安装安装完成之后,Golang 会帮你配置好环境变量,如果出现未找到命令“go”错误,说明 Golang 配置环境变量出错了(一般是因为你后面安装某些东西覆盖了)
输入命令没反应——解决方案
打开环境变量设置,在系统设置那里,新增一个环境变量:GOROOT,在设置值的时候,把刚才的安装路径放进去
修改 PATH 环境变量,加入GOROOT/bin,反正看到找不到命令之类的错误,首先确认自己是否真的安装了,其次检查PATH 里面有没有假如安装路径配置Go env
(3)配置Go env —— GOPATH
设置环境变量 GOPATH,例如我的设置为“D:\workspace\go”
将 GOPATH 目录下的 bin 文件夹加入 Path 环境变量
(二)安装IDE
1、安装 VS Code
(1)下载并安装 Visual Studio Code
到官网下载:https://code.visualstudio.com/download
(2)安装 Go 语言插件
https://marketplace.visualstudio.com/items?itemName=golang.go
2、安装 IntelliJ Goland
下载地址:https://www.jetbrains.com/go/download/#section=mac
(三)一些基本命令
上面安装 go 之后,使用 go 命令就可以看到 go 支持的命令
例如:
build:compile packages and dependencies(从源代码构建)
env:print Go environment information(设置 Go 的环境变量)
fmt:gofmt (reformat) package sources(对代码进行格式化)
get:add dependencies to current module and install them(安装或下载一个包)
install:compile and install packages and dependencies(编译安装,下载一个包并安装到本地)
mod:module maintenance(做依赖管理的)
test:test packages(做单元测试的)
1、Go build(编译构建)
Go 语言不支持动态链接,因此编译时会将所有依赖编译进同一个二进制文件。这样做的好处是由于所有的依赖都在同一个二进制文件中,因此无论 copy 到任何地方都可以顺利运行,那么不好的就是安装包会比较大,这是一个典型的空间换时间做法,但是现在硬件资源已经比较廉价,因此这样做的收益会更大。
代码:
package main import "fmt" func main() { fmt.Println("Hello World") }
(1)通过 go build 命令将代码编译成二进制文件,使用 -o 设置参数将二进制文件输出到哪里:go build –o bin/mybinary
使用 go build编译文件,可以看到生成了一个可执行的 helloworld的文件,然后就可以直接执行
conglongli@ConglongdeMacBook-Pro helloworld % ls main.go conglongli@ConglongdeMacBook-Pro helloworld % go build conglongli@ConglongdeMacBook-Pro helloworld % ls helloworld main.go conglongli@ConglongdeMacBook-Pro helloworld % ./helloworld Hello World
如果在执行 go build 报错:go: go.mod file not found in current directory or any parent directory; see 'go help modules',需要设置模块支持:go env -w GO111MODULE=auto
conglongli@ConglongdeMacBook-Pro helloworld % go build go: go.mod file not found in current directory or any parent directory; see 'go help modules' conglongli@ConglongdeMacBook-Pro helloworld % go env -w GO111MODULE=auto
(2)通过GOOS设置编译操作系统,使用GOARCH设置CPU架构,即编译后的二进制文件要在哪个操作系统的哪个CPU架构上运行。GOOS=linux GOARCH=amd64 go build
下面指定了编译的操作系统为linux,但是我本地为mac,一次执行报错:zsh: exec format error
conglongli@ConglongdeMacBook-Pro helloworld % GOOS=linux go build main.go conglongli@ConglongdeMacBook-Pro helloworld % ls main main.go conglongli@ConglongdeMacBook-Pro helloworld % ./main zsh: exec format error: ./main
(3)查看支持的OS和CPU列表列表。
可以通过 GOROOT中的 syslist.go 查看,$GOROOT/src/go/build/syslist.go
conglongli@ConglongdeMacBook-Pro helloworld % go env ...... GOROOT="/usr/local/go" ...... conglongli@ConglongdeMacBook-Pro build % cat /usr/local/go/src/go/build/syslist.go package build const goosList = "aix android darwin dragonfly freebsd hurd illumos ios js linux nacl netbsd openbsd plan9 solaris windows zos " const goarchList = "386 amd64 amd64p32 arm armbe arm64 arm64be loong64 mips mipsle mips64 mips64le mips64p32 mips64p32le ppc ppc64 ppc64le riscv riscv64 s390 s390x sparc sparc64 wasm "
2、Go test(Go 语言原生自带测试)
做测试时,首先要引入一个 testing的包,该包是用来支撑单测的。
测试方法的入口函数的入参必须是 testing.T
package main import ( "github.com/stretchr/testify/assert" "testing" ) func add(a, b int) int { return a + b } func TestIncrease(t *testing.T) { t.Log("Start testing") result := add(1, 2) assert.Equal(t, result, 3) }
go test ./… -v 运行测试
go test命令会扫描所有*_test.go为结尾的文件,其认为这些文件都是测试文件,并且执行文件中参数是 testing.T 的函数。
惯例是将测试代码与正式代码放在同目录,这样的好处是私有变量或私有函数都可以在单测中引用。
测试代码一般以 业务代码_test 命名,如 foo.go 的测试代码一般写在 foo_test.go
conglongli@ConglongdeMacBook-Pro callbacks % go test . ok _/Users/conglongli/Documents/workspace/golang/cncamp/example/module1/callbacks 0.395s
3、Go vet(代码静态检查,发现可能的 bug 或者可疑的构造)
有时我们在代码中写了错误的内容,可能会导致编译错误,这种在 IDE 中会变红,直接就能看出来。但是有的错误并不会影响编译,但是在运行中会出现问题,因此就提供了 Go vet 来做代码的检查。下面列举几种常见的问题:
(1)Print-format 错误,检查类型不匹配的print
下面的代码输出时,使用了 %d,表示输出一个数字,但是 str 是一个字符串,这种虽然不会出现编译和运行错误,但是输出的结果并不是按照期望的内容来输出的。
str := "hello world!" kk := 10 fmt.Printf("%d\n", str) fmt.Printf("%d\n", kk)
(2)Boolean 错误,检查一直为 true、false 或者冗余的表达式
i := 1 fmt.Println(i != 0 || i != 1)
(3)Range 循环,比如如下代码主协程会先退出,go routine无法被执行
words := []string{"foo", "bar", "baz"} for _, word := range words { go func() { fmt.Println(word) }() }
(4)Unreachable的代码,如 return 之后的代码
(5)其他错误,比如变量自赋值,error 检查滞后等
res, err := http.Get("https://www.spreadsheetdb.io/") defer res.Body.Close() if err != nil { log.Fatal(err) }
执行 go vet ,可以看到其提示出了所有的问题
conglongli@ConglongdeMacBook-Pro helloworld % go vet # _/Users/conglongli/Documents/workspace/golang/cncamp/example/module1/helloworld ./main.go:20:14: suspect or: i != 0 || i != 1 ./main.go:32:8: using res before checking for errors ./main.go:26:16: loop variable word captured by func literal ./main.go:15:2: fmt.Printf format %d has arg str of wrong type string
(6)官方提供的编辑器
如果没有 IDE,Goland 官方也提供了在线的编写 go 语言的编辑器 Golang playground:
官方 playground,https://play.golang.org/ ,可直接编写和运行 Go 语言程序
国内可直接访问的 playground,https://goplay.tools/
三、Go语言控制结构、常用数据结构
(一)语言控制结构
1、If
基本形式
if condition1 { // do something } else if condition2 { // do something else } else { // catch-all or default }
if 的简短语句
同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。
if v := x - 100; v < 0{ return v }
2、switch
switch var1 { case val1: //空分支 case val2: fallthrough //执行case3中的f() case val3: f() default: //默认分支 ... }
3、For
Go 只有一种循环结构:for 循环。
// 1、计入计数器的循环 for 初始化语句; 条件语句; 修饰语句 {} sum := 10 for i := 0; i < 10; i++ { sum += i } // 2\初始化语句和后置语句是可选的,此场景与 while 等价(Go 语言不支持 while) for ; sum < 1000; { sum += sum } // 3、无限循环 for { if condition1 { break } }
4、for-range
遍历数组,切片,字符串,Map 等
for index, char := range myString { ... } for key, value := range MyMap { ... } for index, value := range MyArray { ... }
需要注意:如果 for range 遍历指针数组,则 value 取出的指针地址为原指针地址的拷贝
(二)常用数据结构
1、变量与常量
常量是指定义后不能再被修改,而变量是指定义后还可以被修改。常量使用 const 修饰,变量使用 var 修饰。
变量定义:var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。(var c, python, java bool 表示定义了c、python、java三个变量,都是布尔类型)
变量的初始化:变量声明可以包含初始值,每个变量对应一个;如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型(var i, j int = 1, 2 表示定义了两个int类型的变量 i 和 j,值分别是 1 和 2)
短变量声明:在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。(c, python, java := true, false, "no!")
类型转换:表达式 T(v) 将值 v 转换为类型 T。
// 一些关于数值的转换: var i int = 42 var f float64 = float64(i) var u uint = uint(f) // 或者,更加简单的形式: i := 42 f := float64(i) u := uint(f)
类型推导:在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。
var i int j := i // j 也是一个 int
2、数组
定义方法:var identifier [len]type
示例:myArray := [3]int{1,2,3}
3、切片(slice)
切片是对数组一个连续片段的引用,数组定义中不指定长度即为切片:var identifier []type。
切片在未初始化之前默认为nil, 长度为0。
(1)常用方法:
func main() { myArray := [5]int{1, 2, 3, 4, 5} // 创建切片,并制定切片范围 mySlice := myArray[1:3] fmt.Printf("mySlice %+v\n" , mySlice) // 如果不指定范围,则表示全量数组的长度 fullSlice := myArray[:] // go原生没有提供切片的删除功能,那么可以自己写一个 remove3rdItem := deleteItem(fullSlice, 2) fmt.Printf("remove3rdItem %+v\n" ,remove3rdItem) } // 删除切片中的某一个元素:将切片按照删除的下标分为前后两个切片,再将两个切片合并 func deleteItem(slice []int, index int) []int { return append(slice[:index], slice[index+1:]...) }
(2)创建一个切片:
创建切片的方式有多种:
例如上面的先有数组,再将切片作为数据的一部分;
或者直接使用 var identifier []type 来声明一个切片;
同时 go 还提供了 Make 和 New 内置关键字来定义一个切片。
New 返回指针地址,Make 返回第一个元素,可预设内存空间,避免未来的内存拷贝
上面提到切片在未初始化之前默认为nil, 长度为0,那么在添加元素时就需要动态的进行扩容,会有一定的开销,如果在前期能预估切片的大小,可以设置切片的长度,并且还可以设置 capacity,实际上切片就类似于 java 的 ArrayList
mySlice1 := new([]int) mySlice2 := make([]int, 0) mySlice3 := make([]int, 10) mySlice4 := make([]int, 10, 20)
(3)关于切片的常见问题
a、切片是连续内存并且可以动态扩展,由此引发的问题?
下面的代码,首先创建了 ab 两个切片,然后将 a 赋值给 c ,此时 c 和 a 指向的是同一个内存地址,因此是等的。最后一步是将 b 添加了一个 1 ,然后赋值给 a ,此时如果内存地址不足以放置 4 个数据,那么其会重新开辟一个内存空间进行存放新的切片,那么 a 的地址就是新的内存地址,但是 c 还是旧的内存地址,因此 a 就不等于 c 。
a := []int{} b := []int{1,2,3} c := a a = append(b, 1)
b、修改切片的值?
在进行forRange循环时,value是新开辟的一个内存空间来存储数据的,因此直接操作value的值,并不会赋值给切片,只是赋值给了临时的空间。如果想要修改切片的value值,需要根据下标进行设置。
mySlice := []int{10, 20, 30, 40, 50} for _, value := range mySlice { value *= 2 } fmt.Printf("mySlice %+v\n" , mySlice) for index, _ := range mySlice { mySlice[index] *= 2 } fmt.Printf("mySlice %+v\n" , mySlice)
4、Map
声明方法:var map1 map[keytype]valuetype,valuetype可以使用函数
示例:
myMap := make(map[string]string, 10) myMap["a"] = "b" myFuncMap := map[string]func() int{ "funcA": func() int { return 1 }, } fmt.Println(myFuncMap) f := myFuncMap["funcA"] fmt.Println(f())
访问 Map 元素
// 按 Key 取值 value, exists := myMap["a"] if exists { println(value) } // 遍历 Map for k, v := range myMap { println(k, v) }
5、结构体和指针
结构体就是定义一个对象,通过 type … struct 关键字自定义结构体
type MyType struct { Name string } func printMyType(t *MyType){ println(t.Name) }
指针变量的值为内存地址,未赋值的指针为 nil。可以使用 & 获取内存地址。Go 语言支持指针,但不支持指针运算
func main(){ t := MyType{Name: "test"} printMyType(&t) }
结构体标签:针对结构体,go语言有额外的支持--结构体标签,K8S 就是有效的利用了结构体标签,来完成对象的序列化和反序列化。
如下面的例子所示,就是创建了一个结构体,定义了一个 json tag,然后使用反射的机制来获取变量名的tag,下面的案例中 json 就是 key,name就是value
type MyType struct { Name string `json:"name"` } func main() { mt := MyType{Name: "test"} myType := reflect.TypeOf(mt) name := myType.Field(0) tag := name.Tag.Get("json") println(tag) }
K8S APIServer 中对所有资源的定义都用 Json tag 来完成对象的序列化和反序列化。
6、类型重命名
类似于枚举,就是定义一个类型,go中本身不支持枚举,但是可以使用类型重命名的方式来完成这个目的。
例如下面的代码,是K8S源码中对于服务类型的定义。定义了一个ServiceType的类型,但是这个类型实际上还是 string 类型,可以定义一些枚举的变量放在新的类型内,当引用新的ServiceType时,值必须是在ServiceType中定义的枚举值。
type ServiceType string const ( ServiceTypeClusterIP ServiceType = "ClusterIP" ServiceTypeNodePort ServiceType = "NodePort" ServiceTypeLoadBalancer ServiceType = "LoadBalancer" ServiceTypeExternalName ServiceType = "ExternalName" )
四、Go语言函数
1、Main 函数
每个 Go 语言程序都应该有个 main package,Main package 里的 main 函数是 Go 语言程序入口
package main func main() { args := os.Args if len(args) != 0 { println("Do not accept any argument") os.Exit(1) } println("Hello world") }
参数解析:
请注意 main 函数与其他语言不同,没有类似 java 的 []string args 参数,Go 语言如何传入参数呢?
方法1:fmt.Println("os args is:", os.Args)
方法2:name := flag.String("name", "world", "specify the name you want to say hi") flag.Parse()
func main() { name := flag.String("name", "zhangsan", "test param: ") flag.Parse() fmt.Println("os args is:", os.Args) fmt.Println("input param is:", *name) fullStrings := fmt.Sprintf("hello %s from go\n", *name) fmt.Println(fullStrings) }
编译后运行,如果直接执行,os.Ars只是文件名,name走的是默认值,如果使用 --name设置了启动参数,则可以获取到指定的参数
conglongli@ConglongdeMacBook-Pro param % ./param os args is: [./param] input param is: zhangsan hello zhangsan from go conglongli@ConglongdeMacBook-Pro param % ./param --name lisi os args is: [./param --name lisi] input param is: lisi hello lisi from go
2、Init 函数
Init 函数会在包初始化时运行,也就是会在main函数前执行。在项目运行时,首先会运行main包,如果引用了其他包,则需要先执行其他包,运行一个包时,会按照顺序初始化常量、变量、init方法等。
定义一个init方法
package main var myVariable = 0 func init() { myVariable = 1 }
3、返回值
多值返回:函数可以返回任意数量的返回值
命名返回值:Go 的返回值可被命名,它们会被视作定义在函数顶部的变量; 返回值的名称应当具有一定的意义,它可以作为文档使用;没有参数的 return 语句返回已命名的返回值。也就是直接返回。例如下面的代码就不能使用 x := a,因为在返回值中已经定义过了,这样做的好处是可以可读性好,在生成文档时,这些变量名都已经定义清楚了。
func passValue(a, b int)(x, y int) { x = a y = b return x,y }
调用者忽略部分返回值:result, _ = strconv.Atoi(origStr)
4、传递变长参数
Go 语言中的可变长参数允许调用方传递任意多个相同类型的参数。
函数定义(在参数前加三个点,就表示可变长参数),例如切片的append方法:func append(slice []Type, elems ...Type) []Type
调用方法:
myArray := []string{} myArray = append(myArray, "a","b","c")
5、内置函数
go保留的关键字非常少,内置的函数如下所示
6、回调函数(Callback)
函数作为参数传入其它函数,并在其他函数内部调用执行。
如下代码所示:定义了两个函数 increase 和 decrease,然后定义了一个 DoOperation 方法,入参是一个数值和一个函数,在方法中调用入参的函数。
func main() { DoOperation(1, increase) DoOperation(1, decrease) } func increase(a, b int) { println(“increase result is:”, a+b) } func DoOperation(y int, f func(int, int)) { f(y, 1) } func decrease(a, b int) { println("decrease result is:", a-b) }
7、闭包(匿名函数)
没有方法名字,不能独立存在,只能在其他函数中使用
可以赋值给其他变量: x:= func(){}
可以直接调用: func(x,y int){println(x+y)}(1,2)
可作为函数返回值:func Add() (func(b int) int)
使用场景:在特定场景下需要运行,但是没有必要为其创建一个函数的场景,可以使用闭包。
defer func() { if r := recover(); r != nil { println(“recovered in FuncX”) } }()
8、方法
方法是指作用在接收者上的函数:func (recv receiver_type) methodName(parameter_list) (return_value_list)
使用场景:很多场景下,函数需要的上下文可以保存在receiver属性中,通过定义 receiver 的方法,该方法可以直接访问 receiver 属性,减少参数传递需求
func (s *Server) StartTLS() { if s.URL != "" { panic("Server already started") } if s.client == nil { s.client = &http.Client{Transport: &http.Transport{}} } }
9、传值还是传指针
Go 语言只有一种规则-传值,函数内修改参数的值不会影响函数外原始变量的值,可以传递指针参数将变量地址传递给调用函数,Go 语言会复制该指针作为函数内的地址,但指向同一地址。
思考:当我们写代码的时候,函数的参数传递应该用 struct还是 pointer?
如果使用指针,那传递的是一个指针地址,就不会涉及一些值的拷贝,性能上会好一些;如果传递的是struct,如果函数推出,那之前的临时变量都立马被回收,对GC会更好一些。所以具体使用哪种方式,可以视情况而定。
10、接口
接口定义一组方法集合
type IF interface { Method1(param_list) return_type }
适用场景:Kubernetes 中有大量的接口抽象和多种实现
Struct 无需显示声明实现 interface,只需直接实现方法,Struct 除实现 interface 定义的接口外,还可以有额外的方法
一个类型可实现多个接口(Go 语言的多重继承)
Go 语言中接口不接受属性定义
接口可以嵌套其他接口
以下面的代码为例,定义了一个 IF 接口,接口中有getName方法,又定义了Human、Plane、car的Struct;在main函数中,首先定义了一个 IF 类型的切片,然后放入的是具体的实现,最终输出时,go 可以根据实现的类型调用其实现的方法。
package main import "fmt" type IF interface { getName() string } type Human struct { firstName, lastName string } type Plane struct { vendor string model string } func (h *Human) getName() string { return h.firstName + "," + h.lastName } func (p Plane) getName() string { return fmt.Sprintf("vendor: %s, model: %s", p.vendor, p.model) } type Car struct { factory, model string } func (c *Car) getName() string { return c.factory + "-" + c.model } func main() { interfaces := []IF{} h := new(Human) h.firstName = "first" h.lastName = "last" interfaces = append(interfaces, h) c := new(Car) c.factory = "benz" c.model = "s" interfaces = append(interfaces, c) for _, f := range interfaces { fmt.Println(f.getName()) } p := Plane{} p.vendor = "testVendor" p.model = "testModel" fmt.Println(p.getName()) }
注意事项:
Interface 是可能为 nil 的,所以针对 interface 的使用一定要预先判空,否则会引起程序 crash(nil panic)
Struct 初始化意味着空间分配,对 struct 的引用不会出现空指针
11、反射机制
go支持反射机制,reflect.TypeOf ()返回被检查对象的类型,reflect.ValueOf()返回被检查对象的值
func main() { // basic type myMap := make(map[string]string, 10) myMap["a"] = "b" t := reflect.TypeOf(myMap) fmt.Println("type:", t) v := reflect.ValueOf(myMap) fmt.Println("value:", v) // struct myStruct := T{A: "a"} v1 := reflect.ValueOf(myStruct) for i := 0; i < v1.NumField(); i++ { fmt.Printf("Field %d: %v\n", i, v1.Field(i)) } for i := 0; i < v1.NumMethod(); i++ { fmt.Printf("Method %d: %v\n", i, v1.Method(i)) } // 需要注意receive是struct还是指针 result := v1.Method(0).Call(nil) fmt.Println("result:", result) } type T struct { A string } // 需要注意receive是struct还是指针 func (t T) String() string { return t.A + "1" }
12、Go 语言中的面向对象编程
有了以上的特性,实际上Go 语言就是面向对象编程的语言。面向对象编程的语言有可见性控制、继承、多肽等特性。
可见性控制:java 里面使用public 和 private等修饰,而go使用大小写表示,大写的常量、变量、类型、接口、结构、函数表示public;非大写表示private,就只能在包内使用
继承:通过组合实现,内嵌一个或多个 struct
多态:通过接口实现,通过接口定义方法集,编写多套实现
13、Json 编解码
Unmarshal: 从 string 转换至 struct
func unmarshal2Struct(humanStr string)Human { h := Human{} err := json.Unmarshal([]byte(humanStr), &h) if err != nil { println(err) } return h }
Marshal: 从 struct 转换至 string
func marshal2JsonString(h Human) string { h.Age = 30 updatedBytes, err := json.Marshal(&h) if err != nil { println(err) } return string(updatedBytes) }
json 包使用 map[string]interface{} 和 []interface{} 类型保存任意对象
可通过如下逻辑解析任意 json
var obj interface{} err := json.Unmarshal([]byte(humanStr), &obj) objMap, ok := obj.(map[string]interface{}) for k, v := range objMap { switch value := v.(type) { case string: fmt.Printf("type of %s is string, value is %v\n", k, value) case interface{}: fmt.Printf("type of %s is interface{}, value is %v\n", k, value) default: fmt.Printf("type of %s is wrong, value is %v\n", k, value) } }
五、Go语言常用语法和多线程
(一)常用语法
1、错误处理
Go 语言无内置 exception 机制,只提供 error 接口供定义错误
type error interface { Error() string }
可通过 errors.New 或 fmt.Errorf 创建新的 error:var errNotFound error = errors.New("NotFound")
通常应用程序对 error 的处理大部分是判断error 是否为 nil
如需将 error 归类,例如返回指定的code码,通常交给应用程序自定义,一般的操作是实现error接口,然后在struct中做其他的处理。比如 kubernetes 自定义了与 apiserver 交互的不同类型错误
type StatusError struct { ErrStatus metav1.Status } var _error = &StatusError{} // Error implements the Error interface. func (e *StatusError) Error() string { return e.ErrStatus.Message }
2、defer
函数返回之前执行某个语句或函数,等同于 Java 和 C# 的 finally。
常见的 defer 使用场景:记得关闭你打开的资源。(defer file.Close();defer mu.Unlock();defer println("") )
如果同时遇到多个defer,其是按照先进后出的原则执行。
defer是在函数推出时执行的。例如下面的代码,会发生死锁,因为虽然在循环中执行了解锁操作,但是是使用的defer,其是在方法退出的时候执行的,因此第一次循环后并没有释放锁,第二次循环加锁时,就发生了死锁。
func main() { defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3") loopFunc() time.Sleep(time.Second) } func loopFunc() { lock := sync.Mutex{} for i := 0; i < 3; i++ { // go func(i int) { lock.Lock() defer lock.Unlock() fmt.Println("loopFunc:", i) // }(i) } }
针对这种情况,就可以使用闭包处理
func loopFunc() { lock := sync.Mutex{} for i := 0; i < 3; i++ { go func(i int) { lock.Lock() defer lock.Unlock() fmt.Println("loopFunc:", i) }(i) } }
3、Panic 和 recover
panic: 可在系统出现不可恢复错误时主动调用 panic, panic 会使当前线程直接 crash,类似于java的catch
defer: 保证执行并把控制权交还给接收到 panic 的函数调用者,类似于finally,但是defer只能定义在panic之前,因为在panic之后的代码都不会执行
recover: 函数从 panic 或 错误场景中恢复
这些有点像java中的try catch
defer func() { fmt.Println("defer func is called") if err := recover(); err != nil { fmt.Println(err) } }() panic("a panic is triggered")
(二)多线程
1、并发和并行
并发是指两个或多个事件在同一时间间隔发生;并行两个或者多个事件在同一时刻发生
2、进程、线程、协程
进程:分配系统资源(CPU 时间、内存等)基本单位,有独立的内存空间,切换开销大
线程:进程的一个执行流,是 CPU 调度并能独立运行的的基本单位。同一进程中的多线程共享内存空间,线程切换代价小,多线程通信方便,从内核层面来看线程其实也是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共享了地址空间和信号处理函数
协程:Go 语言中的轻量级线程实现,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的 CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。
3、CSP模型(Communicating Sequential Process):
CSP:描述两个独立的并发实体通过共享的通讯 channel 进行通信的并发模型。
Go 协程 goroutine:是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与 Coroutine 协程也有区别,能够在发现堵塞后启动新的微线程。
通道 channel:类似 Unix 的 Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和 Channel 有着耦合。
4、线程和协程的差异
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(goroutine:2KB;线程:8MB)
线程 goroutine 切换开销方面,goroutine 远比线程小(线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新;goroutine:只有三个寄存器的值修改 - PC / SP / DX.)
GOMAXPROCS(控制并行线程数量)
5、协程示例
启动新协程:只需要在要新启动协程的代码前加上 go 关键字即可
for i := 0; i < 10; i++ { go fmt.Println(i) } time.Sleep(time.Second)
6、channel - 多线程通信
Channel 是多个协程之间通讯的管道,一端发送数据,一端接收数据,同一时间只有一个协程可以访问数据,无共享内存模式可能出现的内存竞争,协调协程的执行顺序。
声明方式:var identifier chan datatype,操作符 <-
如下代码所示,首先定义了一个 channel,在主线程中使用 go 关键字开启的新协程,在新协程中给channel赋值(箭头指向channel即为写,在右侧即为输出),最后在主线程中输出
ch := make(chan int) go func() { fmt.Println("hello from goroutine") ch <- 3 //数据写入Channel }() k := <-ch//从Channel中取数据并赋值 fmt.Println("=====",k)
7、通道缓冲
(1)双向通道
基于 Channel 的通信是同步的,当缓冲区满时,数据的发送是阻塞的,通过 make 关键字创建通道时可定义缓冲区容量,默认缓冲区容量为 0。例如上面的实例,没有设置缓冲区,那么缓冲区大小为0,那么数据接收方必须要就绪,如果不就绪,发送方就不能发送数据,也就是说,如果没有数据接收方,发送方就不能发送数据。
遍历通道缓冲区:
ch1 := make(chan int, 10) go func() { for i := 0; i < 10; i++ { rand.Seed(time.Now().UnixNano()) n := rand.Intn(10) // n will be between 0 and 10 fmt.Println("putting: ", n) ch1 <- i } close(ch1) }() fmt.Println("hello from main") for v := range ch1 { fmt.Println("receiving: ", v) }
(2)单向通道
上面的通道定义时没有使用操作符,表示既可以写也可以读,可以在定义通道是使用操作符,直接指定是发送通道还是接收通道。只发送通道:var sendOnly chan<- int,只接收通道:var readOnly <-chan int。
单向通道不能独立存在,但是其有自己的适用场景,例如在Istio中就利用了单向通道的机制。Istio webhook controller:func (w *WebhookCertPatcher) runWebhookController(stopChan <-chan struct{}) {}
如何用: 先创建一个双向通道,然后定义两个函数,一个函数只从通道中写数据,一个函数只从通道中读数据,类似于生产者消费者。
var c = make(chan int) go prod(c) go consume(c) func prod(ch chan<- int){ for { ch <- 1 } } func consume(ch <-chan int) { for { <-ch } }
(3)关闭通道
通道无需每次关闭,关闭的作用是告诉接收者该通道再无新数据发送,只有发送方需要关闭通道
ch := make(chan int) defer close(ch) if v, notClosed := <-ch; notClosed { fmt.Println(v) }
(4)使用 Channel 模拟生产者消费者
在K8S中很多都是生产者消费者模式,比如说K8S的控制器,主线程watch apiserver 的变化,worker线程处理数据,两者之间通过channel处理,或者通过worker Queue处理。
下面代码使用channel模拟生产者消费者模型:
func main() { messages := make(chan int, 10) done := make(chan bool) defer close(messages) // consumer go func() { ticker := time.NewTicker(1 * time.Second) for _ = range ticker.C { select { case <-done: fmt.Println("child process interrupt...") return default: fmt.Printf("send message: %d\n", <-messages) } } }() // producer for i := 0; i < 10; i++ { messages <- i } time.Sleep(5 * time.Second) close(done) time.Sleep(1 * time.Second) fmt.Println("main process exit!") }
8、select
当多个协程同时运行时,可通过 select 轮询多个通道,如果所有通道都阻塞则等待,如定义了 default 则执行 default,如多个通道就绪则随机选择。
select { case v:= <- ch1: ... case v:= <- ch2: ... default: ... }
9、定时器 Timer
time.Ticker 以指定的时间间隔重复的向通道 C 发送时间值。
使用场景:为协程设定超时时间。
下面代码创建了一个定时器,如果在select超时后,会走超时分支。
timer := time.NewTimer(time.Second) select { // check normal channel case <-ch: fmt.Println("received from ch") case <-timer.C: fmt.Println("timeout waiting from channel ch") }
9、上下文 Context
上下文 Context对多线程的操作做了简化,例如一个线程可以创建多个子线程,那么就可以通过上下文 Context传递一些变量,例如超时、取消操作或者一些异常情况。
Context 是设置截止日期、同步信号,传递请求相关值的结构体
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Context本身是个接口,定义了多个函数,例如 Deadline、Done、Err、Value等。
以下是一些结构体对于Context的实现:
context.Background:Background 通常被用于主函数、初始化以及测试中,作为一个顶层的 context,也就是说一般我们创建的 context 都是基于 Background
context.TODO:TODO 是在不确定使用什么 context 的时候才会使用
context.WithDeadline:设置超时时间,时间一到就会通知子线程
context.WithValue:向 context 添加键值对
context.WithCancel:创建一个可取消的 context
func main() { baseCtx := context.Background() ctx := context.WithValue(baseCtx, "a", "b") go func(c context.Context) { fmt.Println(c.Value("a")) }(ctx) timeoutCtx, cancel := context.WithTimeout(baseCtx, time.Second) defer cancel() go func(ctx context.Context) { ticker := time.NewTicker(1 * time.Second) for _ = range ticker.C { select { case <-ctx.Done(): fmt.Println("child process interrupt...") return default: fmt.Println("enter default") } } }(timeoutCtx) select { case <-timeoutCtx.Done(): time.Sleep(1 * time.Second) fmt.Println("main process exit!") } // time.Sleep(time.Second * 5) }
10、如何停止一个子协程
done := make(chan bool) go func() { for { select { case <-done: fmt.Println("done channel is triggerred, exit child go routine") return } } }() close(done)
基于 Context 停止子协程:Context 是 Go 语言对 go routine 和 timer 的封装
ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() go process(ctx, 100*time.Millisecond) <-ctx.Done() fmt.Println("main:", ctx.Err())
-----------------------------------------------------------
---------------------------------------------
朦胧的夜 留笔~~