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())

 

 

 

posted @ 2022-09-19 15:18  李聪龙  阅读(570)  评论(0编辑  收藏  举报