cloudenative1-2
GO语言特性
module1的第二次课
---------------------------
5.函数
module1/helloworld/main.go
package main
import (
"flag"
"fmt"
"os"
)
func main() {
name := flag.String("name", "world", "specify the name you want to say hi")
flag.Parse()
fmt.Println("os args is:", os.Args)
fmt.Println("input parameter is:", *name)
fullString := fmt.Sprintf("Hello %s from Go\n", *name)
fmt.Println(fullString)
}
func DuplicateString(input string) (error, string) {
if input == "aaa" {
return fmt.Errorf("aaa is not allowed"), ""
}
return nil, input + input
}
$ go build main.go
zxl@qwq:~/go/src/github.com/cncamp/golang/examples/module1/helloworld$ ./main
os args is: [./main]
input parameter is: world
Hello world from Go
$ ./main aaa
os args is: [./main aaa]
input parameter is: world
Hello world from Go
$ ./main --name jesse
os args is: [./main --name jesse]
input parameter is: jesse
Hello jesse from Go
err, result := DuplicateString("aaa")
if err == nil {
fmt.Println(result)
} else {
fmt.Println(err)
}
---------------------------
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()
---------------------------
Init 函数
-Init 函数:会在包初始化时运行
-谨慎使用 init 函数
--当多个依赖项目引用统一项目,且被引用项目的初始化在 init 中完成,并且不可重复运行时,会导
致启动错误
package main
var myVariable = 0
func init() {
myVariable = 1
}
例子:module1/init/main.go
package main
import (
"fmt"
_ "github.com/cncamp/golang/examples/module1/init/a"
_ "github.com/cncamp/golang/examples/module1/init/b"
)
func init() {
fmt.Println("main init")
}
func main() {
}
init from b
init from a
main init
kubernetes->glog->init->flag parse parameter
kubernetes->a->vendor->glog->init->flag parse parameter
a应该直接用上面的glog,不应该自己vendor一份
glog已经废弃,切到klog
---------------------------
返回值
-多值返回
•函数可以返回任意数量的返回值
-命名返回值
•Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。
•返回值的名称应当具有一定的意义,它可以作为文档使用。
•没有参数的 return 语句返回已命名的返回值。也就是直接返回。
-调用者忽略部分返回值
result, _ = strconv.Atoi(origStr)
---------------------------
传递变长参数
Go 语言中的可变长参数允许调用方传递任意多个相同类型的参数
• 函数定义
func append(slice []Type, elems ...Type) []Type
• 调用方法
myArray := []string{}
myArray = append(myArray, "a","b","c")
---------------------------
内置函数
close 管道关闭
len, cap 返回数组、切片,Map 的长度或容量
new, make 内存分配
copy, append 操作切片
panic, recover 错误处理
print, println 打印
complex, real, imag 操作复数
---------------------------
回调函数(Callback)
• 函数作为参数传入其它函数,并在其他函数内部调用执行
strings.IndexFunc(line, unicode.IsSpace)
Kubernetes controller的leaderelection
示例:
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)
}
---------------------------
闭包
匿名函数
•不能独立存在
•可以赋值给其他变量
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”)
}
}()
---------------------------
方法
• 方法:作用在接收者上的函数--为interface,struct定义方法
func (recv receiver_type) methodName(parameter_list) (return_value_list)
函数调用,是要告诉调用方调用哪个包里面的函数。
method是定义在接收者上面的。
•使用场景
-很多场景下,函数需要的上下文可以保存在receiver属性中,
通过定义 receiver 的方法,该方法可以直接访问 receiver 属性,减少参数传递需求
// StartTLS starts TLS on a server from NewUnstartedServer.
func (s *Server) StartTLS() {
if s.URL != “” {
panic(“Server already started”)
}
if s.client == nil {
s.client = &http.Client{Transport: &http.Transport{}}
}
---------------------------
传值还是传指针
• Go 语言只有一种规则-传值
• 函数内修改参数的值不会影响函数外原始变量的值
• 可以传递指针参数将变量地址传递给调用函数,Go 语言会
复制该指针作为函数内的地址,但指向同一地址
• 思考:当我们写代码的时候,函数的参数传递应该用struct
还是pointer?
例子:module1/pointer/main.go
func changeParameter(para *ParameterStruct, value string) {
para.Name = value
}
func cannotChangeParameter(para ParameterStruct, value string) {
para.Name = value
}
---------------------------
接口
• 接口定义一组方法集合
type IF interface {
Method1(param_list) return_type
}
• 适用场景:Kubernetes 中有大量的接口抽象和多种实现
• Struct 无需显示声明实现 interface,只需直接实现方法
• Struct 除实现 interface 定义的接口外,还可以有额外的方法
• 一个类型可实现多个接口(Go 语言的多重继承)
• Go 语言中接口不接受属性定义 -- JAVA在接口里面可以有property
• 接口可以嵌套其他接口
---------------------------
接口
例子:module1\interface\main.go
type IF interface {
getName() string
}
type Human struct {
firstName,lastName string
}
func (h *Human) getName() string {
return h.firstName + "," + h.lastName
}
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 的引用不会出现空指针
---------------------------
反射机制
reflect.TypeOf ()返回被检查对象的类型
•reflect.ValueOf()返回被检查对象的值
•示例
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 的反射
// 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)
反射机制有性能损耗
---------------------------
Go 语言中的面向对象编程
可见性控制
•public - 常量、变量、类型、接口、结构、函数等的名称大写
•private - 非大写就只能在包内使用
继承
•通过组合实现,内嵌一个或多个 struct
多态
•通过接口实现,通过接口定义方法集,编写多套实现
---------------------------
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 编解码
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)
}
}
---------------------------
6.常用语法
---------------------------
错误处理
•Go 语言无内置 exception 机制,只提供 error 接口供定义错误
type error interface {
Error() string
}
•可通过 errors.New 或 fmt.Errorf 创建新的 error
var errNotFound error = errors.New("NotFound")
•通常应用程序对 error 的处理大部分是判断 error 是否为 nil
如需将 error 归类,通常交给应用程序自定义,比如 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
}
---------------------------
defer
•函数返回之前执行某个语句或函数
等同于 Java 和 C# 的 finally
• 常见的 defer 使用场景:记得关闭你打开的资源
•defer file.Close()
•defer mu.Unlock()
•defer println("")
---------------------------
Panic 和 recover
• panic: 可在系统出现不可恢复错误时主动调用 panic, panic 会使当前线程直接 crash
• defer: 保证执行并把控制权交还给接收到 panic 的函数调用者
• recover: 函数从 panic 或 错误场景中恢复
defer func() {
fmt.Println("defer func is called")
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic("a panic is triggered")
---------------------------
7.多线程
---------------------------
并发和并行
并发(concurrency)
•两个或多个事件在同一时间间隔发生
并行(parallellism)
两个或者多个事件在同一时刻发生
---------------------------
协程
进程:
•分配系统资源(CPU 时间、内存等)基本单位
•有独立的内存空间,切换开销大
线程:进程的一个执行流,是 CPU 调度并能独立运行的的基本单位
•同一进程中的多线程共享内存空间,线程切换代价小
•多线程通信方便
•从内核层面来看线程其实也是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共
享了地址空间和信号处理函数
协程
•Go 语言中的轻量级线程实现
•Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行
或者进行系统调用时,会主动把当前 goroutine 的 CPU (P) 转让出去,让其他 goroutine 能被调度
并执行,也就是 Golang 从语言层面支持了协程
---------------------------
Communicating Sequential Process
CSP
•描述两个独立的并发实体通过共享的通讯 channel 进行通信的并发模型。
Go 协程 goroutine
•是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协
作式调度。
•是一种绿色线程,微线程,它与 Coroutine 协程也有区别,能够在发现堵塞后启动新的微线程。
通道 channel
•类似 Unix 的 Pipe,用于协程之间通讯和同步。
•协程之间虽然解耦,但是它们和 Channel 有着耦合。
---------------------------
线程和协程的差异
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少
• goroutine:2KB
• 线程:8MB
线程/goroutine 切换开销方面,goroutine 远比线程小
• 线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新
• goroutine:只有三个寄存器的值修改 - PC / SP / DX.
GOMAXPROCS
•控制并行线程数量
---------------------------
协程示例
• 启动新协程:go functionName()
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
time.Sleep(time.Second)
module1/defer/main.go
for i := 0; i < 3; i++ {
go func(i int) {
// lock.Lock()
// defer lock.Unlock()
fmt.Println("loopFunc:", i)
}(i)
}
go fmt.println("loopFunc:",i) --乱序执行
---------------------------
channel - 多线程通信
Channel 是多个协程之间通讯的管道
•一端发送数据,一端接收数据
•同一时间只有一个协程可以访问数据,无共享内存模式可能出现的内存竞争
•协调协程的执行顺序
声明方式
•var identifier chan datatype
•操作符<-
示例
ch := make(chan int)
go func() {
fmt.Println("hello from goroutine")
ch <- 0 //数据写入Channel
}()
i := <-ch//从Channel中取数据并赋值
实现无锁方式消息传递
---------------------------
通道缓冲
• 基于 Channel 的通信是同步的
• 当缓冲区满时,数据的发送是阻塞的
• 通过 make 关键字创建通道时可定义缓冲区容量,默认缓冲区容量为 0
下面两个定义的区别?
•ch := make(chan int)
•ch := make(chan int,1)
---------------------------
遍历通道缓冲区
ch := 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)
ch <- n
}
close(ch)
}()
fmt.Println("hello from main")
for v := range ch {
fmt.Println("receiving: ", v)
}
---------------------------
单向通道--配合双向通道使用
只发送通道
•var sendOnly chan<- int
只接收通道
•var readOnly <-chan int
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 }
}
---------------------------
关闭通道
•通道无需每次关闭
•关闭的作用是告诉接收者该通道再无新数据发送
•只有发送方需要关闭通道
ch := make(chan int)
defer close(ch)
if v, notClosed := <-ch; notClosed {
fmt.Println(v)
}
---------------------------
select
当多个协程同时运行时,可通过 select 轮询多个通道
•如果所有通道都阻塞则等待,如定义了 default 则执行 default
•如多个通道就绪则随机选择
select {
case v:= <- ch1:
...
case v:= <- ch2:
...
default:
...
}
---------------------------
定时器 Timer
time.Ticker 以指定的时间间隔重复的向通道 C 发送时间值
使用场景
•为协程设定超时时间
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")
}
---------------------------
---------------------------
上下文 Context
超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作
Context 是设置截止日期、同步信号,传递请求相关值的结构体
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
用法
•context.Background
Background通常被用于主函数、初始化和测试中,作为一个顶层context
•context.TODO
TODO是在不确定使用什么context时才会用
•context.WithDeadline
超时时间
•context.WithValue
向context添加键值对
•context.WithCancel
可取消的context
---------------------------
如何停止一个子协程
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())
1)基于context传递键值对
module1/context/context/main.go
baseCtx := context.Background()
ctx := context.WithValue(baseCtx, "a", "b")
go func(c context.Context) {
fmt.Println(c.Value("a"))
}(ctx)
2)超时的context触发协程退出
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!")
}
---------------------------
课后练习1.2
• 基于 Channel 编写一个简单的单线程生产者消费者模型
• 队列:
队列长度10,队列元素类型为 int
• 生产者:
每1秒往队列中放入一个类型为 int 的元素,队列满时生产者可以阻塞
• 消费者:
每一秒从队列中获取一个元素并打印,队列为空时消费者阻塞
---------------------------