标签 docker 下的文章

十分钟入门Go语言

本文永久链接 – https://tonybai.com/2023/02/23/learn-go-in-10-min

本文旨在带大家快速入门Go语言,期望小伙伴们在花费十分钟左右通读全文后能对Go语言有一个初步的认知,为后续进一步深入学习Go奠定基础。

本文假设你完全没有接触过Go,你可能是一名精通其他编程语言的程序员,也可能是毫无编程经验、刚刚想转行为码农的热血青年。

编程简介

编程就是生产可在计算机上执行的程序的过程(如下图)。在这个过程中,程序员是“劳动力”,编程语言是工具,可执行的程序是生产结果。而Go语言就是程序员在编程生产过程中使用的一种优秀生产工具。

作为“劳动力”的程序员在这个过程中要做的就是使用某种编程语言作为生产工具,将事先设计好的执行逻辑组织和表达出来,这与一个作家将其大脑中设计好的故事情节用人类语言组织和书写在纸上的过程颇为类似(如下图)。

通过这个类比来看,学习一门编程语言,就好比学习一门人类语言,其词汇和语法将是我们的主要学习内容,本文就将围绕Go语言的主要“词汇”和语法形式进行快速说明。

Go简介

Go语言是由Google公司的三位大神级程序员Robert Griesemer、Rob Pike和Ken Thompson在2007年共同开发的一种新的后端编程语言,2009年,Go语言宣布开源。

Go语言的特点是简单易学、静态类型、编译速度快,运行效率高,代码简洁,并且原生支持并发编程。它还支持自动内存管理,可以让开发者更加专注于编程本身,而不用担心内存泄漏的问题。此外,Go语言还支持多核处理器,可以更好地利用多核处理器的优势,提高程序的运行效率。

经过十多年的发展,Go语言现在已经成为一种流行的编程语言,它可以用于开发各种应用程序,包括Web应用、网络服务、系统管理工具、移动应用、游戏开发、数据库管理等。Go语言常用于构建大型分布式系统,以及构建高性能的服务器端应用程序。Go为当前的云原生计算时代开发了一批“杀手级”应用,包括Docker、Kubernetes、Prometheus、InfluxDB、Cilium等。

安装Go

Go是静态语言,需要先编译,再执行,因此在开发Go程序之前,我们首先需要安装Go编译器以及相关工具链。安装的步骤很简单:

  • Go官网下载最新版本的Go语言安装包 – https://go.dev/dl/
  • 解压安装包,并将其复制到您想要安装的位置,例如:/usr/local/go;如果是Windows、MacOS平台,也可以下载图形化安装的安装包;
  • 设置环境变量,将Go语言的安装路径添加到PATH变量中;
  • 打开终端,输入go version,检查Go语言是否安装成功。如输出类似下面的内容,则表明安装成功!
$go version
go version go1.20 darwin/amd64

注:位于中国大陆的开发者们还需要一个额外的设置:export GOPROXY=’https://goproxy.cn’或将这个设置置于shell配置文件(比如.bashrc)中并使之生效。

第一个Go程序:Hello World

建立一个新目录,并在其中创建新文件helloworld.go,用任意编辑器打开helloworld.go,输入下面Go源码:

//helloworld.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Go支持直接运行某个源文件:

$go run helloworld.go
Hello, World!

但通常我们会先编译这个源文件(helloworld.go),生成可执行的二进制程序(./helloworld),然后再运行它:

$go build -o helloworld helloworld.go
$./helloworld
Hello, World!

Go包(package)

Go包是Go语言中的一种封装技术,它可以将一组Go语言源文件组织成一个可重用的单元,以便在其他Go程序中使用。同属于一个Go包的所有源文件放在一个目录下,并且按惯例该目录的名字与包名相同。以Go标准库的io包为例,其包内的源文件列表如下:

// $GOROOT/src/io目录下的文件列表:
io.go
multi.go
pipe.go

Go包也是Go编译的基本单元,Go编译器可以将包编译为可执行文件(如何该包为main包,且包含main函数实现),也可以编译为可重用的库文件(.a)。

包声明

Go包的声明通常是在每个Go源文件的开头,使用关键字package进行声明,例如:

// mypackage.go
package mypackage

... ...

package的名字按惯例通常为全小写的单个单词或缩略词,比如io、net、os、fmt、strconv、bytes等。

导入Go包

如果要复用已有的Go包,我们需要在源码中导入该包。要导入Go包,可以使用import关键字,例如:

import "fmt"                    // 导入标准库的fmt包

import "github.com/spf13/pflag" // 导入spf13开源的pflag包

import _ "net/http/pprof"       // 导入标准库net/http/pprof包,
                                // 但不显式使用该包中的类型、变量、函数等标识符

import myfmt "fmt"              // 将导入的包重命名为myfmt

Go模块

Go模块(module)是Go语言在1.11版本中引入的新特性,Go module是一组相关的Go package的集合,这个包集合被当做一个独立的单元进行统一版本管理。Go module这种新的依赖管理机制可以让开发者更轻松地管理Go语言项目的依赖关系,并且可以更好地支持多版本的依赖管理。在具有实用价值的Go项目中,我们都会使用Go module进行依赖管理。Go module有版本之分,Go module的版本依赖关系是建立在对语义版本(semver)严格遵守的前提下的。

Go使用go.mod文件来精确记录依赖关系要求,下面是go.mod中依赖关系的操作方法:

$go mod init demo // 创建一个module root为demo的go.mod
$go mod init github.com/bigwhite/mymodule // 创建一个module root为github.com/bigwhite/mymodule的go.mod

$go get github.com/bigwhite/foo@latest  // 向go.mod中添加一个依赖包github.com/bigwhite/foo的最新版本
$go get github.com/bigwhite/foo         // 与上面命令等价
$go get github.com/bigwhite/foo@v1.2.3  // 显式指定要获取v1.2.3版本

$go mod tidy   // 自动添加缺失的依赖包和清理不用的依赖包
$go mod verify // 确认所有依赖都有效

Go最小项目结构

Go官方并没有规定Go项目的标准结构布局,下面是Go核心团队技术负责人Russ Cox推荐的Go最小项目结构:

// 在Go项目仓库根路径下

- go.mod
- LICENSE
- README
- xx.go
- yy.go
... ...

// 在Go项目仓库根路径下

- go.mod
- LICENSE
- README
- package1/
    - package1.go
- package2/
    - package2.go
... ...

变量

Go语言有两种变量声明方式:

  • 使用var关键字

使用var关键字进行声明的方式适合所有场合。

var a int     // 声明一个int型变量a,初值为0
var b int = 5 // 声明一个int型变量b,初值为5
var c = 6     // Go会根据右值自动为变量c的赋予默认类型,默认的整型为int

var (         // 我们可以将变量声明统一放置在一个var块中,这与上面的声明方式等价
    a int
    b int = 5
    c = 6
)

注:Go变量声明采用变量在前,类型在后的方式,这与C、C++、Java等静态编程语言有较大不同。

  • 使用短声明方式声明变量
a := 5       // 声明一个变量a,Go会根据右值自动为变量a的赋予默认类型,默认的整型为int
s := "hello" // 声明一个变量s,Go会根据右值自动为变量s的赋予默认类型,默认的字符串类型为string

注:这种声明方式仅限于在函数或方法内使用,不能用于声明包级变量或全局变量。

常量

Go语言的常量使用const关键字进行声明:

const a int       // 声明一个int型常量a,其值为0
const b int = 5   // 声明一个int型常量b,其值为5
const c = 6       // 声明一个常量c,Go会根据右值自动为常量c的赋予默认类型,默认的整型为int
const s = "hello" // 声明一个常量s,Go会根据右值自动为常量s的赋予默认类型,默认的字符串类型为string

const (           // 我们可以将常量声明统一放置在一个const块中,这与上面的声明方式等价
    a int
    b int = 5
    c = 6
    s = "hello"
)

类型

Go原生内置了多种基本类型与复合类型。

基本类型

Go原生支持的基本类型包括布尔型、数值类型(整型、浮点型、复数类型)、字符串类型,下面是一些示例:

bool  // 布尔类型,默认值false

uint     // 架构相关的无符号整型,64位平台上其长度为8字节
int      // 架构相关的有符号整型,64位平台上其长度为8字节
uintptr  // 架构相关的用于表示指针值的类型,它是一个无符号的整数,大到足以存储一个任意类型的指针的值

uint8    // 架构无关的8位无符号整型
uint16   // 架构无关的16位无符号整型
uint32   // 架构无关的32位无符号整型
uint64   // 架构无关的64位无符号整型

int8     // 架构无关的8位有符号整型
int16    // 架构无关的16位有符号整型
int32    // 架构无关的32位有符号整型
int64    // 架构无关的64位有符号整型

byte     // uint8类型的别名
rune     // int32类型的别名,用于表示一个unicode字符(码点)

float32     // 单精度浮点类型,满足IEEE-754规范
float64     // 双精度浮点类型,满足IEEE-754规范

complex64   // 复数类型,其实部和虚部均为float32浮点类型
complex128  // 复数类型,其实部和虚部均为float64浮点类型

string      // 字符串类型,默认值为""

我们可以使用预定义函数complex来构造复数类型,比如:complex(1.0, -1.4)构造的复数为1 – 1.4i。

复合类型

Go原生支持的复合类型包括数组(array)、切片(slice)、结构体(struct)、指针(pointer)、函数(function)、接口(interface)、map、channel。

数组类型

数组类型是一组同构类型元素组成的连续体,它具有固定的长度(length),不能动态伸缩:

[8]int      // 一个元素类型为int、长度为16的数组类型
[32]byte    // 一个元素类型为byte、长度为32的数组类型
[2]string   // 一个元素类型为string、长度为2的数组类型
[N]T        // 一个元素类型为T、长度为N的数组类型

通过预定义函数len可以得到数组的长度:

var a = [8]int{11, 12, 13, 14, 15, 16, 17, 18}
println(len(a)) // 8

通过数组下标(从0开始)可以直接访问到数组中的任意元素:

println(a[0]) // 11
println(a[2]) // 13
println(a[7]) // 18

Go支持声明多维数组,即数组的元素类型依然为数组类型:

[2][3][5]float64  // 一个多维数组类型,等价于[2]([3]([5]float64))

切片类型

切片类型与数组类型类似,也是同构类型元素的连续体。不同的是切片类型的长度可变,我们在声明切片类型时无需传入长度属性:

[]int       // 一个元素类型为int的切片类型
[]string    // 一个元素类型为string的切片类型
[]T         // 一个元素类型为T的切片类型
[][][]float64 // 多维切片类型,等价于[]([]([]float64))

通过预定义函数len可以得到切片的当前长度:

var sl = []int{11, 12} // 一个元素类型为int的切片,其长度(len)为2, 其值为[11 12]
println(len(sl)) // 2

切片还有一个属性,那就是容量,通过预定义函数cap可以获得其容量值:

println(cap(sl)) // 2

和数组不同,切片可以动态伸缩,Go会根据元素的数量动态对切片容量进行扩展。我们可以通过append函数向切片追加元素:

sl = append(sl, 13)     // 向sl中追加新元素,操作后sl为[11 12 13]
sl = append(sl, 14)     // 向sl中追加新元素,操作后sl为[11 12 13 14]
sl = append(sl, 15)     // 向sl中追加新元素,操作后sl为[11 12 13 14 15]
println(len(sl), cap(sl)) // 5 8 追加后切片容量自动扩展为8

和数组一样,切片也是使用下标直接访问其中的元素:

println(sl[0]) // 11
println(sl[2]) // 13
println(sl[4]) // 15

结构体类型

Go的结构体类型是一种异构类型字段的聚合体,它提供了一种通用的、对实体对象进行聚合抽象的能力。下面是一个包含三个字段的结构体类型:

struct {
    name string
    age  int
    gender string
}

我们通常会给这样的一个结构体类型起一个名字,比如下面的Person:

type Person struct {
    name string
    age  int
    gender string
}

下面声明了一个Person类型的变量:

var p = Person {
    name: "tony bai",
    age: 20,
    gender: "male",
}

我们可以通过p.FieldName来访问结构体中的字段:

println(p.name) // tony bai
p.age = 21

结构体类型T的定义中可以包含类型为*T的字段成员,但不能递归包含T类型的字段成员:

type T struct {
    ... ...
    p *T    // ok
    t T     // 错误:递归定义
}

Go结构体亦可以在定义中嵌入其他类型:

type F struct {
    ... ...
}

type MyInt int

type T struct {
    MyInt
    F
    ... ...
}

嵌入类型的名字将作为字段名:

var t = T {
    MyInt: 5,
    F: F {
        ... ...
    },
}

println(t.MyInt) // 5

Go支持不包含任何字段的空结构体:

struct{}
type Empty struct{}        // 一个空结构体类型

空结构体类型的大小为0,这在很多场景下很有用(省去了内存分配的开销):

var t = Empty{}
println(unsafe.Sizeof(t)) // 0

指针类型

int类型对应的指针类型为*int,推而广之T类型对应的指针类型为*T。和非指针类型不同,指针类型变量存储的是内存单元的地址,*T指针类型变量的大小与T类型大小无关,而是和系统地址的表示长度有关。

*int     // 一个int指针类型
*[4]byte // 一个[4]byte数组指针类型

var a = 6
var p *T // 声明一个T类型指针变量p,默认值为nil
p = &a   // 用变量a的内存地址给指针变量p赋值
*p = 7   // 指针解引用,通过指针p将变量a的值由6改为7

n := new(int)  // 预定义函数返回一个*int类型指针
arr := new([4]int)  // 使用预定义函数new分配一个[4]int数组并返回一个*[4]int类型指针

map类型

map是Go语言提供的一种抽象数据类型,它表示一组无序的键值对,下面定义了一组map类型:

map[string]int                // 一个key类型为string,value类型为int的map类型
map[*T]struct{ x, y float64 } // 一个key类型为*T,value类型为struct{ x, y float64 }的map类型
map[string]interface{}        // 一个key类型为string,value类型为interface{}的map类型

我们可以用map字面量或make来创建一个map类型实例:

var m = map[string]int{}      // 声明一个map[string]int类型变量并初始化
var m1 = make(map[string]int) // 与上面的声明等价
var m2 = make(map[string]int, 100) // 声明一个map[string]int类型变量并初始化,其初始容量建议为100

操作map变量的方法也很简单:

m["key1"] = 5  // 添加/设置一个键值对
v, ok := m["key1"]  // 获取“key1”这个键的值,如果存在,则其值存储在v中,ok为true
delete(m, "key1") // 从m这个map中删除“key1”这个键以及其对应的值

其他类型

函数、接口、channel类型在后面有详细说明。

自定义类型

使用type关键字可以实现自定义类型:

type T1 int         // 定义一个新类型T1,其底层类型(underlying type)为int
type T2 string      // 定义一个新类型T2,其底层类型为string
type T3 struct{     // 定义一个新类型T3,其底层类型为一个结构体类型
    x, y int
    z string
}
type T4 []float64   // 定义一个新类型T4,其底层类型为[]float64切片类型
type T5 T4          // 定义一个新类型T5,其底层类型为[]float64切片类型

Go也支持为类型定义别名(alias),其形式如下;

type T1 = int       // 定义int的类型别名为T1,T1与int等价
type T2 = string    // 定义string的类型别名为T2,T2与string等价
type T3 = T2        // 定义T的类型别名为T3,T3与T2等价,也与string等价

类型转换

Go不支持隐式自动转型,如果要进行类型转换操作,我们必须显式进行,即便两个类型的底层类型相同也需如此:

type T1 int
type T2 int
var t1 T1
var n int = 5
t1 = T1(n)      // 显式将int类型变量转换为T1类型
var t2 T2
t2 = T2(t1)     // 显式将T1类型变量转换为T2类型

Go很多原生类型支持相互转换:

// 数值类型的相互转换

var a int16 = 16
b := int32(a)
c := uint16(a)
f := float64(a)

// 切片与数组的转换(Go 1.17版本及后续版本支持)

var a [3]int = [3]int([]int{1,2,3}) // 切片转换为数组
var pa *[3]int = (*[3]int)([]int{1,2,3}) // 切片转换为数组指针
sl := a[:] // 数组转换为切片

// 字符串与切片的相互转换

var sl = []byte{'h', 'e','l', 'l', 'o'}
var s = string(sl) // s为hello
var sl1 = []byte(s) // sl1为['h' 'e' 'l' 'l' 'o']
string([]rune{0x767d, 0x9d6c, 0x7fd4})  // []rune切片到string的转换

控制语句

Go提供了常见的控制语句,包括条件分支(if)、循环语句(for)和选择分支语句(switch)。

条件分支语句

// if ...

if a == 1 {
    ... ...
}

// if - else if - else

if a == 1 {

} else if b == 2 {

} else {

}

// 带有条件语句自用变量
if a := 1; a != 0 {

}

// if语句嵌套

if a == 1 {
    if b == 2 {

    } else if c == 3 {

    } else {

    }
}

循环语句

// 经典循环

for i := 0; i < 10; i++ {
    ...
}

// 模拟while ... do

for i < 10 {

}

// 无限循环

for {

}

// for range

var s = "hello"
for i, c := range s {

}

var sl = []int{... ...}
for i, v := range sl {

}

var m = map[string]int{}
for k, v := range m {

}

var c = make(chan int, 100)
for v := range c {

}

选择分支语句

var n = 5
switch n {
    case 0, 1, 2, 3:
        s1()
    case 4, 5, 6, 7:
        s2()
    default: // 默认分支
        s3()
}

switch n {
    case 0, 1:
        fallthrough  // 显式告知执行下面分支的动作
    case 2, 3:
        s1()
    case 4, 5, 6, 7:
        s2()
    default:
        s3()
}

switch x := f(); {
    case x < 0:
        return -x
    default:
        return x
}

switch {
    case x < y:
        f1()
    case x < z:
        f2()
    case x == 4:
        f3()
}

函数

Go使用func关键字来声明一个函数:

func greet(name string) string {
    return fmt.Sprintf("Hello %s", name)
}

函数由函数名、可选的参数列表和返回值列表组成。Go函数支持返回多个返回值,并且我们通常将表示错误值的返回类型放在返回值列表的最后面:

func Atoi(s string) (int, error) {
    ... ...
    return n, nil
}

在Go中函数是一等公民,因此函数自身也可以作为参数或返回值:

func MultiplyN(n int) func(x int) int {
  return func(x int) int {
    return x * n
  }
}

像上面MultiplyN函数中定义的匿名函数func(x int) int,它的实现中引用了它的外围函数MultiplyN的参数n,这样的匿名函数也被称为闭包(closure)

说到函数,我们就不能不提defer。在某函数F调用的前面加上defer,该函数F的执行将被“延后”至其调用者A结束之后:

func F() {
    fmt.Println("call F")
}

func A() {
    fmt.Println("call A")
    defer F()
    fmt.Println("exit A")
}

func main() {
    A()
}

上面示例输出:

call A
exit A
call F

在一个函数中可以多次使用defer:

func B() {
    defer F()
    defer G()
    defer H()
}

被defer修饰的函数将按照“先入后出”的顺序在B函数结束后被调用,上面B函数执行后将输出:

call H
call G
call F

方法

方法是带有receiver的函数。下面是Point类型的一个方法Length:

type Point struct {
    x, y float64
}

func (p Point) Length() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

而在func关键字与函数名之间的部分便是receiver。这个receiver也是Length方法与Point类型之间纽带。我们可以通过Point类型变量来调用Length方法:

var p = Point{3,4}
fmt.Println(p.Length())

亦可以将方法当作函数来用:

var p = Point{3,4}
fmt.Println(Point.Length(p)) // 这种用法也被称为方法表达式(method expression)

接口

接口是一组方法的集合,它代表一个“契约”,下面是一个由三个方法组成的方法集合的接口类型:

type MyInterface interface {
    M1(int) int
    M2(string) error
    M3()
}

Go推崇面向接口编程,因为通过接口我们可以很容易构建低耦合的应用。

Go还支持在接口类型(如I)中嵌套其他接口类型(如io.Writer、sync.Locker),其结果就是新接口类型I的方法集合为其方法集合与嵌入的接口类型Writer和Locker的方法集合的并集:

type I interface { // 一个嵌入了其他接口类型的接口类型
   io.Writer
   sync.Locker
}

接口实现

如果一个类型T实现了某个接口类型MyInterface方法集合中的所有方法,那么我们说该类型T实现了接口MyInterface,于是T类型的变量t可以赋值给接口类型MyInterface的变量i,此时变量i的动态类型为T:

var t T
var i MyInterface = t // ok

通过上述变量i可以调用T的方法:

i.M1(5)
i.M2("demo")
i.M3()

方法集合为空的接口类型interface{}被称为“空接口类型”,空白的“契约”意味着任何类型都实现了该空接口类型,即任何变量都可以赋值给interface{}类型的变量:

var i interface{} = 5 // ok
i = "demo"            // ok
i = T{}               // ok
i = &T{}              // ok
i = []T{}             // ok

注:Go 1.18中引入的新预定义标识符any与interface{}是等价类型。

接口的类型断言

Go支持通过类型断言从接口变量中提取其动态类型的值:

v, ok := i.(T) // 类型断言

如果接口变量i的动态类型确为T,那么v将被赋予该动态类型的值,ok为true;否则,v为T类型的零值,ok为false。

类型断言也支持下面这种语法形式:

v := i.(T)

但在这种形式下,一旦接口变量i之前被赋予的值不是T类型的值,那么这个语句将抛出panic。

接口类型的type switch

“type switch”这是一种特殊的switch语句用法,仅用于接口类型变量:

func main() {
    var x interface{} = 13
    switch x.(type) {
    case nil:
        println("x is nil")
    case int:
        println("the type of x is int") // 执行这一分支case
    case string:
        println("the type of x is string")
    case bool:
        println("the type of x is string")
    default:
        println("don't support the type")
    }
}

switch关键字后面跟着的表达式为x.(type),这种表达式形式是switch语句专有的,而且也只能在switch语句中使用。这个表达式中的x必须是一个接口类型变量,表达式的求值结果是这个接口类型变量对应的动态类型。

上述例子中switch后面的表达式也可由x.(type)换成了v := x.(type)。v中将存储变量x的动态类型对应的值信息:

var x interface{} = 13
switch x.(type) {
    case nil:
        println("v is nil")
    case int:
        println("the type of v is int, v =", v) // 执行这一分支case,v = 13
    ... ...
}

泛型

Go从1.18版本开始支持泛型。Go泛型的基本语法是类型参数(type parameter),Go泛型方案的实质是对类型参数的支持,包括:

  • 泛型函数(generic function):带有类型参数的函数;
  • 泛型类型(generic type):带有类型参数的自定义类型;
  • 泛型方法(generic method):泛型类型的方法。

泛型函数

下面是一个泛型函数max的定义:

type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

func max[T ordered](sl []T) T {
    ... ...
}

与普通Go函数相比,max函数在函数名称与函数参数列表之间多了一段由方括号括起的代码:[T ordered];max参数列表中的参数类型以及返回值列表中的返回值类型都是T,而不是某个具体的类型。

max函数中多出的[T ordered]就是Go泛型的类型参数列表(type parameters list),示例中这个列表中仅有一个类型参数T,ordered为类型参数的类型约束(type constraint)。

我们可以像普通函数一样调用泛型函数,我们可以显式指定类型实参:

var m int = max[int]([]int{1, 2, -4, -6, 7, 0})  // 显式指定类型实参为int
fmt.Println(m) // 输出:7

Go也支持自动推断出类型实参:

var m int = max([]int{1, 2, -4, -6, 7, 0}) // 自动推断T为int
fmt.Println(m) // 输出:7

泛型类型

所谓泛型类型,就是在类型声明中带有类型参数的Go类型:

type Set[T comparable] map[T]string

type element[T any] struct {
    next *element[T]
    val  T
}

type Map[K, V any] struct {
  root    *node[K, V]
  compare func(K, K) int
}

以泛型类型Set为例,其使用方法如下:

var s = Set[string]{}
s["key1"] = "value1"
println(s["key1"]) // value1

泛型方法

Go类型可以拥有自己的方法(method),泛型类型也不例外,为泛型类型定义的方法称为泛型方法(generic method)。

type Set[T comparable] map[T]string

func (s Set[T]) Insert(key T, val string) {
    s[key] = val
}

func (s Set[T]) Get(key T) (string, error) {
    val, ok := s[key]
    if !ok {
        return "", errors.New("not found")
    }
    return val, nil
}

func main() {
    var s = Set[string]{
        "key": "value1",
    }
    s.Insert("key2", "value2")
    v, err := s.Get("key2")
    fmt.Println(v, err) // value2 <nil>
}

类型约束

Go通过类型约束(constraint)对泛型函数的类型参数以及泛型函数中的实现代码设置限制。Go使用扩展语法后的interface类型来定义约束。

下面是使用常规接口类型作为约束的例子:

type Stringer interface {
    String() string
}

func Stringify[T fmt.Stringer](s []T) (ret []string) { // 通过Stringer约束了T的实参只能是实现了Stringer接口的类型
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

Go接口类型声明语法做了扩展,支持在接口类型中放入类型元素(type element)信息:

type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 | ~string
}

func Less[T ordered](a, b T) bool {
    return a < b
}

type Person struct {
    name string
    age  int
}

func main() {
    println(Less(1, 2)) // true
    println(Less(Person{"tony", 11}, Person{"tom", 23})) // Person不满足ordered的约束,会导致编译错误
}

并发

Go语言原生支持并发,Go并没有使用操作系统线程作为并发的基本执行单元,而是实现了goroutine这一由Go运行时(runtime)负责调度的、轻量的用户级线程,为并发程序设计提供原生支持。

goroutine

通过go关键字+函数/方法的方式,我们便可以创建一个goroutine。创建后,新goroutine将拥有独立的代码执行流,并与创建它的goroutine一起被Go运行时调度。

go fmt.Println("I am a goroutine")

// $GOROOT/src/net/http/server.go
c := srv.newConn(rw)
go c.serve(connCtx)

goroutine的执行函数返回后,goroutine便退出。如果是主goroutine(执行main.main的goroutine)退出,那么整个Go应用进程将会退出,程序生命周期结束。

channel

Go提供了原生的用于goroutine之间通信的机制channel,channel的定义与操作方式如下:

// channel类型
chan T          // 一个元素类型为T的channel类型
chan<- float64  // 一个元素类型为float64的只发送channel类型
<-chan int      // 一个元素类型为int的只接收channel类型

var c chan int             // 声明一个元素类型为int的channel类型的变量,初值为nil
c1 := make(chan int)       // 声明一个元素类型为int的无缓冲的channel类型的变量
c2 := make(chan int, 100)  // 声明一个元素类型为int的带缓冲的channel类型的变量,缓冲大小为100
close(c)                   // 关闭一个channel

下面是两个goroutine基于channel通信的例子:

func main() {
    var c = make(chan int)
    go func(a, b int) {
        c <- a + b
    }(3,4)
    println(<-c) // 7
}

当涉及同时对多个channel进行操作时,Go提供了select机制。通过select,我们可以同时在多个channel上进行发送/接收操作:

select {
case x := <-ch1:     // 从channel ch1接收数据
  ... ...

case y, ok := <-ch2: // 从channel ch2接收数据,并根据ok值判断ch2是否已经关闭
  ... ...

case ch3 <- z:       // 将z值发送到channel ch3中:
  ... ...

default:             // 当上面case中的channel通信均无法实施时,执行该默认分支
}

错误处理

Go提供了简单的、基于错误值比较的错误处理机制,这种机制让每个开发人员必须显式地去关注和处理每个错误。

error类型

Go用error这个接口类型表示错误,并且按惯例,我们通常将error类型返回值放在返回值列表的末尾。

// $GOROOT/src/builtin/builtin.go
type error interface {
    Error() string
}

任何实现了error的Error方法的类型的实例,都可以作为错误值赋值给error接口变量。

Go提供了便捷的构造错误值的方法:

err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)

错误处理形式

Go最常见的错误处理形式如下:

err := doSomething()
if err != nil {
    ... ...
    return err
}

通常我们会定义一些“哨兵”错误值来辅助错误处理方检视(inspect)错误值并做出错误处理分支的决策:

// $GOROOT/src/bufio/bufio.go
var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

func doSomething() {
    ... ...
    data, err := b.Peek(1)
    if err != nil {
        switch err {
        case bufio.ErrNegativeCount:
            // ... ...
            return
        case bufio.ErrBufferFull:
            // ... ...
            return
        case bufio.ErrInvalidUnreadByte:
            // ... ...
            return
        default:
            // ... ...
            return
        }
    }
    ... ...
}

Is和As

从Go 1.13版本开始,标准库errors包提供了Is函数用于错误处理方对错误值的检视。Is函数类似于把一个error类型变量与“哨兵”错误值进行比较:

// 类似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {
    // 越界的错误处理
}

不同的是,如果error类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。

标准库errors包还提供了As函数给错误处理方检视错误值。As函数类似于通过类型断言判断一个error类型变量是否为特定的自定义错误类型:

// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
    // 如果err类型为*MyError,变量e将被设置为对应的错误值
}

如果error类型变量的动态错误值是一个包装错误,errors.As函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像errors.Is函数那样。

小结

读到这里,你已经对Go语言有了入门级的认知,但要想成为一名Gopher(对Go开发人员的称呼),还需要更进一步的学习与实践。我的极客时间专栏《Go语言第一课》是一个很好的起点,欢迎大家订阅学习^_^。

BTW,本文部分内容由ChatGPT生成!你能猜到是哪些部分吗^_^。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

使用Go开发Kubernetes Operator:基本结构

本文永久链接 – https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1

注:文章首图基于《Kubernetes Operators Explained》修改

几年前,我还称Kubernetes为服务编排和容器调度领域的事实标准,如今K8s已经是这个领域的“霸主”,地位无可撼动。不过,虽然Kubernetes发展演化到今天已经变得非常复杂,但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像Operator这样的应用模式和扩展方式日益受到开发者与运维者的欢迎。

我们的平台内部存在有状态(stateful)的后端服务,对有状态的服务的部署和运维是k8s operator的拿手好戏,是时候来研究一下operator了。

一. Operator的优点

kubernetes operator的概念最初来自CoreOS – 一家被红帽(redhat)收购的容器技术公司。

CoreOS在引入Operator概念的同时,也给出了Operator的第一批参考实现:etcd operatorprometheus operator

注:etcd于2013年由CoreOS以开源形式发布;prometheus作为首款面向云原生服务的时序数据存储与监控系统,由SoundCloud公司于2012年以开源的形式发布。

下面是CoreOS对Operator这一概念的诠释:Operator在软件中代表了人类的运维操作知识,通过它可以可靠地管理一个应用程序


图:CoreOS对operator的诠释(截图来自CoreOS官方博客归档)

Operator出现的初衷就是用来解放运维人员的,如今Operator也越来越受到云原生运维开发人员的青睐。

那么operator好处究竟在哪里呢?下面示意图对使用Operator和不使用Operator进行了对比:

通过这张图,即便对operator不甚了解,你也能大致感受到operator的优点吧。

我们看到在使用operator的情况下,对有状态应用的伸缩操作(这里以伸缩操作为例,也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作),运维人员仅需一个简单的命令即可,运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。

在没有使用operator的情况下,运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知,并按顺序逐个执行一个命令序列中的命令并检查命令响应,遇到失败的情况时还需要进行重试,直到伸缩成功。

我们看到operator就好比一个内置于k8s中的经验丰富运维人员,时刻监控目标对象的状态,把复杂性留给自己,给运维人员一个简洁的交互接口,同时operator也能降低运维人员因个人原因导致的操作失误的概率。

不过,operator虽好,但开发门槛却不低。开发门槛至少体现在如下几个方面:

  • 对operator概念的理解是基于对k8s的理解的基础之上的,而k8s自从2014年开源以来,变的日益复杂,理解起来需要一定时间投入;
  • 从头手撸operator很verbose,几乎无人这么做,大多数开发者都会去学习相应的开发框架与工具,比如:kubebuilderoperator framework sdk等;
  • operator的能力也有高低之分,operator framework就提出了一个包含五个等级的operator能力模型(CAPABILITY MODEL),见下图。使用Go开发高能力等级的operator需要对client-go这个kubernetes官方go client库中的API有深入的了解。


图:operator能力模型(截图来自operator framework官网)

当然在这些门槛当中,对operator概念的理解既是基础也是前提,而理解operator的前提又是对kubernetes的诸多概念要有深入理解,尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。

二. Kubernetes resource、resource type、API和controller介绍

Kubernetes发展到今天,其本质已经显现:

  • Kubernetes就是一个“数据库”(数据实际持久存储在etcd中);
  • 其API就是“sql语句”;
  • API设计采用基于resource的Restful风格, resource type是API的端点(endpoint);
  • 每一类resource(即Resource Type)是一张“表”,Resource Type的spec对应“表结构”信息(schema);
  • 每张“表”里的一行记录就是一个resource,即该表对应的Resource Type的一个实例(instance);
  • Kubernetes这个“数据库”内置了很多“表”,比如Pod、Deployment、DaemonSet、ReplicaSet等;

下面是一个Kubernetes API与resource关系的示意图:

我们看到resource type有两类,一类的namespace相关的(namespace-scoped),我们通过下面形式的API操作这类resource type的实例:

VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE - 操作某特定namespace下面的resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME - 操作某特定namespace下面的resource type中的某个具体的resource实例

另外一类则是namespace无关,即cluster范围(cluster-scoped)的,我们通过下面形式的API对这类resource type的实例进行操作:

VERB /apis/GROUP/VERSION/RESOURCETYPE - 操作resouce type中的resource实例集合
VERB /apis/GROUP/VERSION/RESOURCETYPE/NAME - 操作resource type中的某个具体的resource实例

我们知道Kubernetes并非真的只是一个“数据库”,它是服务编排和容器调度的平台标准,它的基本调度单元是Pod(也是一个resource type),即一组容器的集合。那么Pod又是如何被创建、更新和删除的呢?这就离不开控制器(controller)了。每一类resource type都有自己对应的控制器(controller)。以pod这个resource type为例,它的controller为ReplicasSet的实例。

控制器的运行逻辑如下图所示:


图:控制器运行逻辑(引自《Kubernetes Operators Explained》一文)

控制器一旦启动,将尝试获得resource的当前状态(current state),并与存储在k8s中的resource的期望状态(desired state,即spec)做比对,如果不一致,controller就会调用相应API进行调整,尽力使得current state与期望状态达成一致。这个达成一致的过程被称为协调(reconciliation),协调过程的伪代码逻辑如下:

for {
    desired := getDesiredState()
    current := getCurrentState()
    makeChanges(desired, current)
}

注:k8s中有一个object的概念?那么object是什么呢?它类似于Java Object基类或Ruby中的Object超类。不仅resource type的实例resource是一个(is-a)object,resource type本身也是一个object,它是kubernetes concept的实例。

有了上面对k8s这些概念的初步理解,我们下面就来理解一下Operator究竟是什么!

三. Operator模式 = 操作对象(CRD) + 控制逻辑(controller)

如果让运维人员直面这些内置的resource type(如deployment、pod等),也就是前面“使用operator vs. 不使用operator”对比图中的第二种情况, 运维人员面临的情况将会很复杂,且操作易错。

那么如果不直面内置的resource type,那么我们如何自定义resource type呢, Kubernetes提供了Custom Resource Definition,CRD(在coreos刚提出operator概念的时候,crd的前身是Third Party Resource, TPR)可以用于自定义resource type。

根据前面我们对resource type理解,定义CRD相当于建立新“表”(resource type),一旦CRD建立,k8s会为我们自动生成对应CRD的API endpoint,我们就可以通过yaml或API来操作这个“表”。我们可以向“表”中“插入”数据,即基于CRD创建Custom Resource(CR),这就好比我们创建Deployment实例,向Deployment“表”中插入数据一样。

和原生内置的resource type一样,光有存储对象状态的CR还不够,原生resource type有对应controller负责协调(reconciliation)实例的创建、伸缩与删除,CR也需要这样的“协调者”,即我们也需要定义一个controller来负责监听CR状态并管理CR创建、伸缩、删除以及保持期望状态(spec)与当前状态(current state)的一致。这个controller不再是面向原生Resource type的实例,而是面向CRD的实例CR的controller

有了自定义的操作对象类型(CRD),有了面向操作对象类型实例的controller,我们将其打包为一个概念:“Operator模式”,operator模式中的controller也被称为operator,它是在集群中对CR进行维护操作的主体。

四. 使用kubebuilder开发webserver operator

假设:此时你的本地开发环境已经具备访问实验用k8s环境的一切配置,通过kubectl工具可以任意操作k8s。

再深入浅出的概念讲解都不如一次实战对理解概念更有帮助,下面我们就来开发一个简单的Operator。

前面提过operator开发非常verbose,因此社区提供了开发工具和框架来帮助开发人员简化开发过程,目前主流的包括operator framework sdk和kubebuilder,前者是redhat开源并维护的一套工具,支持使用go、ansible、helm进行operator开发(其中只有go可以开发到能力级别5的operator,其他两种则不行);而kubebuilder则是kubernetes官方的一个sig(特别兴趣小组)维护的operator开发工具。目前基于operator framework sdk和go进行operator开发时,operator sdk底层使用的也是kubebuilder,所以这里我们就直接使用kubebuilder来开发operator。

按照operator能力模型,我们这个operator差不多处于2级这个层次,我们定义一个Webserver的resource type,它代表的是一个基于nginx的webserver集群,我们的operator支持创建webserver示例(一个nginx集群),支持nginx集群伸缩,支持集群中nginx的版本升级。

下面我们就用kubebuilder来实现这个operator!

1. 安装kubebuilder

这里我们采用源码构建方式安装,步骤如下:

$git clone git@github.com:kubernetes-sigs/kubebuilder.git
$cd kubebuilder
$make
$cd bin
$./kubebuilder version
Version: main.version{KubeBuilderVersion:"v3.5.0-101-g5c949c2e",
KubernetesVendor:"unknown",
GitCommit:"5c949c2e50ca8eec80d64878b88e1b2ee30bf0bc",
BuildDate:"2022-08-06T09:12:50Z", GoOs:"linux", GoArch:"amd64"}

然后将bin/kubebuilder拷贝到你的PATH环境变量中的某个路径下即可。

2. 创建webserver-operator工程

接下来,我们就可以使用kubebuilder创建webserver-operator工程了:

$mkdir webserver-operator
$cd webserver-operator
$kubebuilder init  --repo github.com/bigwhite/webserver-operator --project-name webserver-operator

Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.12.2
go: downloading k8s.io/client-go v0.24.2
go: downloading k8s.io/component-base v0.24.2
Update dependencies:
$ go mod tidy
Next: define a resource with:
kubebuilder create api

注:–repo指定go.mod中的module root path,你可以定义你自己的module root path。

3. 创建API,生成初始CRD

Operator包括CRD和controller,这里我们就来建立自己的CRD,即自定义的resource type,也就是API的endpoint,我们使用下面kubebuilder create命令来完成这个步骤:

$kubebuilder create api --version v1 --kind WebServer
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/webserver_types.go
controllers/webserver_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests

之后,我们执行make manifests来生成最终CRD对应的yaml文件:

$make manifests
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

此刻,整个工程的目录文件布局如下:

$tree -F .
.
├── api/
│   └── v1/
│       ├── groupversion_info.go
│       ├── webserver_types.go
│       └── zz_generated.deepcopy.go
├── bin/
│   └── controller-gen*
├── config/
│   ├── crd/
│   │   ├── bases/
│   │   │   └── my.domain_webservers.yaml
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches/
│   │       ├── cainjection_in_webservers.yaml
│   │       └── webhook_in_webservers.yaml
│   ├── default/
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager/
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus/
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac/
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── role_binding.yaml
│   │   ├── role.yaml
│   │   ├── service_account.yaml
│   │   ├── webserver_editor_role.yaml
│   │   └── webserver_viewer_role.yaml
│   └── samples/
│       └── _v1_webserver.yaml
├── controllers/
│   ├── suite_test.go
│   └── webserver_controller.go
├── Dockerfile
├── go.mod
├── go.sum
├── hack/
│   └── boilerplate.go.txt
├── main.go
├── Makefile
├── PROJECT
└── README.md

14 directories, 40 files

4. webserver-operator的基本结构

忽略我们此次不关心的诸如leader election、auth_proxy等,我将这个operator例子的主要部分整理到下面这张图中:

图中的各个部分就是使用kubebuilder生成的operator的基本结构

webserver operator主要由CRD和controller组成:

  • CRD

图中的左下角的框框就是上面生成的CRD yaml文件:config/crd/bases/my.domain_webservers.yaml。CRD与api/v1/webserver_types.go密切相关。我们在api/v1/webserver_types.go中为CRD定义spec相关字段,之后make manifests命令可以解析webserver_types.go中的变化并更新CRD的yaml文件。

  • controller

从图的右侧部分可以看出,controller自身就是作为一个deployment部署在k8s集群中运行的,它监视CRD的实例CR的运行状态,并在Reconcile方法中检查预期状态与当前状态是否一致,如果不一致,则执行相关操作。

  • 其它

图中左上角是有关controller的权限的设置,controller通过serviceaccount访问k8s API server,通过role.yaml和role_binding.yaml设置controller的角色和权限。

5. 为CRD spec添加字段(field)

为了实现Webserver operator的功能目标,我们需要为CRD spec添加一些状态字段。前面说过,CRD与api中的webserver_types.go文件是同步的,我们只需修改webserver_types.go文件即可。我们在WebServerSpec结构体中增加Replicas和Image两个字段,它们分别用于表示webserver实例的副本数量以及使用的容器镜像:

// api/v1/webserver_types.go

// WebServerSpec defines the desired state of WebServer
type WebServerSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // The number of replicas that the webserver should have
    Replicas int `json:"replicas,omitempty"`

    // The container image of the webserver
    Image string `json:"image,omitempty"`

    // Foo is an example field of WebServer. Edit webserver_types.go to remove/update
    Foo string `json:"foo,omitempty"`
}

保存修改后,执行make manifests重新生成config/crd/bases/my.domain_webservers.yaml

$cat my.domain_webservers.yaml
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.9.2
  creationTimestamp: null
  name: webservers.my.domain
spec:
  group: my.domain
  names:
    kind: WebServer
    listKind: WebServerList
    plural: webservers
    singular: webserver
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: WebServer is the Schema for the webservers API
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            description: WebServerSpec defines the desired state of WebServer
            properties:
              foo:
                description: Foo is an example field of WebServer. Edit webserver_types.go
                  to remove/update
                type: string
              image:
                description: The container image of the webserver
                type: string
              replicas:
                description: The number of replicas that the webserver should have
                type: integer
            type: object
          status:
            description: WebServerStatus defines the observed state of WebServer
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}

一旦定义完CRD,我们就可以将其安装到k8s中:

$make install
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
{Version:kustomize/v3.8.7 GitCommit:ad092cc7a91c07fdf63a2e4b7f13fa588a39af4f BuildDate:2020-11-11T23:14:14Z GoOs:linux GoArch:amd64}
kustomize installed to /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain created

检查安装情况:

$kubectl get crd|grep webservers
webservers.my.domain                                             2022-08-06T21:55:45Z

6. 修改role.yaml

在开始controller开发之前,我们先来为controller后续的运行“铺平道路”,即设置好相应权限。

我们在controller中会为CRD实例创建对应deployment和service,这样就要求controller有操作deployments和services的权限,这样就需要我们修改role.yaml,增加service account: controller-manager 操作deployments和services的权限:

// config/rbac/role.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  creationTimestamp: null
  name: manager-role
rules:
- apiGroups:
  - my.domain
  resources:
  - webservers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - my.domain
  resources:
  - webservers/finalizers
  verbs:
  - update
- apiGroups:
  - my.domain
  resources:
  - webservers/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - apps
  - ""
  resources:
  - services
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch

修改后的role.yaml先放在这里,后续与controller一并部署到k8s上。

7. 实现controller的Reconcile(协调)逻辑

kubebuilder为我们搭好了controller的代码架子,我们只需要在controllers/webserver_controller.go中实现WebServerReconciler的Reconcile方法即可。下面是Reconcile的一个简易流程图,结合这幅图理解代码就容易的多了:

下面是对应的Reconcile方法的代码:

// controllers/webserver_controller.go

func (r *WebServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := r.Log.WithValues("Webserver", req.NamespacedName)

    instance := &mydomainv1.WebServer{}
    err := r.Get(ctx, req.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Return and don't requeue
            log.Info("Webserver resource not found. Ignoring since object must be deleted")
            return ctrl.Result{}, nil
        }

        // Error reading the object - requeue the request.
        log.Error(err, "Failed to get Webserver")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Check if the webserver deployment already exists, if not, create a new one
    found := &appsv1.Deployment{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found)
    if err != nil && errors.IsNotFound(err) {
        // Define a new deployment
        dep := r.deploymentForWebserver(instance)
        log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Deployment created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Deployment")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Ensure the deployment replicas and image are the same as the spec
    var replicas int32 = int32(instance.Spec.Replicas)
    image := instance.Spec.Image

    var needUpd bool
    if *found.Spec.Replicas != replicas {
        log.Info("Deployment spec.replicas change", "from", *found.Spec.Replicas, "to", replicas)
        found.Spec.Replicas = &replicas
        needUpd = true
    }

    if (*found).Spec.Template.Spec.Containers[0].Image != image {
        log.Info("Deployment spec.template.spec.container[0].image change", "from", (*found).Spec.Template.Spec.Containers[0].Image, "to", image)
        found.Spec.Template.Spec.Containers[0].Image = image
        needUpd = true
    }

    if needUpd {
        err = r.Update(ctx, found)
        if err != nil {
            log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Spec updated - return and requeue
        return ctrl.Result{Requeue: true}, nil
    }

    // Check if the webserver service already exists, if not, create a new one
    foundService := &corev1.Service{}
    err = r.Get(ctx, types.NamespacedName{Name: instance.Name + "-service", Namespace: instance.Namespace}, foundService)
    if err != nil && errors.IsNotFound(err) {
        // Define a new service
        srv := r.serviceForWebserver(instance)
        log.Info("Creating a new Service", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
        err = r.Create(ctx, srv)
        if err != nil {
            log.Error(err, "Failed to create new Servie", "Service.Namespace", srv.Namespace, "Service.Name", srv.Name)
            return ctrl.Result{RequeueAfter: time.Second * 5}, err
        }
        // Service created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Service")
        return ctrl.Result{RequeueAfter: time.Second * 5}, err
    }

    // Tbd: Ensure the service state is the same as the spec, your homework

    // reconcile webserver operator in again 10 seconds
    return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}

这里大家可能发现了:原来CRD的controller最终还是将CR翻译为k8s原生Resource,比如service、deployment等。CR的状态变化(比如这里的replicas、image等)最终都转换成了deployment等原生resource的update操作,这就是operator的精髓!理解到这一层,operator对大家来说就不再是什么密不可及的概念了。

有些朋友可能也会发现,上面流程图中似乎没有考虑CR实例被删除时对deployment、service的操作,的确如此。不过对于一个7×24小时运行于后台的服务来说,我们更多关注的是其变更、伸缩、升级等操作,删除是优先级最低的需求。

8. 构建controller image

controller代码写完后,我们就来构建controller的image。通过前文我们知道,这个controller其实就是运行在k8s中的一个deployment下的pod。我们需要构建其image并通过deployment部署到k8s中。

kubebuilder创建的operator工程中包含了Makefile,通过make docker-build即可构建controller image。docker-build使用golang builder image来构建controller源码,不过如果不对Dockerfile稍作修改,你很难编译过去,因为默认GOPROXY在国内无法访问。这里最简单的改造方式是使用vendor构建,下面是改造后的Dockerfile:

# Build the manager binary
FROM golang:1.18 as builder

ENV GOPROXY https://goproxy.cn
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
COPY vendor/ vendor/
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
#RUN go mod download

# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
#FROM gcr.io/distroless/static:nonroot
FROM katanomi/distroless-static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]

下面是构建的步骤:

$go mod vendor
$make docker-build

test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
KUBEBUILDER_ASSETS="/home/tonybai/.local/share/kubebuilder-envtest/k8s/1.24.2-linux-amd64" go test ./... -coverprofile cover.out
?       github.com/bigwhite/webserver-operator    [no test files]
?       github.com/bigwhite/webserver-operator/api/v1    [no test files]
ok      github.com/bigwhite/webserver-operator/controllers    4.530s    coverage: 0.0% of statements
docker build -t bigwhite/webserver-controller:latest .
Sending build context to Docker daemon  47.51MB
Step 1/15 : FROM golang:1.18 as builder
 ---> 2d952adaec1e
Step 2/15 : ENV GOPROXY https://goproxy.cn
 ---> Using cache
 ---> db2b06a078e3
Step 3/15 : WORKDIR /workspace
 ---> Using cache
 ---> cc3c613c19c6
Step 4/15 : COPY go.mod go.mod
 ---> Using cache
 ---> 5fa5c0d89350
Step 5/15 : COPY go.sum go.sum
 ---> Using cache
 ---> 71669cd0fe8e
Step 6/15 : COPY vendor/ vendor/
 ---> Using cache
 ---> 502b280a0e67
Step 7/15 : COPY main.go main.go
 ---> Using cache
 ---> 0c59a69091bb
Step 8/15 : COPY api/ api/
 ---> Using cache
 ---> 2b81131c681f
Step 9/15 : COPY controllers/ controllers/
 ---> Using cache
 ---> e3fd48c88ccb
Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go
 ---> Using cache
 ---> 548ac10321a2
Step 11/15 : FROM katanomi/distroless-static:nonroot
 ---> 421f180b71d8
Step 12/15 : WORKDIR /
 ---> Running in ea7cb03027c0
Removing intermediate container ea7cb03027c0
 ---> 9d3c0ea19c3b
Step 13/15 : COPY --from=builder /workspace/manager .
 ---> a4387fe33ab7
Step 14/15 : USER 65532:65532
 ---> Running in 739a32d251b6
Removing intermediate container 739a32d251b6
 ---> 52ae8742f9c5
Step 15/15 : ENTRYPOINT ["/manager"]
 ---> Running in 897893b0c9df
Removing intermediate container 897893b0c9df
 ---> e375cc2adb08
Successfully built e375cc2adb08
Successfully tagged bigwhite/webserver-controller:latest

注:执行make命令之前,先将Makefile中的IMG变量初值改为IMG ?= bigwhite/webserver-controller:latest

构建成功后,执行make docker-push将image推送到镜像仓库中(这里使用了docker公司提供的公共仓库)。

9. 部署controller

之前我们已经通过make install将CRD安装到k8s中了,接下来再把controller部署到k8s上,我们的operator就算部署完毕了。执行make deploy即可实现部署:

$make deploy
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; }
cd config/manager && /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize edit set image controller=bigwhite/webserver-controller:latest
/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/webserver-operator-system created
customresourcedefinition.apiextensions.k8s.io/webservers.my.domain unchanged
serviceaccount/webserver-operator-controller-manager created
role.rbac.authorization.k8s.io/webserver-operator-leader-election-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-manager-role created
clusterrole.rbac.authorization.k8s.io/webserver-operator-metrics-reader created
clusterrole.rbac.authorization.k8s.io/webserver-operator-proxy-role created
rolebinding.rbac.authorization.k8s.io/webserver-operator-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-proxy-rolebinding created
configmap/webserver-operator-manager-config created
service/webserver-operator-controller-manager-metrics-service created
deployment.apps/webserver-operator-controller-manager created

我们看到deploy不仅会安装controller、serviceaccount、role、rolebinding,它还会创建namespace,也会将crd安装一遍。也就是说deploy是一个完整的operator安装命令。

注:使用make undeploy可以完整卸载operator相关resource。

我们用kubectl logs查看一下controller的运行日志:

$kubectl logs -f deployment.apps/webserver-operator-controller-manager -n webserver-operator-system
1.6600280818476188e+09    INFO    controller-runtime.metrics    Metrics server is starting to listen    {"addr": "127.0.0.1:8080"}
1.6600280818478029e+09    INFO    setup    starting manager
1.6600280818480284e+09    INFO    Starting server    {"path": "/metrics", "kind": "metrics", "addr": "127.0.0.1:8080"}
1.660028081848097e+09    INFO    Starting server    {"kind": "health probe", "addr": "[::]:8081"}
I0809 06:54:41.848093       1 leaderelection.go:248] attempting to acquire leader lease webserver-operator-system/63e5a746.my.domain...
I0809 06:54:57.072336       1 leaderelection.go:258] successfully acquired lease webserver-operator-system/63e5a746.my.domain
1.6600280970724037e+09    DEBUG    events    Normal    {"object": {"kind":"Lease","namespace":"webserver-operator-system","name":"63e5a746.my.domain","uid":"e05aaeb5-4a3a-4272-b036-80d61f0b6788","apiVersion":"coordination.k8s.io/v1","resourceVersion":"5238800"}, "reason": "LeaderElection", "message": "webserver-operator-controller-manager-6f45bc88f7-ptxlc_0e960015-9fbe-466d-a6b1-ff31af63a797 became leader"}
1.6600280970724993e+09    INFO    Starting EventSource    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "source": "kind source: *v1.WebServer"}
1.6600280970725305e+09    INFO    Starting Controller    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer"}
1.660028097173026e+09    INFO    Starting workers    {"controller": "webserver", "controllerGroup": "my.domain", "controllerKind": "WebServer", "worker count": 1}

可以看到,controller已经成功启动,正在等待一个WebServer CR的相关事件(比如创建)!下面我们就来创建一个WebServer CR!

10. 创建WebServer CR

webserver-operator项目中有一个CR sample,位于config/samples下面,我们对其进行改造,添加我们在spec中加入的字段:

// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 3

我们通过kubectl创建该WebServer CR:

$cd config/samples
$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample created

观察controller的日志:

1.6602084232243123e+09  INFO    controllers.WebServer   Creating a new Deployment   {"Webserver": "default/webserver-sample", "Deployment.Namespace": "default", "Deployment.Name": "webserver-sample"}
1.6602084233446114e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}

我们看到当CR被创建后,controller监听到相关事件,创建了对应的Deployment和service,我们查看一下为CR创建的Deployment、三个Pod以及service:

$kubectl get service
NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes                 ClusterIP   172.26.0.1     <none>        443/TCP        22d
webserver-sample-service   NodePort    172.26.173.0   <none>        80:30010/TCP   2m58s

$kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
webserver-sample   3/3     3            3           4m44s

$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          4m52s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          4m52s

我们访问一下该服务:

$curl http://192.168.10.182:30010
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

服务如预期返回响应!

11. 伸缩、变更版本和Service自愈

接下来我们来对CR做一些常见的运维操作。

  • 副本数由3变为4

我们将CR的replicas由3改为4,对容器实例做一次扩展操作:

// config/samples/_v1_webserver.yaml 

apiVersion: my.domain/v1
kind: WebServer
metadata:
  name: webserver-sample
spec:
  # TODO(user): Add fields here
  image: nginx:1.23.1
  replicas: 4

然后通过kubectl apply使之生效:

$kubectl apply -f _v1_webserver.yaml
webserver.my.domain/webserver-sample configured

上述命令执行后,我们观察到operator的controller日志如下:

1.660208962767797e+09   INFO    controllers.WebServer   Deployment spec.replicas change {"Webserver": "default/webserver-sample", "from": 3, "to": 4}

稍后,查看pod数量:

$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-v9gvg   1/1     Running   0          42s
webserver-sample-bc698b9fb-vk6gw   1/1     Running   0          9m41s
webserver-sample-bc698b9fb-xgrgb   1/1     Running   0          9m41s

webserver pod副本数量成功从3扩为4。

  • 变更webserver image版本

我们将CR的image的版本从nginx:1.23.1改为nginx:1.23.0,然后执行kubectl apply使之生效。

我们查看controller的响应日志如下:

1.6602090494113188e+09  INFO    controllers.WebServer   Deployment spec.template.spec.container[0].image change {"Webserver": "default/webserver-sample", "from": "nginx:1.23.1", "to": "nginx:1.23.0"}

controller会更新deployment,导致所辖pod进行滚动升级:

$kubectl get pods
NAME                               READY   STATUS              RESTARTS   AGE
webserver-sample-bc698b9fb-8gq2h   1/1     Running             0          10m
webserver-sample-bc698b9fb-vk6gw   1/1     Running             0          10m
webserver-sample-bc698b9fb-xgrgb   1/1     Running             0          10m
webserver-sample-ffcf549ff-g6whk   0/1     ContainerCreating   0          12s
webserver-sample-ffcf549ff-ngjz6   0/1     ContainerCreating   0          12s

耐心等一小会儿,最终的pod列表为:

$kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
webserver-sample-ffcf549ff-g6whk   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-m6z24   1/1     Running   0          3m12s
webserver-sample-ffcf549ff-ngjz6   1/1     Running   0          6m22s
webserver-sample-ffcf549ff-t7gvc   1/1     Running   0          4m16s
  • service自愈:恢复被无删除的Service

我们来一次“误操作”,将webserver-sample-service删除,看看controller能否帮助service自愈:

$kubectl delete service/webserver-sample-service
service "webserver-sample-service" deleted

查看controller日志:

1.6602096994710526e+09  INFO    controllers.WebServer   Creating a new Service  {"Webserver": "default/webserver-sample", "Service.Namespace": "default", "Service.Name": "webserver-sample-service"}

我们看到controller检测到了service被删除的状态,并重建了一个新service!

访问新建的service:

$curl http://192.168.10.182:30010
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

可以看到service在controller的帮助下完成了自愈!

五. 小结

本文对Kubernetes Operator的概念以及优点做了初步的介绍,并基于kubebuilder这个工具开发了一个具有2级能力的operator。当然这个operator离完善还有很远的距离,其主要目的还是帮助大家理解operator的概念以及实现套路。

相信你阅读完本文后,对operator,尤其是其基本结构会有一个较为清晰的了解,并具备开发简单operator的能力!

文中涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/webserver-operator。

六. 参考资料

  • kubernetes operator 101, Part 1: Overview and key features – https://developers.redhat.com/articles/2021/06/11/kubernetes-operators-101-part-1-overview-and-key-features
  • Kubernetes Operators 101, Part 2: How operators work – https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work
  • Operator SDK: Build Kubernetes Operators – https://developers.redhat.com/blog/2020/04/28/operator-sdk-build-kubernetes-operators-and-deploy-them-on-openshift
  • kubernetes doc: Custom Resources – https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
  • kubernetes doc: Operator pattern – https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
  • kubernetes doc: API concepts – https://kubernetes.io/docs/reference/using-api/api-concepts/
  • Introducing Operators: Putting Operational Knowledge into Software 第一篇有关operator的文章 by coreos – https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html
  • CNCF Operator白皮书v1.0 – https://github.com/cncf/tag-app-delivery/blob/main/operator-whitepaper/v1/Operator-WhitePaper_v1-0.md
  • Best practices for building Kubernetes Operators and stateful apps – https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-stateful-apps
  • A deep dive into Kubernetes controllers – https://docs.bitnami.com/tutorials/a-deep-dive-into-kubernetes-controllers
  • Kubernetes Operators Explained – https://blog.container-solutions.com/kubernetes-operators-explained
  • 书籍《Kubernetes Operator》 – https://book.douban.com/subject/34796009/
  • 书籍《Programming Kubernetes》 – https://book.douban.com/subject/35498478/
  • Operator SDK Reaches v1.0 – https://cloud.redhat.com/blog/operator-sdk-reaches-v1.0
  • What is the difference between kubebuilder and operator-sdk – https://github.com/operator-framework/operator-sdk/issues/1758
  • Kubernetes Operators in Depth – https://www.infoq.com/articles/kubernetes-operators-in-depth/
  • Get started using Kubernetes Operators – https://developer.ibm.com/learningpaths/kubernetes-operators/
  • Use Kubernetes operators to extend Kubernetes’ functionality – https://developer.ibm.com/learningpaths/kubernetes-operators/operators-extend-kubernetes/
  • memcached operator – https://github.com/operator-framework/operator-sdk-samples/tree/master/go/memcached-operator

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats