• [知识分享] 实践GoF的23种设计模式:装饰者模式
    >摘要:装饰者模式通过组合的方式,提供了能够动态地给对象/模块扩展新功能的能力。理论上,只要没有限制,它可以一直把功能叠加下去,具有很高的灵活性。 本文分享自华为云社区《[【Go实现】实践GoF的23种设计模式:装饰者模式](https://bbs.huaweicloud.com/blogs/362755?utm_source=csdn&utm_medium=bbs-ex&utm_campaign=other&utm_content=content)》,作者: 元闰子。 # 简介 我们经常会遇到“**给现有对象/模块新增功能**”的场景,比如 http router 的开发场景下,除了最基础的路由功能之外,我们常常还会加上如日志、鉴权、流控等 middleware。如果你查看框架的源码,就会发现 middleware 功能的实现用的就是**装饰者模式**(Decorator Pattern)。 GoF 给装饰者模式的定义如下: >Decorators provide a flexible alternative to subclassing for extending functionality. Attach additional responsibilities to an object dynamically. 简单来说,装饰者模式通过组合的方式,提供了能够动态地给对象/模块扩展新功能的能力。理论上,只要没有限制,它可以一直把功能叠加下去,具有很高的灵活性。 如果写过 Java,那么一定对 I/O Stream 体系不陌生,它是装饰者模式的经典用法,客户端程序可以动态地为原始的输入输出流添加功能,比如按字符串输入输出,加入缓冲等,使得整个 I/O Stream 体系具有很高的可扩展性和灵活性。 # UML 结构 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/1/1656639343791508197.png) # 场景上下文 在简单的分布式应用系统(示例代码工程)中,我们设计了 Sidecar 边车模块,它的用处主要是为了 1)方便扩展 network.Socket 的功能,如增加日志、流控等非业务功能;2)让这些附加功能对业务程序隐藏起来,也即业务程序只须关心看到 network.Socket 接口即可。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/1/1656639359370480443.png) # 代码实现 Sidecar 的这个功能场景,很适合使用装饰者模式来实现,代码如下: ``` // demo/network/socket.go package network ​ // 关键点1: 定义被装饰的抽象接口 // Socket 网络通信Socket接口 type Socket interface { // Listen 在endpoint指向地址上起监听 Listen(endpoint Endpoint) error // Close 关闭监听 Close(endpoint Endpoint) // Send 发送网络报文 Send(packet *Packet) error // Receive 接收网络报文 Receive(packet *Packet) // AddListener 增加网络报文监听者 AddListener(listener SocketListener) } ​ // 关键点2: 提供一个默认的基础实现 type socketImpl struct { listener SocketListener } ​ func DefaultSocket() *socketImpl { return &socketImpl{} } ​ func (s *socketImpl) Listen(endpoint Endpoint) error { return Instance().Listen(endpoint, s) } ... // socketImpl的其他Socket实现方法 ​ ​ // demo/sidecar/flowctrl_sidecar.go package sidecar ​ // 关键点3: 定义装饰器,实现被装饰的接口 // FlowCtrlSidecar HTTP接收端流控功能装饰器,自动拦截Socket接收报文,实现流控功能 type FlowCtrlSidecar struct { // 关键点4: 装饰器持有被装饰的抽象接口作为成员属性 socket network.Socket ctx *flowctrl.Context } ​ // 关键点5: 对于需要扩展功能的方法,新增扩展功能 func (f *FlowCtrlSidecar) Receive(packet *network.Packet) { httpReq, ok := packet.Payload().(*http.Request) // 如果不是HTTP请求,则不做流控处理 if !ok { f.socket.Receive(packet) return } // 流控后返回429 Too Many Request响应 if !f.ctx.TryAccept() { httpResp := http.ResponseOfId(httpReq.ReqId()). AddStatusCode(http.StatusTooManyRequest). AddProblemDetails("enter flow ctrl state") f.socket.Send(network.NewPacket(packet.Dest(), packet.Src(), httpResp)) return } f.socket.Receive(packet) } ​ // 关键点6: 不需要扩展功能的方法,直接调用被装饰接口的原生方法即可 func (f *FlowCtrlSidecar) Close(endpoint network.Endpoint) { f.socket.Close(endpoint) } ... // FlowCtrlSidecar的其他方法 ​ // 关键点7: 定义装饰器的工厂方法,入参为被装饰接口 func NewFlowCtrlSidecar(socket network.Socket) *FlowCtrlSidecar { return &FlowCtrlSidecar{ socket: socket, ctx: flowctrl.NewContext(), } } ​ // demo/sidecar/all_in_one_sidecar_factory.go // 关键点8: 使用时,通过装饰器的工厂方法,把所有装饰器和被装饰者串联起来 func (a AllInOneFactory) Create() network.Socket { return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), a.producer) } ``` 总结实现装饰者模式的几个关键点: 1. 定义需要被装饰的抽象接口,后续的装饰器都是基于该接口进行扩展。 2. 为抽象接口提供一个基础实现。 3. 定义装饰器,并实现被装饰的抽象接口。 4. 装饰器持有被装饰的抽象接口作为成员属性。“装饰”的意思是在原有功能的基础上扩展新功能,因此必须持有原有功能的抽象接口。 5. 在装饰器中,对于需要扩展功能的方法,新增扩展功能。 6. 不需要扩展功能的方法,直接调用被装饰接口的原生方法即可。 7. 为装饰器定义一个工厂方法,入参为被装饰接口。 8. 使用时,通过装饰器的工厂方法,把所有装饰器和被装饰者串联起来。 # 扩展 ## Go 风格的实现 在 Sidecar 的场景上下文中,被装饰的 Socket 是一个相对复杂的接口,装饰器通过实现 Socket 接口来进行功能扩展,是典型的面向对象风格。 如果被装饰者是一个简单的接口/方法/函数,我们可以用更具 Go 风格的实现方式,考虑前文提到的 http router 场景。如果你使用原生的 net/http 进行 http router 开发,通常会这么实现: ``` func main() { // 注册/hello的router http.HandleFunc("/hello", hello) // 启动http服务器 http.ListenAndServe("localhost:8080", nil) } ​ // 具体的请求处理逻辑,类型是 http.HandlerFunc func hello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello, world")) } ``` 其中,我们通过 http.HandleFunc 来注册具体的 router, hello 是具体的请求处理方法。现在,我们想为该 http 服务器增加日志、鉴权等通用功能,那么可以把 func(w http.ResponseWriter, r *http.Request) 作为被装饰的抽象接口,通过新增日志、鉴权等装饰器完成功能扩展。 ``` // demo/network/http/http_handle_func_decorator.go ​ // 关键点1: 确定被装饰接口,这里为原生的http.HandlerFunc type HandlerFunc func(ResponseWriter, *Request) ​ // 关键点2: 定义装饰器类型,是一个函数类型,入参和返回值都是 http.HandlerFunc 函数 type HttpHandlerFuncDecorator func(http.HandlerFunc) http.HandlerFunc ​ // 关键点3: 定义装饰函数,入参为被装饰的接口和装饰器可变列表 func Decorate(h http.HandlerFunc, decorators ...HttpHandlerFuncDecorator) http.HandlerFunc { // 关键点4: 通过for循环遍历装饰器,完成对被装饰接口的装饰 for _, decorator := range decorators { h = decorator(h) } return h } ​ // 关键点5: 实现具体的装饰器 func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("Auth") if err != nil || cookie.Value != "Pass" { w.WriteHeader(http.StatusForbidden) return } // 关键点6: 完成功能扩展之后,调用被装饰的方法,才能将所有装饰器和被装饰者串起来 h(w, r) } } ​ func WithLogger(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Println(r.Form) log.Printf("path %s", r.URL.Path) h(w, r) } } ​ func hello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello, world")) } ​ func main() { // 关键点7: 通过Decorate函数完成对hello的装饰 http.HandleFunc("/hello", Decorate(hello, WithLogger, WithBasicAuth)) // 启动http服务器 http.ListenAndServe("localhost:8080", nil) } ``` 上述的装饰者模式的实现,用到了类似于 Functional Options 的技巧,也是巧妙利用了 Go 的函数式编程的特点,总结下来有如下几个关键点: 1. 确定被装饰的接口,上述例子为 http.HandlerFunc。 2. 定义装饰器类型,是一个函数类型,入参和返回值都是被装饰接口,上述例子为 func(http.HandlerFunc) http.HandlerFunc。 3. 定义装饰函数,入参为被装饰的接口和装饰器可变列表,上述例子为 Decorate 方法。 4. 在装饰方法中,通过for循环遍历装饰器,完成对被装饰接口的装饰。这里是用来类似 Functional Options 的技巧,一定要注意装饰器的顺序! 5. 实现具体的装饰器,上述例子为 WithBasicAuth 和 WithLogger 函数。 6. 在装饰器中,完成功能扩展之后,记得调用被装饰者的接口,这样才能将所有装饰器和被装饰者串起来。 7. 在使用时,通过装饰函数完成对被装饰者的装饰,上述例子为 Decorate(hello, WithLogger, WithBasicAuth)。 ## Go 标准库中的装饰者模式 在 Go 标准库中,也有一个运用了装饰者模式的模块,就是 context,其中关键的接口如下: ``` package context ​ // 被装饰接口 type Context interface { Deadline() (deadline time.Time, ok bool) Done() -chan struct{} Err() error Value(key any) any } ​ // cancel装饰器 type cancelCtx struct { Context // 被装饰接口 mu sync.Mutex done atomic.Value children map[canceler]struct{}= err error } // cancel装饰器的工厂方法 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { // ... c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } } ​ // timer装饰器 type timerCtx struct { cancelCtx // 被装饰接口 timer *time.Timer ​ deadline time.Time } // timer装饰器的工厂方法 func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { // ... c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } // ... return c, func() { c.cancel(true, Canceled) } } // timer装饰器的工厂方法 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } ​ // value装饰器 type valueCtx struct { Context // 被装饰接口 key, val any } // value装饰器的工厂方法 func WithValue(parent Context, key, val any) Context { if parent == nil { panic("cannot create context from nil parent") } // ... return &valueCtx{parent, key, val} } ``` ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/1/1656639484664736619.png) 使用时,可以这样: ``` // 使用时,可以这样 func main() { ctx := context.Background() ctx = context.WithValue(ctx, "key1", "value1") ctx, _ = context.WithTimeout(ctx, time.Duration(1)) ctx = context.WithValue(ctx, "key2", "value2") } ``` 不管是 UML 结构,还是使用方法,context 模块都与传统的装饰者模式有一定出入,但也不妨碍 context 是装饰者模式的典型运用。还是那句话,**学习设计模式,不能只记住它的结构,而是学习其中的动机和原理**。 ## 典型使用场景 - I/O 流,比如为原始的 I/O 流增加缓冲、压缩等功能。 - Http Router,比如为基础的 Http Router 能力增加日志、鉴权、Cookie等功能。 - ...... ## 优缺点 **优点** 1. 遵循开闭原则,能够在不修改老代码的情况下扩展新功能。 2. 可以用多个装饰器把多个功能组合起来,理论上可以无限组合。 **缺点** 1. 一定要注意装饰器装饰的顺序,否则容易出现不在预期内的行为。 2. 当装饰器越来越多之后,系统也会变得复杂。 ## 与其他模式的关联 装饰者模式和代理模式具有很高的相似性,但是两种所强调的点不一样。前者强调的是为本体对象添加新的功能;后者强调的是对本体对象的访问控制。 装饰者模式和适配器模式的区别是,前者只会扩展功能而不会修改接口;后者则会修改接口。
  • [技术干货] GO语言类型系统之结构体与初始化笔记分享
    Go语言的结构体(struct)和其他语言的类(class)有同等的地位,但Go语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个 基础的特性。 组合甚至不能算面向对象特性,因为在C语言这样的过程式编程语言中,也有结构体,也有组合。组合只是形成复合类型的基础。 type Rect struct {     x, y float64     width, height float64 } 然后我们定义成员方法Area()来计算矩形的面积: func (r *Rect) Area() float64 {     return r.width * r.height } 可以看出, Go语言中结构体的使用方式与C语言并没有明显不同。在定义了Rect类型后,该如何创建并初始化Rect类型的对象实例呢?这可以通过如下几种方法实现: rect1 := new(Rect) rect2 := &Rect{} rect3 := &Rect{0, 0, 100, 200} rect4 := &Rect{width: 100, height: 200} 在Go语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如bool类型的零值为false,int类型的零值为0,string类型的零值为空字符串。 在Go语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以NewXXX来命名,表示“构造函数”: func NewRect(x, y, width, height float64) *Rect {     return &Rect{x, y, width, height} }
  • [技术干货] GO语言类型系统之值语义和引用语义笔记分享
    值语义和引用语义的差别在于赋值,比如: b = a b.Modify() 如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型。 Go语言中的大多数类型都基于值语义,包括: 基本类型,如byte、int、bool、float32、float64和string等; 复合类型,如数组(array)、结构体(struct)和指针(pointer)等。 Go语言中类型的值语义表现得非常彻底。我们之所以这么说,是因为数组。 Go语言中的数组和基本类型没有区别,是很纯粹的值类型,例如: var a = [3]int{1, 2, 3} var b = a b[1]++ fmt.Println(a, b) 该程序的运行结果如下: [1 2 3] [1 3 3]。 这表明b=a赋值语句是数组内容的完整复制。要想表达引用,需要用指针: var a = [3]int{1, 2, 3} var b = &a b[1]++ fmt.Println(a, *b) 该程序的运行结果如下: [1 3 3] [1 3 3] 这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int类型。 Go语言中有4个类型比较特别,看起来像引用类型,如下所示。 数组切片:指向数组(array)的一个区间。 map:极其常见的数据结构,提供键值查询能力。 channel:执行体(goroutine)间的通信设施。 q 接口(interface):对一组满足某个契约的类型的抽象。 但是这并不影响我们将Go语言类型看做值语义。下面我们来看看这4个类型。 数组切片本质上是一个区间,你可以大致将[]T表示为: type slice struct {     first *T 2     len int     cap int } 因为数组切片内部是指向数组的指针,所以可以改变所指向的数组元素并不奇怪。数组切片类型本身的赋值仍然是值语义。            map本质上是一个字典指针,你可以大致将map[K]V表示为:                                            4 type Map_K_V struct {  // ... }                type map[K]V struct {                                                          5     impl *Map_K_V } 基于指针,我们完全可以自定义一个引用类型,如:               type IntegerRef struct {                                                       6    impl *int } channel和map类似,本质上是一个指针。将它们设计为引用类型而不是统一的值类型的原因是,完整复制一个channel或map并不是常规需求。 7 同样,接口具备引用语义,是因为内部维持了两个指针,示意为: type interface struct {     data *void     itab *Itab               }                                                                              8 接口在Go语言中的地位非常重要。
  • [技术干货] GO语言类型系统之为类型添加方法笔记分享
    在Go语言中,你可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法: type Integer int  func (a Integer) Less(b Integer) bool {     return a < b } 在这个例子中,我们定义了一个新类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法Less()。 这样实现了Integer后,就可以让整型像一个普通的类一样使用: func main() {     var a Integer = 1     if a.Less(2) {         fmt.Println(a, "Less 2")     } } 在学其他语言(尤其是C++语言)的时候,很多初学者对面向对象的概念感觉很神秘,不知道那些继承和多态到底是怎么发生的。不过,如果读者曾经深入了解过C++的对象模型,或者完整阅读过《深度探索C++对象模型》这本书,就会理解C++等语言中的面向对象都只是相当于在C语言基础上添加的一个语法糖,接下来解释一下为什么可以这么理解。 type Integer int  func Integer_Less(a Integer, b Integer) bool {     return a < b }  func main() {     var a Integer = 1     if Integer_Less(a, 2) {         fmt.Println(a, "Less 2")     } } 在Go语言中,面向对象的神秘面纱被剥得一干二净。对比下面的两段代码: func (a Integer) Less(b Integer) bool {     // 面向对象     return a < b }  func Integer_Less(a Integer, b Integer) bool {  // 面向过程     return a < b }    a.Less(2)              // 面向对象的用法 Integer_Less(a, 2)    // 面向过程的用法 可以看出,面向对象只是换了一种语法形式来表达。C++语言的面向对象之所以让有些人迷惑的一大原因就在于其隐藏的this指针。一旦把隐藏的this指针显露出来,大家看到的就是一个面向过程编程。而Java和C#其实都是遵循着C++语言的惯例而设计的,它们的成员方法中都带有一个隐藏的this指针。如果读者了解Python语法,就会知道Python的成员方法中会有一个self 参数,它和this指针的作用是完全一样的。 我们对于一些事物的不理解或者畏惧,原因都在于这些事情所有意无意带有的绚丽外衣和神秘面纱。只要揭开这一层直达本质,就会发现一切其实都很简单。 “在Go语言中没有隐藏的this指针”这句话的含义是: 方法施加的目标(也就是“对象”)显式传递,没有被隐藏起来; 方法施加的目标(也就是“对象”)不需要非得是指针,也不用非得叫this。 我们对比Java语言的代码: class Integer {     private int val;     public boolean Less(Integer b) {         return this.val< b.val;     } } 对于这段Java代码,初学者可能会比较难以理解其背后的机制,以及this到底从何而来。这主要是因为Integer类的Less()方法隐藏了第一个参数Integer* this。如果将其翻译成C代码,会更清晰: struct Integer {     int val; };  bool Integer_Less(Integer* this, Integer* b) {     return this->val < b->val; } 2 Go语言中的面向对象    为直观,也无需支付额外的成本。如果要求对象必须以指针传递,这有时会是个额外成本,因为对象有时很小(比如4字节),用指针传递并不划算。只有在你需要修改对象的时候,才必须用指针。它不是Go语言的约束,而是一种自然约束。举个例子: func (a *Integer) Add(b Integer) {     *a += b } 这里为Integer类型增加了Add()方法。由于Add()方法需要修改对象的值,所以需要用指4 针引用。调用如下: func main() {     var a Integer = 1     a.Add(2)         fmt.Println("a =", a) }    5 运行该程序,得到的结果是:a=3。如果你实现成员方法时传入的不是指针而是值(即传入Integer,而非*Integer),如下所示: func (a Integer) Add(b Integer) { 6     a += b } 那么运行程序得到的结果是a=1,也就是维持原来的值。读者可以亲自动手尝试一下。 究其原因,是因为Go语言和C语言一样,类型都是基于值传递的。要想修改变量的值,只能传递指针。                                                                                                                                                     7 Go 语言包经常使用此功能,比如http包中关于HTTP头部信息的Header类型(参见$GOROOT/src/pkg/http/header.go)就是通过Go内置的map类型赋予新的语义来实现的。下面是 Header类型实现的部分代码: 8 // Header类型用于表达HTTP头部的键值对信息 type Header map[string][]string // Add()方法用于添加一个键值对到HTTP头部 // 如果该键已存在,则会将值添加到已存在的值后面 func (h Header) Add(key, value string) {                   textproto.MIMEHeader(h).Add(key, value)                                    8 //(} ) Set()方法用于设置某个键对应的值,如果该键已存在,则替换已存在的值func (h Header) Set(key, value string) {     textproto.MIMEHeader(h).Set(key, value)  }  // 还有更多其他方法 Header类型其实就是一个map,但通过为map起一个Header别名并增加了一系列方法,它就变成了一个全新的类型,但这个新类型又完全拥有map的功能。是不是很酷? Go 语言毕竟还是一门比较新的语言,学习资源相比 C++/Java/C#自然会略显缺乏。
  • [技术干货] 什么是GO类型系统
    类型系统是一门编程语言的地基,它的地位至关重要。顾名思义,类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容: 基础类型,如byte、int、bool、float等; 复合类型,如数组、结构体、指针等; 可以指向任意对象的类型(Any类型); 值语义和引用语义; 面向对象,即所有具备面向对象特征(比如成员方法)的类型; 接口。 类型系统描述的是这些内容在一个语言中如何被关联。因为Java语言自诞生以来被称为 纯正的面向对象语言,所以我们就先以Java语言为例讲一讲类型系统。在Java语言中,存在两套完全独立的类型系统:一套是值类型系统,主要是基本类型,如byte、 int、boolean、char、double等,这些类型基于值语义;一套是以Object类型为根的对象类型系统,这些类型可以定义成员变量和成员方法,可以有虚函数,基于引用语义,只允许在堆上创建(通过使用关键字new)。Java语言中的Any类型就是整个对象类型系统的根——java.lang.Object类型,只有对象类型系统中的实例才可以被Any类型引用。值类型想要被Any类型引用,需要装箱                                                                                                                                  2 (boxing)过程,比如int类型需要装箱成为Integer类型。另外,只有对象类型系统中的类型才可以实现接口,具体方法是让该类型从要实现的接口继承。 相比之下,Go语言中的大多数类型都是值语义,并且都可以包含对应的操作方法。在需要的时候,你可以给任何类型(包括内置类型)“增加”新方法。而在实现某个接口时,无需从该接口继承(事实上,Go语言根本就不支持面向对象思想中的继承语法),只需要实现该接口要求的所有方法即可。任何类型都可以被Any类型引用。Any类型就是空接口,即interface{}。
  • [技术干货] go语言monkey 组件迁移
    基于鲲鹏的Monkey补丁迁移 关于Monkey是基于Go 语言编写的一种补丁,它能在程序运行时插入汇编指令,使得调用目标函数时可以重定向到替换函数执行,同时能够取消插入的汇编指令,恢复对目标函数的正常调用。例如:有两函数 a, b,它们的实现分别如下:由图可知,a函数实现了一个 从 0到10的累加,b函数则直接返回一个 221在经过 monkey函数处理之后 monkey.patch(a, b)最后执行a函数,发现并没有执行从 0 到 10的累加,而是直接返回了221,也即最后执行的是b函数,起到了一个偷梁换柱的效果。Monkey设计当初是为x86架构设计的,未考虑Arm架构,且在 2015年停止了后续更新,即后续不会再推出适配Arm的版本。 项目背景某客户自研软件用到猴子补丁库 – Monkey, 软件在编译过程中报错,堆栈信息显示,编译过程中出现了不合法的地址:问题定位用devle工具进行单步调试追踪发现,在Monkey补丁的根目录下的monkey_386 .go 和monkey_amd64.go 文件中,均出现了x86的机器码:完整的机器码:问题根因:Monkey强行将x86的机器码硬编码进了程序中,从而导致在Arm架构的机器编译时无法正确识别,引发报错。 迁移思路迁移难点:从根因可知,Monkey的迁移,大概分为五个步骤:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制);翻译机器码,找到其对应的x86汇编代码,例如 0xFF,0x22 对应 jmp QWORD PTR [rdx] 将x86的汇编代码,翻译成Arm架构下所对应的汇编代码将Arm架构下的汇编代码,翻译成对应的Arm的机器码将Arm的机器码,改写成go对应的语句,并重新编译验证迁移思路:   迁移思路:Devkit工具扫描,看迁移建议。Exager: 将客户软件直接运行在Exager上直面问题,按五步法走 Devkit扫描:工具未给出任何迁移建议Exager 与 宿主机共用同一内核,pagesize为64K, 而原生 x86的go,是运行在4k上的,故go无法在 Exager上运行,除非修改pagesize为4k,但pagesize的修改有可能导致软件性能下降,客户不接受此方案。只能采用五步法为了更好地实施五步法,我们还需要了解Monkey的整体设计思路,做好全局的把控, Monkey的设计现有如下程序写法:执行变量f,发现执行的是a函数,是因为,a函数把其地址,赋值给了变量f对上面代码进行反汇编,可以发现:main.a.f加载到寄存器rdx里,然后把rdx寄存器指向的地址存入rbx里,最后调用。函数的地址值总是会加载到rdx寄存器里面,当代码调用的时候可以用来加载一些可能会用到的额外信息。回到开头,两函数,a, b 如何实现,调用a,实际上调用b?我们需要修改函数a,让它跳转到b的代码,跳过执行它自己的代码。实际上,我们需要通过这种方法来实现替换,加载函数b到寄存器rdx,然后执行时跳转到rdx上面。对应的x86汇编代码与机器码如下:其中:mov rdx是固定的,对应 48 C7 C2main.b.f 为函数地址,作为立即数,是动态变化的,用??取代,当立即数为0时,即为00,最大有8组,即64位立即数jmp [rdx] 也是固定的,对应的机器码为 FF 22因此我们可以得到如下类似函数:由此可知,动态变化的机器码,即为b函数地址,我们需要做的,就是将地址作为立即数,赋值给 rdx. X86与Arm的汇编差异了解了Monkey的设计思路后,我们再回看根目录下的 monkey_amd.64很明显,问题的关键,在于如何把to,进行拆分,并赋值给寄存器。X86的汇编很直接,寄存器能一口气存下 最大64位的立即数:即 movabs rdx, XX XX XX XX..  xx 每两个一组,每组8位,一共8组若立即数不够 64位,后面补0即可。即一个16位立即数,最后赋值会是 movabs rdx, xx xx 00 00 00 00 00 00 Arm则不通,最多一次只能存下 16位立即数,若立即数大于16位,比如64位,则必须拆分为4段。例如,现有立即数:0x8877665544332211对应机器码为 :1000100001110111011001100101010101000100001100110010001000010001从高位进行拆分,每16位拆分一段:第一段:1000100001110111011001100101010101000100001100110010001000010001即为:0x2211第二段:1000100001110111011001100101010101000100001100110010001000010001即为:0x4433第三段:1000100001110111011001100101010101000100001100110010001000010001即为:0x6655第四段:1000100001110111011001100101010101000100001100110010001000010001即为:0x8877综上,把一个0x8877665544332211 的立即数,赋值给一个寄存器(例如x10)最后的写法如下: 迁移实施综上。我们根据五步法,进行迁移尝试。第一步:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制)to是b函数地址,这里我们假设,其数值为0x8877665544332211 第二步:翻译机器码,找到其对应的x86汇编代码0x48, 0xBA, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,0x77,0x88对应汇编:movabs rdx, 0x88776655443322110xFF, 0x22对应汇编:jmp QWORD PTR [rdx] 第三步:将x86的汇编代码,翻译成Arm架构下所对应的汇编代码movabs 我们用 mov movk 对应jmp 我们用 ldr 和 br进行对应故翻译过来就是:第四步:将Arm架构下的汇编代码,翻译成对应的Arm的机器码第五步:将Arm的机器码,改写成go对应的语句,并重新编译验证这两句是固定不变的0x4b, 0x01, 0x40, 0xf90x60, 0x01, 0x1f, 0xd6问题在于,如何将变化的部分,转换成go的代码呢?从机器码可以看出,变化的部分,仅仅只是红框框起来的部分,黑色部分是不变的。我们拿第一行进行举例:mov x10, 0x2211                 11010010100001000100001000101010我们将红色部分归零             11010010100000000000000000001010对应的16进制是:0xd280000a我们要做的,把0想象成一个个坑,我们要做的就是填坑,如何把0x2211 填到坑里面去从图中可知,头尾两部分,11010010100 和01010是固定不变的,需要保留,我们可以将0x2211 左移五位,把空位都留出来。即:00000000000001000100001000100000最后与 0xd280000a 相或即可,0与0相或为0, 0与1相或为1, 1与1相或也为1,这样,就做到了一个填坑的效果写成代码,即:  Arm64的cache一致性要让变更的机器码发挥作用,需要对内存进行修改,我们可以看到在后续的操作中。monkey通过copyToLocation 进行了内存修改操作X86架构从硬件方面保证了cache一致性,而Arm架构则需要通过软件进行保证,对内存修改操作后,需要对cache进行刷新,保证cache一致性。因此,我们需要在 copyToLocation中,进行cache刷新。修改copyToLocation函数如下:func copyToLocation(location uintptr, data []byte) {        f := rawMemoryAccess(location, len(data))        mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)        paddr := (*C.char)(&f[0])         copy(f, data[:])        C.dcache_flush(paddr, 64)         mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_EXEC)} 函数汇编指令过短问题从前面的论述我们得知,由于两种架构汇编指令得差异,同样的修改效果,x86架构所需汇编指令2条,Arm需要6条测试得知,当被替换函数过于简单,本身汇编指令条数小于6条时,且替换函数与被替换函数相邻时,会发生指令覆盖踩踏,导致错误。例如,有如下两个函数:no 是被替换函数,yes是替换函数它们的汇编指令如下:可以看到,no函数的指令条数是4, 小于6条当用monkey进行修改时,会发生如下现象no的汇编指令全被6条指令覆盖了,no本身只有4条,多出来的2条,把yes本身的前两条汇编指令也覆盖了,也即,yes函数的完整性遭到了破坏,因此br跳转时,会找不到yes函数的首地址,引发报错。规避措施:对于不在场景限制里的情况,无须考虑任何措施;对于处在场景限制里的情况,考虑对被替换函数增加空函数调用,如图1-11所示。
  • [技术干货] go语言monkey 组件迁移
    基于鲲鹏的Monkey补丁迁移 关于Monkey是基于Go 语言编写的一种补丁,它能在程序运行时插入汇编指令,使得调用目标函数时可以重定向到替换函数执行,同时能够取消插入的汇编指令,恢复对目标函数的正常调用。例如:有两函数 a, b,它们的实现分别如下:由图可知,a函数实现了一个 从 0到10的累加,b函数则直接返回一个 221在经过 monkey函数处理之后 monkey.patch(a, b)最后执行a函数,发现并没有执行从 0 到 10的累加,而是直接返回了221,也即最后执行的是b函数,起到了一个偷梁换柱的效果。Monkey设计当初是为x86架构设计的,未考虑Arm架构,且在 2015年停止了后续更新,即后续不会再推出适配Arm的版本。 项目背景某客户自研软件用到猴子补丁库 – Monkey, 软件在编译过程中报错,堆栈信息显示,编译过程中出现了不合法的地址:问题定位用devle工具进行单步调试追踪发现,在Monkey补丁的根目录下的monkey_386 .go 和monkey_amd64.go 文件中,均出现了x86的机器码:完整的机器码:问题根因:Monkey强行将x86的机器码硬编码进了程序中,从而导致在Arm架构的机器编译时无法正确识别,引发报错。 迁移思路迁移难点:从根因可知,Monkey的迁移,大概分为五个步骤:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制);翻译机器码,找到其对应的x86汇编代码,例如 0xFF,0x22 对应 jmp QWORD PTR [rdx] 将x86的汇编代码,翻译成Arm架构下所对应的汇编代码将Arm架构下的汇编代码,翻译成对应的Arm的机器码将Arm的机器码,改写成go对应的语句,并重新编译验证迁移思路:   迁移思路:Devkit工具扫描,看迁移建议。Exager: 将客户软件直接运行在Exager上直面问题,按五步法走 Devkit扫描:工具未给出任何迁移建议Exager 与 宿主机共用同一内核,pagesize为64K, 而原生 x86的go,是运行在4k上的,故go无法在 Exager上运行,除非修改pagesize为4k,但pagesize的修改有可能导致软件性能下降,客户不接受此方案。只能采用五步法为了更好地实施五步法,我们还需要了解Monkey的整体设计思路,做好全局的把控, Monkey的设计现有如下程序写法:执行变量f,发现执行的是a函数,是因为,a函数把其地址,赋值给了变量f对上面代码进行反汇编,可以发现:main.a.f加载到寄存器rdx里,然后把rdx寄存器指向的地址存入rbx里,最后调用。函数的地址值总是会加载到rdx寄存器里面,当代码调用的时候可以用来加载一些可能会用到的额外信息。回到开头,两函数,a, b 如何实现,调用a,实际上调用b?我们需要修改函数a,让它跳转到b的代码,跳过执行它自己的代码。实际上,我们需要通过这种方法来实现替换,加载函数b到寄存器rdx,然后执行时跳转到rdx上面。对应的x86汇编代码与机器码如下:其中:mov rdx是固定的,对应 48 C7 C2main.b.f 为函数地址,作为立即数,是动态变化的,用??取代,当立即数为0时,即为00,最大有8组,即64位立即数jmp [rdx] 也是固定的,对应的机器码为 FF 22因此我们可以得到如下类似函数:由此可知,动态变化的机器码,即为b函数地址,我们需要做的,就是将地址作为立即数,赋值给 rdx. X86与Arm的汇编差异了解了Monkey的设计思路后,我们再回看根目录下的 monkey_amd.64很明显,问题的关键,在于如何把to,进行拆分,并赋值给寄存器。X86的汇编很直接,寄存器能一口气存下 最大64位的立即数:即 movabs rdx, XX XX XX XX..  xx 每两个一组,每组8位,一共8组若立即数不够 64位,后面补0即可。即一个16位立即数,最后赋值会是 movabs rdx, xx xx 00 00 00 00 00 00 Arm则不通,最多一次只能存下 16位立即数,若立即数大于16位,比如64位,则必须拆分为4段。例如,现有立即数:0x8877665544332211对应机器码为 :1000100001110111011001100101010101000100001100110010001000010001从高位进行拆分,每16位拆分一段:第一段:1000100001110111011001100101010101000100001100110010001000010001即为:0x2211第二段:1000100001110111011001100101010101000100001100110010001000010001即为:0x4433第三段:1000100001110111011001100101010101000100001100110010001000010001即为:0x6655第四段:1000100001110111011001100101010101000100001100110010001000010001即为:0x8877综上,把一个0x8877665544332211 的立即数,赋值给一个寄存器(例如x10)最后的写法如下: 迁移实施综上。我们根据五步法,进行迁移尝试。第一步:解构 go代码,将移位(byte(to >> XXXXXX))翻译成对应的机器码(16进制)to是b函数地址,这里我们假设,其数值为0x8877665544332211 第二步:翻译机器码,找到其对应的x86汇编代码0x48, 0xBA, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,0x77,0x88对应汇编:movabs rdx, 0x88776655443322110xFF, 0x22对应汇编:jmp QWORD PTR [rdx] 第三步:将x86的汇编代码,翻译成Arm架构下所对应的汇编代码movabs 我们用 mov movk 对应jmp 我们用 ldr 和 br进行对应故翻译过来就是:第四步:将Arm架构下的汇编代码,翻译成对应的Arm的机器码第五步:将Arm的机器码,改写成go对应的语句,并重新编译验证这两句是固定不变的0x4b, 0x01, 0x40, 0xf90x60, 0x01, 0x1f, 0xd6问题在于,如何将变化的部分,转换成go的代码呢?从机器码可以看出,变化的部分,仅仅只是红框框起来的部分,黑色部分是不变的。我们拿第一行进行举例:mov x10, 0x2211                 11010010100001000100001000101010我们将红色部分归零             11010010100000000000000000001010对应的16进制是:0xd280000a我们要做的,把0想象成一个个坑,我们要做的就是填坑,如何把0x2211 填到坑里面去从图中可知,头尾两部分,11010010100 和01010是固定不变的,需要保留,我们可以将0x2211 左移五位,把空位都留出来。即:00000000000001000100001000100000最后与 0xd280000a 相或即可,0与0相或为0, 0与1相或为1, 1与1相或也为1,这样,就做到了一个填坑的效果写成代码,即:  Arm64的cache一致性要让变更的机器码发挥作用,需要对内存进行修改,我们可以看到在后续的操作中。monkey通过copyToLocation 进行了内存修改操作X86架构从硬件方面保证了cache一致性,而Arm架构则需要通过软件进行保证,对内存修改操作后,需要对cache进行刷新,保证cache一致性。因此,我们需要在 copyToLocation中,进行cache刷新。修改copyToLocation函数如下:func copyToLocation(location uintptr, data []byte) {        f := rawMemoryAccess(location, len(data))        mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)        paddr := (*C.char)(&f[0])         copy(f, data[:])        C.dcache_flush(paddr, 64)         mprotectCrossPage(location, len(data), syscall.PROT_READ|syscall.PROT_EXEC)} 函数汇编指令过短问题从前面的论述我们得知,由于两种架构汇编指令得差异,同样的修改效果,x86架构所需汇编指令2条,Arm需要6条测试得知,当被替换函数过于简单,本身汇编指令条数小于6条时,且替换函数与被替换函数相邻时,会发生指令覆盖踩踏,导致错误。例如,有如下两个函数:no 是被替换函数,yes是替换函数它们的汇编指令如下:可以看到,no函数的指令条数是4, 小于6条当用monkey进行修改时,会发生如下现象no的汇编指令全被6条指令覆盖了,no本身只有4条,多出来的2条,把yes本身的前两条汇编指令也覆盖了,也即,yes函数的完整性遭到了破坏,因此br跳转时,会找不到yes函数的首地址,引发报错。规避措施:对于不在场景限制里的情况,无须考虑任何措施;对于处在场景限制里的情况,考虑对被替换函数增加空函数调用,如图1-11所示。
  • [技术干货] 为什么我们从 Python 切换到 Go[转载]
    为什么我们从 Python 切换到 Go原文链接:https://softwareengineeringdaily.com/2021/03/03/why-we-switched-from-python-to-go/转换到一种新的语言总是一个很大的步骤,尤其是当你的团队成员中只有一个人有这种语言的经验时。今年早些时候,我们将 Stream 的主要编程语言从 Python 转换到了 Go。这篇文章将解释为什么我们决定放弃 Python 转而使用 Go 的一些原因。原因 1 – 性能Go 非常快。性能类似于 Java 或 C++。对于我们的用例,Go 通常比 Python 快 40 倍。这是一个比较Go 和 Python的小型基准测试游戏。原因 2 – 语言表现很重要对于许多应用程序来说,编程语言只是应用程序和数据库之间的粘合剂。语言本身的表现通常并不重要。然而,Stream 是一个为700家公司和超过5亿终端用户提供 feed 和聊天平台的 API 提供商。多年来,我们一直在优化 Cassandra、 PostgreSQL、 Redis 等等,但最终,我们达到了所使用语言的极限。Python 是一种很棒的语言,但是对于序列化/反序列化、排序和聚合等用例来说,它的性能相当缓慢。我们经常遇到性能问题,Cassandra 需要1ms 来检索数据,而 Python 需要10ms 来将数据转换为对象。原因 3 – 开发人员的生产力和没有太有创意看看 我如何开始 Go 教程中的一小段 Go 代码。(这是一个很棒的教程,也是学习 Go 的一个很好的起点。)如果您是 Go 新手,那么在阅读那个小代码片段时不会有太多让您感到惊讶的事情。它展示了多个赋值、数据结构、指针、格式和一个内置的 HTTP 库。当我第一次开始编程时,我一直喜欢使用 Python 更高级的功能。Python 允许您在编写代码时获得相当的创意。例如,您可以:Use MetaClasses to self-register classes upon code initialization 在代码初始化时使用元类自寄存器类Swap out True and False 替换掉 True 和 FalseAdd functions to the list of built-in functions 将函数添加到内置函数列表中Overload operators via magic methods 通过魔术方法重载操作符Use functions as properties via the @property decorator 通过@property decorator 将函数用作属性这些功能玩起来很有趣,但是,正如大多数程序员会同意的那样,在阅读别人的作品时,它们通常会使代码更难理解。Go 迫使你坚持基础。这使得阅读任何人的代码并立即了解发生了什么变得非常容易。 注意:当然,它实际上有多“容易”取决于您的用例。如果你想创建一个基本的 CRUD API,我仍然推荐 Django + DRF或 Rails。这些功能玩起来很有趣,但是,正如大多数程序员会同意的那样,在阅读别人的作品时,它们通常会使代码更难理解。Go 迫使你坚持基础。这使得阅读任何人的代码并立即了解发生了什么变得非常容易。 注意:当然,它实际上有多“容易”取决于您的场景。如果你想创建一个基本的 CRUD API,我仍然推荐 Django + DRF或 Rails。原因 4 – 并发和通道作为一门语言,Go 试图让事情变得简单。它没有引入许多新概念。重点是创建一种非常快速且易于使用的简单语言。它唯一具有创新性的领域是 goroutine 和通道。(100% 正确 CSP的概念始于 1977 年,所以这项创新更多是对旧思想的一种新方法。)Goroutines 是 Go 的轻量级线程方法,通道是 goroutines 之间通信的首选方式。Goroutines 的创建非常便宜,并且只需要几 KB 的额外内存。因为 Goroutine 非常轻量,所以有可能同时运行数百甚至数千个。您可以使用通道在 goroutine 之间进行通信。Go 运行时处理所有复杂性。goroutines 和基于通道的并发方法使得使用所有可用的 CPU 内核和处理并发 IO 变得非常容易——所有这些都不会使开发复杂化。与 Python/Java 相比,在 goroutine 上运行函数需要最少的样例代码。您只需在函数调用前加上关键字“go”:https://tour.golang.org/concurrency/1 Go 的并发方法很容易使用。与 Node 相比,这是一种有趣的方法,开发人员必须密切关注异步代码的处理方式。Go 中并发的另一个重要方面是 竞争检测器。这样可以很容易地确定异步代码中是否存在任何竞争条件。以下是开始使用 Go 和频道的一些很好的资源:https://gobyexample.com/channelshttps://tour.golang.org/concurrency/2http://guzalexander.com/2013/12/06/golang-channels-tutorial.htmlhttps://www.golang-book.com/books/intro/10https://www.goinggo.net/2014/02/the-nature-of-channels-in-go.htmlGoroutines vs Green threads原因 5 – 快速编译时间我们目前用 Go 编写的最大的微服务编译需要 4 秒。与以编译速度慢而闻名的 Java 和 C++ 等语言相比,Go 的快速编译时间是一项重大的生产力胜利。我喜欢剑术,但在我还记得代码应该做什么的同时完成事情会更好:理由 6 - 团队建设的能力首先,让我们从显而易见的开始:与 C++ 和 Java 等旧语言相比,Go 开发人员的数量并不多。根据 StackOverflow的数据, 38% 的开发人员知道 Java, 19.3%的 人知道 C++,只有 4.6%的 人知道 Go。 GitHub 数据 显示了 类似的趋势:Go 比 Erlang、Scala 和 Elixir 等语言使用更广泛,但不如 Java 和 C++ 流行。幸运的是,Go 是一种非常简单易学的语言。它提供了您需要的基本功能,仅此而已。它引入的新概念是“延迟”声明和内置的并发管理与“goroutines”和通道。(对于纯粹主义者来说:Go 并不是第一种实现这些概念的语言,只是第一种使它们流行起来的语言。)任何加入团队的 Python、Elixir、C++、Scala 或 Java 开发人员都可以在一个月内在 Go 上上手,因为它的简单性。与许多其他语言相比,我们发现组建 Go 开发人员团队更容易。如果您在生态系统中招聘人员, 这是一项重要的优势。理由 7 – 强大的生态系统对于我们这样规模的团队(大约20人)来说,生态系统很重要。如果你不得不彻底改造每一个小功能,你就不能为你的客户创造价值。Go 对我们使用的工具有很好的支持。实体库已经可用于 Redis、RabbitMQ、PostgreSQL、模板解析、任务调度、表达式解析和 RocksDB。与 Rust 或 Elixir 等其他较新的语言相比,Go 的生态系统是一个重大胜利。它当然不如 Java、Python 或 Node 之类的语言好,但它很可靠,而且对于许多基本需求,你会发现已经有高质量的包可用。原因 8 – Gofmt,强制代码格式化让我们从什么是 Gofmt 开始?不,这不是一个发誓的话。Gofmt 是一个很棒的命令行实用程序,内置在 Go 编译器中,用于格式化代码。就功能而言,它与 Python 的 autopep8 非常相似。尽管《硅谷》的节目以其他方式描绘,但我们大多数人并不真正喜欢争论制表符与空格。格式的一致性很重要,但实际的格式标准并不那么重要。Gofmt 通过使用一种正式的方式来格式化您的代码来避免所有这些讨论。原因 9 – gRPC 和协议缓冲区Go 对 Protocol Buffers 和 gRPC 有一流的支持。这两个工具非常适合构建需要通过 RPC 进行通信的微服务。您需要做的就是编写一个清单,定义可以进行的 RPC 调用以及它们采用的参数。然后从这个清单中自动生成服务器和客户端代码。生成的代码速度快,网络占用空间小,易于使用。从同一个清单中,您甚至可以为许多不同的语言生成客户端代码,例如 C++、Java、Python 和 Ruby。因此,内部流量不再有模棱两可的 REST 端点,而且您每次都必须编写几乎相同的客户端和服务器代码。缺点 1 – 缺乏框架Go 没有像 Rails 用于 Ruby、Django 用于 Python 或 Laravel 用于 PHP 那样的单一主导框架。这是 Go 社区内激烈争论的话题,因为许多人主张你不应该一开始就使用框架。我完全同意这对于某些用例是正确的。但是,如果有人想构建一个简单的 CRUD API,他们将更容易使用 Django/DJRF、Rails Laravel 或 Phoenix。 更新: 正如评论所指出的,有几个项目为 Go 提供了框架。 Revel , Iris , Echo , Macaron 和 Buffalo 似乎是主要的竞争者。对于 Stream 的用例,我们更喜欢不使用框架。然而,对于许多希望提供简单 CRUD API 的新项目来说,缺乏主导框架将是一个严重的劣势。缺点 2 – 错误处理Go 通过简单地从函数返回错误并期望调用代码来处理错误(或将其返回到调用堆栈)来处理错误。虽然这种方法有效,但很容易失去问题的范围,以确保您可以向用户提供有意义的错误。错误包 通过允许您向错误添加上下文和堆栈跟踪来解决此问题。 另一个问题是很容易忘记处理错误。像 errcheck 和 megacheck 这样的静态分析工具可以方便地避免犯这些错误。虽然这些变通办法效果很好,但感觉不太对劲。您希望该语言支持正确的错误处理。劣势3——包管理更新:自写这篇文章以来,Go 的包管理已经取得了长足的进步。 Go 模块 是一个有效的解决方案,我看到的唯一问题是它们破坏了一些静态分析工具,如 errcheck。这是一个使用 Go modules学习使用 Go 的教程 。 Go 的包管理绝不是完美的。默认情况下,它无法指定特定版本的依赖项,也无法创建可重现的构建。Python、Node 和 Ruby 都有更好的包管理系统。但是,使用正确的工具,Go 的包管理工作得很好。您可以使用 Dep 来管理您的依赖项,以允许指定和固定版本。除此之外,我们还贡献了一个名为的开源工具 VirtualGo ,它可以更轻松地处理用 Go 编写的多个项目。结论Go 是一种非常高性能的语言,对并发有很好的支持。它几乎与 C++ 和 Java 等语言一样快。虽然与 Python 或 Ruby 相比,使用 Go 构建东西确实需要更多时间,但您将节省大量用于优化代码的时间。我们在 Stream有一个小型开发团队, 为超过 5 亿最终用户 提供动力和 聊天。Go 结合了 强大的生态系统、 新开发人员的 轻松入门、快速的性能、 对并发的 可靠支持和高效的编程环境 ,使其成为一个不错的选择。 **Stream 仍然在我们的仪表板、站点和机器学习中利用 Python 来提供 个性化的订阅源. 我们不会很快与 Python 说再见,但今后所有性能密集型代码都将使用 Go 编写。我们新的 聊天 API 也完全用 Go 编写。**如果您想了解有关 Go 的更多信息,请查看下面列出的博客文章。原文链接:https://blog.csdn.net/inthat/article/details/124994033
  • [技术干货] GO语言问题追踪和调试笔记分享
    Go语言所提供的是尽量简单的语法和尽量完善的库,以尽可能降低问题的发生概率。当然,问题还是会发生的,这时需要用到问题追踪和调试技能。这里我们简单介绍下两个      常规的问题跟踪方法:打印日志和使用GDB进行逐步调试。 打印日志 Go语言包中包含一个fmt包,其中提供了大量易用的打印函数,我们会接触到的主要是 Printf()和Println()。这两个函数可以满足我们的基本调试需求,比如临时打印某个变量。这两个函数的参数非常类似于C语言运行库中的Printf(),有C语言开发经验的同学会很容易上手。下面是几个使用Printf()和Println()的例子: fval := 110.48 ival := 200 sval := "This is a string. " fmt.Println("The value of fval is", fval) fmt.Printf("fval=%f, ival=%d, sval=%s\n", fval, ival, sval) fmt.Printf("fval=%v, ival=%v, sval=%v\n", fval, ival, sval) 输出结果为: The value of fval is 100.48 fval=100.48, ival=200, sval=This is a string. fval=100.48, ival=200, sval=This is a string. fmt包的这一系列格式化打印函数使用起来非常方便,但在正式开始用Go开发服务器系统时,我们就不能只依赖fmt包了,而是需要设计严格的日志规范。Go语言的log包提供了基础的日志功能。如果有需要,你也可以引入自己的log模块。 GDB调试 不用设置什么编译选项,Go语言编译的二进制程序直接支持GDB调试,比如之前用go build calc编译出来的可执行文件calc,就可以直接用以下命令以调试模式运行: $ gdb calc 因为GDB的标准用法与Go没有特别关联,这里就不详细展开了,有兴趣的读者可以自行查看对应的文档。需要注意的是,Go编译器生成的调试信息格式为DWARFv3,只要版本高于7.1的GDB应该都支持它。
  • [技术干货] GO语言错误处理之panic()和recover()函数笔记分享
    Go语言引入了两个内置函数panic()和recover()以报告和处理运行时错误和程序中的错误场景: func panic(interface{})  func recover() interface{}  当在一个函数执行过程中调用panic()函数时,正常的函数执行流程将立即终止,但函数中之前使用defer关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行panic流程,直至所属的goroutine中所有正在执行的函数被终止。错误信息将被报告,包括在调用panic()函数时传入的参数,这个过程称为错误处理流程。 从panic()的参数类型interface{}我们可以得知,该函数接收任意类型的数据,比如整型、字符串、对象等。调用方法很简单,下面为几个例子: panic(404)  panic("network broken")  panic(Error("file not exists")) recover()函数用于终止错误处理流程。一般情况下,recover()应该在一个使用defer 关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复过程(使用recover关键字),会导致该goroutine所属的进程打印异常信息后直接退出。 以下为一个常见的场景。 我们对于foo()函数的执行要么心里没底感觉可能会触发错误处理,或者自己在其中明确加入了按特定条件触发错误处理的语句,那么可以用如下方式在调用代码中截取recover(): defer func() {     if r := recover(); r != nil {         log.Printf("Runtime error caught: %v", r)     }  }()  foo() 无论foo()中是否触发了错误处理流程,该匿名defer函数都将在函数退出时得到执行。假如foo()中触发了错误处理流程,recover()函数执行将使得该错误处理过程终止。如果错误处理流程被触发时,程序传给panic函数的参数不为nil,则该函数还会打印详细的错误信息。
  • [技术干货] GO语言错误处理之defer接口笔记分享
    关键字defer是Go语言引入的一个非常有意思的特性,相信很多C++程序员都写过类似下面这样的代码: class file_closer {     FILE _f;  public:     file_closer(FILE f) : _f(f) {}     ~file_closer() { if (f) fclose(f); }  };  然后在需要使用的地方这么写: void f() {     FILE f = open_file("file.txt"); // 打开一个文件句柄     file_closer _closer(f);      // 对f句柄进行操作 }  为什么需要file_closer这么个包装类呢?因为如果没有这个类,代码中所有退出函数的环节,比如每一个可能抛出异常的地方,每一个return的位置,都需要关掉之前打开的文件句柄。即使你头脑清晰,想明白了每一个分支和可能出错的条件,在该关闭的地方都关闭了,怎么保证你的后继者也能做到同样水平?大量莫名其妙的问题就出现了。 在C/C++中还有另一种解决方案。开发者可以将需要释放的资源变量都声明在函数的开头部分,并在函数的末尾部分统一释放资源。函数需要退出时,就必须使用goto语句跳转到指定位置先完成资源清理工作,而不能调用return语句直接返回。 这种方案是可行的,也仍然在被使用着,但存在非常大的维护性问题。而Go语言使用defer 关键字简简单单地解决了这个问题,比如以下的例子: func CopyFile(dst, src string) (w int64, err error) {     srcFile, err := os.Open(src)     if err != nil {         return     }      defer srcFile.Close()      dstFile, err := os.Create(dstName)     if err != nil {         return     }      defer dstFile.Close()      return io.Copy(dstFile, srcFile)  }  即使其中的Copy()函数抛出异常,Go仍然会保证dstFile和srcFile会被正常关闭。 如果觉得一句话干不完清理的工作,也可以使用在defer后加一个匿名函数的做法: defer func() {     // 做你复杂的清理工作 } ()  另外,一个函数中可以存在多个defer语句,因此需要注意的是,defer语句的调用是遵照先进后出的原则,即 后一个defer语句将   先被执行。只不过,当你需要为defer语句到底哪个先执行这种细节而烦恼的时候,说明你的代码架构可能需要调整一下了。
  • [技术干货] GO语言错误处理之error接口笔记分享
    误处理是学习任何编程语言都需要考虑的一个重要话题。在早期的语言中,错误处理不是语言规范的一部分,通常只作为一种编程范式存在,比如C语言中的errno。但自C++语言以来,语言层面上会增加错误处理的支持,比如异常(exception)的概念和try-catch关键字的引入。Go语言在此功能上考虑得更为深远。漂亮的错误处理规范是Go语言大的亮点之一。 error接口 Go语言引入了一个关于错误处理的标准模式,即error接口,该接口的定义如下: type error interface {                   Error() string                                                             5 }  对于大多数函数,如果要返回错误,大致上都可以定义为如下模式,将error作为多种返回值中的 后一个,但这并非是强制要求:               func Foo(param int)(n int, err error) {                                        6     // ...  } 调用时的代码建议按如下方式处理错误情况: n, err := Foo(0)                                                                                               7 if err != nil {     // 错误处理 } else {     // 使用返回值n  } 下面我用Go库中的实际代码来示范如何使用自定义的error类型。      8 首先,定义一个用于承载错误信息的类型。因为Go语言中接口的灵活性,你根本不需要从 error接口继承或者像Java一样需要使用implements来明确指定类型和接口之间的关系,具体代码如下: type PathError struct {     Op   string     Path string     Err  error  }  如果这样的话,编译器又怎能知道PathError可以当一个error来传递呢?关键在于下面的代码实现了Error()方法: func (e *PathError) Error() string {     return e.Op + " " + e.Path + ": " + e.Err.Error()   }  关于接口的更多细节,可以参见3.5节。之后就可以直接返回PathError变量了,比如在下面的代码中,当syscall.Stat()失败返回err时,将该err包装到一个PathError对象中返回: func Stat(name string) (fi FileInfo, err error) {     var stat syscall.Stat_t      err = syscall.Stat(name, &stat)      if err != nil {         return nil, &PathError{"stat", name, err}     }      return fileInfoFromStat(&stat, name), nil }  如果在处理错误时获取详细信息,而不仅仅满足于打印一句错误信息,那就需要用到类型转换知识了: fi, err := os.Stat("a.txt")   if err != nil {     if e, ok := err.(*os.PathError); ok && e.Err != nil {         // 获取PathError类型变量e中的其他信息并处理     }  }  这就是Go中error类型的使用方法。与其他语言中的异常相比,Go的处理相对比较直观、简单。
  • [干货汇总] 实践GoF的设计模式:工厂方法模式
    >摘要:工厂方法模式(Factory Method Pattern)将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口,常用于不指定对象具体类型的情况下创建对象的场景。 本文分享自华为云社区《[【Go实现】实践GoF的23种设计模式:工厂方法模式](https://bbs.huaweicloud.com/blogs/353969?utm_source=csdn&utm_medium=bbs-ex&utm_campaign=other&utm_content=content)》,作者: 元闰子。 # 简述 工厂方法模式(Factory Method Pattern)跟上一篇讨论的建造者模式类似,**都是将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口**。两者在应用场景上稍有区别,建造者模式常用于需要传递多个参数来进行实例化的场景;工厂方法模式常用于**不指定对象具体类型的情况下创建对象**的场景。 # UML 结构 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20225/27/1653614038725695458.png) # 代码实现 ## 示例 在简单的分布式应用系统(示例代码工程)中,我们设计了 Sidecar 边车模块, Sidecar 的作用是为了给原生的 Socket 增加额外的功能,比如流控、日志等。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20225/27/1653614079361261298.png) Sidecar 模块的设计运用了**装饰者模式**,修饰的是 Socket 。所以客户端其实是把 Sidecar 当成是 Socket 来使用了,比如: ``` // demo/network/http/http_client.go package http // 创建一个新的HTTP客户端,以Socket接口作为入参 func NewClient(socket network.Socket, ip string) (*Client, error) { ... // 一些初始化逻辑 return client, nil } // 使用NewClient时,我们可以传入Sidecar来给Http客户端附加额外的流控功能 client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), "192.168.0.1") ``` 在服务消息中介中,每次收到上游服务的 HTTP 请求,都会调用 http.NewClient 来创建一个 HTTP 客户端,并通过它将请求转发给下游服务: ``` type ServiceMediator struct { ... server *http.Server } // Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task func (s *ServiceMediator) Forward(req *http.Request) *http.Response { ... // 发现下游服务的目的IP地址 dest, err := s.discovery(svcType) // 创建HTTP客户端,硬编码sidecar.NewFlowCtrlSidecar(network.DefaultSocket()) client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), s.localIp) // 通过HTTP客户端转发请求 resp, err := client.Send(dest, forwardReq) ... } ``` 在上述实现中,我们在调用 http.NewClient 时把 sidecar.NewFlowCtrlSidecar(network.DefaultSocket()) 硬编码进去了,那么如果以后要扩展 Sidecar ,就得修改这段代码逻辑,这违反了开闭原则 OCP。 有经验的同学可能会想到,可以通过让 ServiceMediator 依赖 Socket 接口,在 Forward 方法调用 http.NewClient 时把 Socket 接口作为入参;然后在 ServiceMediator 初始化时,将具体类型的 Sidecar 注入到 ServiceMediator 中: ``` type ServiceMediator struct { ... server *http.Server // 依赖Socket抽象接口 socket network.Socket } // Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task func (s *ServiceMediator) Forward(req *http.Request) *http.Response { ... // 发现下游服务的目的IP地址 dest, err := s.discovery(svcType) // 创建HTTP客户端,将s.socket抽象接口作为入参 client, err := http.NewClient(s.socket, s.localIp) // 通过HTTP客户端转发请求 resp, err := client.Send(dest, forwardReq) ... } // 在ServiceMediator初始化时,将具体类型的Sidecar注入到ServiceMediator中 mediator := &ServiceMediator{ socket: sidecar.NewFlowCtrlSidecar(network.DefaultSocket()) } ``` 上述的修改,从原来依赖具体,改成了依赖抽象,符合了开闭原则。 但是, Forward 方法存在并发调用的场景,因此它希望每次被调用时都创建一个新的 Socket/Sidecar 来完成网络通信,否则就需要加锁来保证并发安全。而上述的修改会导致在 ServiceMediator 的生命周期内都使用同一个 Socket/Sidecar,显然不符合要求。 因此,我们需要一个方法,既能够满足开闭原则,而且在每次调用Forward 方法时也能够创建新的 Socket/Sidecar 实例。工厂方法模式恰好就能满足这两点要求,下面我们通过它来完成代码的优化。 # 实现 ``` // demo/sidecar/sidecar_factory.go // 关键点1: 定义一个Sidecar工厂抽象接口 type Factory interface { // 关键点2: 工厂方法返回Socket抽象接口 Create() network.Socket } // 关键点3: 按照需要实现具体的工厂 // demo/sidecar/raw_socket_sidecar_factory.go // RawSocketFactory 只具备原生socket功能的sidecar,实现了Factory接口 type RawSocketFactory struct { } func (r RawSocketFactory) Create() network.Socket { return network.DefaultSocket() } // demo/sidecar/all_in_one_sidecar_factory.go // AllInOneFactory 具备所有功能的sidecar工厂,实现了Factory接口 type AllInOneFactory struct { producer mq.Producible } func (a AllInOneFactory) Create() network.Socket { return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), a.producer) } ``` 上述代码中,我们定义了一个工厂抽象接口 Factory ,并有了 2 个具体的实现 RawSocketFactory 和 AllInOneFactory。最后, ServiceMediator 依赖 Factory ,并在 Forward 方法中通过 Factory 来创建新的 Socket/Sidecar : ``` // demo/service/mediator/service_mediator.go type ServiceMediator struct { ... server *http.Server // 关键点4: 客户端依赖Factory抽象接口 sidecarFactory sidecar.Factory } // Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task func (s *ServiceMediator) Forward(req *http.Request) *http.Response { ... // 发现下游服务的目的IP地址 dest, err := s.discovery(svcType) // 创建HTTP客户端,调用sidecarFactory.Create()生成Socket作为入参 client, err := http.NewClient(s.sidecarFactory.Create(), s.localIp) // 通过HTTP客户端转发请求 resp, err := client.Send(dest, forwardReq) ... } // 关键点5: 在ServiceMediator初始化时,将具体类型的sidecar.Factory注入到ServiceMediator中 mediator := &ServiceMediator{ sidecarFactory: &AllInOneFactory{} // sidecarFactory: &RawSocketFactory{} } ``` 下面总结实现工厂方法模式的几个关键点: 1. 定义一个工厂方法抽象接口,比如前文中的 sidecar.Factory。 2. 工厂方法中,返回需要创建的对象/接口,比如 network.Socket。其中,工厂方法通常命名为 Create。 3. 按照具体需要,定义工厂方法抽象接口的具体实现对象,比如 RawSocketFactory 和 AllInOneFactory。 4. 客户端使用时,依赖工厂方法抽象接口。 5. 在客户端初始化阶段,完成具体工厂对象的依赖注入。 # 扩展 ## Go 风格的实现 前文的工厂方法模式实现,是非常典型的**面向对象风格**,下面我们给出一个更具 Go 风格的实现。 ``` // demo/sidecar/sidecar_factory_func.go // 关键点1: 定义Sidecar工厂方法类型 type FactoryFunc func() network.Socket // 关键点2: 按需定义具体的工厂方法实现,注意这里定义的是工厂方法的工厂方法,返回的是FactoryFunc工厂方法类型 func RawSocketFactoryFunc() FactoryFunc { return func() network.Socket { return network.DefaultSocket() } } func AllInOneFactoryFunc(producer mq.Producible) FactoryFunc { return func() network.Socket { return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer) } } type ServiceMediator struct { ... server *http.Server // 关键点3: 客户端依赖FactoryFunc工厂方法类型 sidecarFactoryFunc FactoryFunc } func (s *ServiceMediator) Forward(req *http.Request) *http.Response { ... dest, err := s.discovery(svcType) // 关键点4: 创建HTTP客户端,调用sidecarFactoryFunc()生成Socket作为入参 client, err := http.NewClient(s.sidecarFactoryFunc(), s.localIp) resp, err := client.Send(dest, forwardReq) ... } // 关键点5: 在ServiceMediator初始化时,将具体类型的FactoryFunc注入到ServiceMediator中 mediator := &ServiceMediator{ sidecarFactoryFunc: RawSocketFactoryFunc() // sidecarFactory: AllInOneFactoryFunc(producer) } ``` 上述的实现,利用了 Go 语言中**函数作为一等公民**的特点,少定义了几个 interface 和 struct,代码更加的简洁。 几个实现的关键点与面向对象风格的实现类似。值得注意的是 关键点2 ,我们相当于定义了一个**工厂方法的工厂方法**,这么做是为了利用函数闭包的特点来**传递参数**。如果直接定义工厂方法,那么 AllInOneFactoryFunc 的实现是下面这样的,无法实现多态: ``` // 并非FactoryFunc类型,无法实现多态 func AllInOneFactoryFunc(producer mq.Producible) network.Socket { return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer) } ``` ## 简单工厂 工厂方法模式的另一个变种是**简单工厂**,它并不通过多态,而是通过简单的 switch-case/if-else 条件判断来决定创建哪种产品: ``` // demo/sidecar/sidecar_simple_factory.go // 关键点1: 定义sidecar类型 type Type uint8 // 关键点2: 按照需要定义sidecar具体类型 const ( Raw Type = iota AllInOne ) // 关键点3: 定义简单工厂对象 type SimpleFactory struct { producer mq.Producible } // 关键点4: 定义工厂方法,入参为sidecar类型,根据switch-case或者if-else来创建产品 func (s SimpleFactory) Create(sidecarType Type) network.Socket { switch sidecarType { case Raw: return network.DefaultSocket() case AllInOne: return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), s.producer) default: return nil } } // 关键点5: 创建产品时传入具体的sidecar类型,比如sidecar.AllInOne simpleFactory := &sidecar.SimpleFactory{producer: producer} sidecar := simpleFactory.Create(sidecar.AllInOne) ``` ## 静态工厂方法 静态工厂方法是 Java/C++ 的说法,主要用于替代构造函数来完成对象的实例化,能够让代码的可读性更好,而且起到了与客户端解耦的作用。比如 Java 的静态工厂方法实现如下: ``` public class Packet { private final Endpoint src; private final Endpoint dest; private final Object payload; private Packet(Endpoint src, Endpoint dest, Object payload) { this.src = src; this.dest = dest; this.payload = payload; } // 静态工厂方法 public static Packet of(Endpoint src, Endpoint dest, Object payload) { return new Packet(src, dest, payload); } ... } // 用法 packet = Packet.of(src, dest, payload) ``` Go 中并没有**静态**一说,直接通过普通函数来完成对象的构造即可,比如: ``` // demo/network/packet.go type Packet struct { src Endpoint dest Endpoint payload interface{} } // 工厂方法 func NewPacket(src, dest Endpoint, payload interface{}) *Packet { return &Packet{ src: src, dest: dest, payload: payload, } } // 用法 packet := NewPacket(src, dest, payload) ``` # 典型应用场景 1. **对象实例化逻辑较为复杂**时,可选择使用工厂方法模式/简单工厂/静态工厂方法来进行封装,为客户端提供一个易用的接口。 2. 如果**实例化的对象/接口涉及多种实现**,可以使用工厂方法模式实现多态。 3. **普通对象的创建,推荐使用静态工厂方法**,比直接的实例化(比如 &Packet{src: src, dest: dest, payload: payload})具备更好的可读性和低耦合。 # 优缺点 ## 优点 1. 代码的可读性更好。 2. 与客户端程序解耦,当实例化逻辑变更时,只需改动工厂方法即可,避免了霰弹式修改。 ## 缺点 1. 引入工厂方法模式会新增一些对象/接口的定义,滥用会导致代码更加复杂。 # 与其他模式的关联 很多同学容易将工厂方法模式和**抽象工厂模式**混淆,抽象工厂模式主要运用在实例化“产品族”的场景,可以看成是工厂方法模式的一种演进。 # 参考 [1] [【Go实现】实践GoF的23种设计模式:SOLID原则](https://mp.weixin.qq.com/s/s3aD4mK2Aw4v99tbCIe9HA), 元闰子 [2] Design Patterns, Chapter 3. Creational Patterns, GoF [3] Factory patterns in Go (Golang), Soham Kamani [4] 工厂方法, 维基百科 简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern–Go-Implementation
  • [知识分享] 设备如何使用go sdk轻松连接华为云IoT平台
    本文分享自华为云社区《[设备如何使用go sdk轻松连接华为云IoT平台](https://bbs.huaweicloud.com/blogs/349842?utm_source=csdn&utm_medium=bbs-ex&utm_campaign=iot&utm_content=content)》,作者:华为云IoT专家团 。 本文介绍使用[huaweicloud-iot-device-sdk-go](https://github.com/ctlove0523/huaweicloud-iot-device-sdk-go) 连接华为云IoT平台,实现简单的华为云文档介绍的四个功能:设备连接鉴权、设备命令、设备消息和设备属性。huaweicloud-iot-device-sdk-go提供设备接入华为云IoT物联网平台的Go版本的SDK,提供设备和平台之间通讯能力,以及设备服务、网关服务、OTA等高级服务。IoT设备开发者使用SDK可以大大简化开发复杂度,快速的接入平台。 Gihub项目地址:[huaweicloud-iot-device-sdk-go](https://github.com/ctlove0523/huaweicloud-iot-device-sdk-go) # 安装和构建 安装和构建的过程取决于使用go的 modules(推荐) 还是还是GOPATH # Modules 如果你使用 modules 只需要导入包"github.com/ctlove0523/huaweicloud-iot-device-sdk-go"即可使用。当你使用go build命令构建项目时,依赖的包会自动被下载。注意使用go build命令构建时会自动下载最新版本,最新版本还没有达到release的标准可能存在一些尚未修复的bug。如果想使用稳定的发布版本可以从release 获取最新稳定的版本号,并在go.mod文件中指定版本号。 module example go 1.15 require github.com/ctlove0523/huaweicloud-iot-device-sdk-go v0.0.1-alpha # GOPATH 如果你使用GOPATH,下面的一条命令即可实现安装 `go get github.com/ctlove0523/huaweicloud-iot-device-sdk-go` # 使用API # 创建设备并初始化 1、首先,在华为云IoT平台创建一个设备,设备的信息如下: 设备ID:5fdb75cccbfe2f02ce81d4bf_go-mqtt 设备密钥:123456789 2、使用SDK创建一个Device对象,并初始化Device。 // 创建一个设备并初始化 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() # 完整样例 import ( "fmt" "github.com/ctlove0523/huaweicloud-iot-device-sdk-go" "time" ) func main() { // 创建一个设备并初始化 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() if device.IsConnected() { fmt.Println("device connect huawei iot platform success") } else { fmt.Println("device connect huawei iot platform failed") } } >iot-mqtts.cn-north-4.myhuaweicloud.com为华为IoT平台(基础班)在华为云北京四的访问端点,如果你购买了标准版或企业版,请将iot-mqtts.cn-north-4.myhuaweicloud.com更换为对应的MQTT协议接入端点。 # 设备处理平台下发的命令 1、首先,在华为云IoT平台创建一个设备,设备的信息如下: 设备ID:5fdb75cccbfe2f02ce81d4bf_go-mqtt 设备密钥:123456789 2、使用SDK创建一个Device对象,并初始化Device。 // 创建一个设备并初始化 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() if device.IsConnected() { fmt.Println("device connect huawei iot platform success") } else { fmt.Println("device connect huawei iot platform failed") } 3、注册命令处理handler,支持注册多个handler并且按照注册的顺序回调 // 添加用于处理平台下发命令的callback device.AddCommandHandler(func(command iot.Command) bool { fmt.Println("First command handler begin to process command.") return true }) device.AddCommandHandler(func(command iot.Command) bool { fmt.Println("Second command handler begin to process command.") return true }) 4、通过应用侧API向设备下发一个命令,可以看到程序输出如下: device connect huawei iot platform success First command handler begin to process command. Second command handler begin to process command. # 完整样例 import ( "fmt" "github.com/ctlove0523/huaweicloud-iot-device-sdk-go" "time" ) // 处理平台下发的同步命令 func main() { // 创建一个设备并初始化 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() if device.IsConnected() { fmt.Println("device connect huawei iot platform success") } else { fmt.Println("device connect huawei iot platform failed") } // 添加用于处理平台下发命令的callback device.AddCommandHandler(func(command iot.Command) bool { fmt.Println("First command handler begin to process command.") return true }) device.AddCommandHandler(func(command iot.Command) bool { fmt.Println("Second command handler begin to process command.") return true }) time.Sleep(1 * time.Minute) } >设备支持的命令定义在产品中 # 设备消息 1、首先,在华为云IoT平台创建一个设备,设备的信息如下: 设备ID:5fdb75cccbfe2f02ce81d4bf_go-mqtt 设备密钥:123456789 2、使用SDK创建一个Device对象,并初始化Device。 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() # 设备消息上报 message := iot.Message{ ObjectDeviceId: uuid.NewV4().String(), Name: "Fist send message to platform", Id: uuid.NewV4().String(), Content: "Hello Huawei IoT Platform", } device.SendMessage(message) # 平台消息下发 接收平台下发的消息,只需注册消息处理handler,支持注册多个handler并按照注册顺序回调。 // 注册平台下发消息的callback,当收到平台下发的消息时,调用此callback. // 支持注册多个callback,并且按照注册顺序调用 device.AddMessageHandler(func(message iot.Message) bool { fmt.Println("first handler called" + iot.Interface2JsonString(message)) return true }) device.AddMessageHandler(func(message iot.Message) bool { fmt.Println("second handler called" + iot.Interface2JsonString(message)) return true }) # 完整样例 import ( "fmt" iot "github.com/ctlove0523/huaweicloud-iot-device-sdk-go" uuid "github.com/satori/go.uuid" "time" ) func main() { // 创建一个设备并初始化 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() // 注册平台下发消息的callback,当收到平台下发的消息时,调用此callback. // 支持注册多个callback,并且按照注册顺序调用 device.AddMessageHandler(func(message iot.Message) bool { fmt.Println("first handler called" + iot.Interface2JsonString(message)) return true }) device.AddMessageHandler(func(message iot.Message) bool { fmt.Println("second handler called" + iot.Interface2JsonString(message)) return true }) //向平台发送消息 message := iot.Message{ ObjectDeviceId: uuid.NewV4().String(), Name: "Fist send message to platform", Id: uuid.NewV4().String(), Content: "Hello Huawei IoT Platform", } device.SendMessage(message) time.Sleep(2 * time.Minute) } # 设备属性 1、首先,在华为云IoT平台创建一个设备,并在该设备下创建3个子设备,设备及子设备的信息如下: 设备ID:5fdb75cccbfe2f02ce81d4bf_go-mqtt 设备密钥:123456789 子设备ID:5fdb75cccbfe2f02ce81d4bf_sub-device-1 子设备ID:5fdb75cccbfe2f02ce81d4bf_sub-device-2 子设备ID:5fdb75cccbfe2f02ce81d4bf_sub-device-3 2、使用SDK创建一个Device对象,并初始化Device。 // 创建设备并初始化 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() fmt.Printf("device connected: %v\n", device.IsConnected()) # 设备属性上报 使用ReportProperties(properties ServiceProperty) bool 上报设备属性 // 设备上报属性 props := iot.ServicePropertyEntry{ ServiceId: "value", EventTime: iot.DataCollectionTime(), Properties: DemoProperties{ Value: "chen tong", MsgType: "23", }, } var content []iot.ServicePropertyEntry content = append(content, props) services := iot.ServiceProperty{ Services: content, } device.ReportProperties(services) # 网关批量设备属性上报 使用BatchReportSubDevicesProperties(service DevicesService) 实现网关批量设备属性上报 // 批量上报子设备属性 subDevice1 := iot.DeviceService{ DeviceId: "5fdb75cccbfe2f02ce81d4bf_sub-device-1", Services: content, } subDevice2 := iot.DeviceService{ DeviceId: "5fdb75cccbfe2f02ce81d4bf_sub-device-2", Services: content, } subDevice3 := iot.DeviceService{ DeviceId: "5fdb75cccbfe2f02ce81d4bf_sub-device-3", Services: content, } var devices []iot.DeviceService devices = append(devices, subDevice1, subDevice2, subDevice3) device.BatchReportSubDevicesProperties(iot.DevicesService{ Devices: devices, }) # 平台设置设备属性 使用AddPropertiesSetHandler(handler DevicePropertiesSetHandler) 注册平台设置设备属性handler,当接收到平台的命令时SDK回调。 // 注册平台设置属性callback,当应用通过API设置设备属性时,会调用此callback,支持注册多个callback device.AddPropertiesSetHandler(func(propertiesSetRequest iot.DevicePropertyDownRequest) bool { fmt.Println("I get property set command") fmt.Printf("request is %s", iot.Interface2JsonString(propertiesSetRequest)) return true }) # 平台查询设备属性 使用SetPropertyQueryHandler(handler DevicePropertyQueryHandler)注册平台查询设备属性handler,当接收到平台的查询请求时SDK回调。 // 注册平台查询设备属性callback,当平台查询设备属性时此callback被调用,仅支持设置一个callback device.SetPropertyQueryHandler(func(query iot.DevicePropertyQueryRequest) iot.ServicePropertyEntry { return iot.ServicePropertyEntry{ ServiceId: "value", Properties: DemoProperties{ Value: "QUERY RESPONSE", MsgType: "query property", }, EventTime: "2020-12-19 02:23:24", } }) # 设备侧获取平台的设备影子数据 使用QueryDeviceShadow(query DevicePropertyQueryRequest, handler DevicePropertyQueryResponseHandler) 可以查询平台的设备影子数据,当接收到平台的响应后SDK自动回调DevicePropertyQueryResponseHandler。 // 设备查询设备影子数据 device.QueryDeviceShadow(iot.DevicePropertyQueryRequest{ ServiceId: "value", }, func(response iot.DevicePropertyQueryResponse) { fmt.Printf("query device shadow success.\n,device shadow data is %s\n", iot.Interface2JsonString(response)) }) # 完整样例 import ( "fmt" iot "github.com/ctlove0523/huaweicloud-iot-device-sdk-go" "time" ) func main() { // 创建设备并初始化 device := iot.CreateIotDevice("5fdb75cccbfe2f02ce81d4bf_go-mqtt", "123456789", "tcp://iot-mqtts.cn-north-4.myhuaweicloud.com:1883") device.Init() fmt.Printf("device connected: %v\n", device.IsConnected()) // 注册平台设置属性callback,当应用通过API设置设备属性时,会调用此callback,支持注册多个callback device.AddPropertiesSetHandler(func(propertiesSetRequest iot.DevicePropertyDownRequest) bool { fmt.Println("I get property set command") fmt.Printf("request is %s", iot.Interface2JsonString(propertiesSetRequest)) return true }) // 注册平台查询设备属性callback,当平台查询设备属性时此callback被调用,仅支持设置一个callback device.SetPropertyQueryHandler(func(query iot.DevicePropertyQueryRequest) iot.ServicePropertyEntry { return iot.ServicePropertyEntry{ ServiceId: "value", Properties: DemoProperties{ Value: "QUERY RESPONSE", MsgType: "query property", }, EventTime: "2020-12-19 02:23:24", } }) // 设备上报属性 props := iot.ServicePropertyEntry{ ServiceId: "value", EventTime: iot.DataCollectionTime(), Properties: DemoProperties{ Value: "chen tong", MsgType: "23", }, } var content []iot.ServicePropertyEntry content = append(content, props) services := iot.ServiceProperty{ Services: content, } device.ReportProperties(services) // 设备查询设备影子数据 device.QueryDeviceShadow(iot.DevicePropertyQueryRequest{ ServiceId: "value", }, func(response iot.DevicePropertyQueryResponse) { fmt.Printf("query device shadow success.\n,device shadow data is %s\n", iot.Interface2JsonString(response)) }) // 批量上报子设备属性 subDevice1 := iot.DeviceService{ DeviceId: "5fdb75cccbfe2f02ce81d4bf_sub-device-1", Services: content, } subDevice2 := iot.DeviceService{ DeviceId: "5fdb75cccbfe2f02ce81d4bf_sub-device-2", Services: content, } subDevice3 := iot.DeviceService{ DeviceId: "5fdb75cccbfe2f02ce81d4bf_sub-device-3", Services: content, } var devices []iot.DeviceService devices = append(devices, subDevice1, subDevice2, subDevice3) device.BatchReportSubDevicesProperties(iot.DevicesService{ Devices: devices, }) time.Sleep(1 * time.Minute) } type DemoProperties struct { Value string `json:"value"` MsgType string `json:"msgType"` }
  • [技术干货] 【Go实现】实践GoF的23种设计模式:单例模式[转载]
    上一篇:【Go实现】实践GoF的23种设计模式:SOLID原则简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern–Go-Implementation简述GoF 对单例模式(Singleton)的定义如下:Ensure a class only has one instance, and provide a global point of access to it.也即,保证一个类只有一个实例,并且为它提供一个全局访问点。在程序设计中,有些对象通常只需要一个共享的实例,比如线程池、全局缓存、对象池等。实现共享实例最简单直接的方式就是全局变量。但是,使用全局变量会带来一些问题,比如:客户端程序可以创建同类实例,从而无法保证在整系统上只有一个共享实例。难以控制对象的访问,比如想增加一个“访问次数统计”的功能就很难,可扩展性较低。把实现细节暴露给客户端程序,加深了耦合,容易产生霰弹式修改。对这种全局唯一的场景,更好的是使用单例模式去实现。单例模式能够限制客户端程序创建同类实例,并且可以在全局访问点上扩展或修改功能,而不影响客户端程序。但是,并非所有的全局唯一都适用单例模式。比如下面这种场景:考虑需要统计一个API调用的情况,有两个指标,成功调用次数和失败调用次数。这两个指标都是全局唯一的,所以有人可能会将其建模成两个单例SuccessApiMetric和FailApiMetric。按照这个思路,随着指标数量的增多,你会发现代码里类的定义会越来越多,也越来越臃肿。这也是单例模式最常见的误用场景,更好的方法是将两个指标设计成一个对象ApiMetric下的两个实例ApiMetic success和ApiMetic fail。那么,如何判断一个对象是否应该被建模成单例?通常,被建模成单例的对象都有“中心点”的含义,比如线程池就是管理所有线程的中心。所以,在判断一个对象是否适合单例模式时,先思考下,是一个中心点吗?UML结构代码实现根据单例模式的定义,实现的关键点有两个:限制调用者直接实例化该对象;为该对象的单例提供一个全局唯一的访问方法。对于 C++ / Java 而言,只需把对象的构造函数设计成私有的,并提供一个 static 方法去访问该对象的唯一实例即可。但 Go 语言并没有构造函数的概念,也没有 static 方法,所以需要另寻出路。我们可以利用 Go 语言 package 的访问规则来实现,将单例对象设计成首字母小写,这样就能限定它的访问范围只在当前package下,模拟了 C++ / Java 的私有构造函数;然后,在当前 package 下实现一个首字母大写的访问函数,也就相当于 static 方法的作用了。示例在简单的分布式应用系统(示例代码工程)中,我们定义了一个网络模块 network,模拟实现了网络报文转发功能。network 的设计也很简单,通过一个哈希表维持了 Endpoint 到 Socket 的映射,报文转发时,通过 Endpoint 寻址到 Socket,再调用 Socket 的 Receive 方法完成转发。因为整系统只需一个 network 对象,而且它在领域模型中具有中心点的语义,所以我们很自然地使用单例模式来实现它。单例模式大致可以分成两类,“饿汉模式”和“懒汉模式”。前者是在系统初始化期间就完成了单例对象的实例化;后者则是在调用时才进行延迟实例化,从而一定程度上节省了内存。“饿汉模式”实现// demo/network/network.go package network // 1、设计为小写字母开头,表示只在network包内可见,限制客户端程序的实例化 type network struct { sockets sync.Mapvar instancevar instance } // 2、定义一个包内可见的实例对象,也即单例 var instance = &network{sockets: sync.Map{}} // 3、定义一个全局可见的唯一访问方法 func Instance() *network { return instance } func (n *network) Listen(endpoint Endpoint, socket Socket) error { if _, ok := n.sockets.Load(endpoint); ok { return ErrEndpointAlreadyListened } n.sockets.Store(endpoint, socket) return nil } func (n *network) Send(packet *Packet) error { record, rOk := n.sockets.Load(packet.Dest()) socket, sOk := record.(Socket) if !rOk || !sOk { return ErrConnectionRefuse } go socket.Receive(packet) return nil }那么,客户端就可以通过 network.Instance() 引用该单例了:// demo/sidecar/flowctrl_sidecar.go package sidecar type FlowCtrlSidecar struct {...} // 通过 network.Instance() 直接引用单例 func (f *FlowCtrlSidecar) Listen(endpoint network.Endpoint) error { return network.Instance().Listen(endpoint, f) } ...“懒汉模式”实现众所周知,“懒汉模式”会带来线程安全问题,可以通过普通加锁,或者更高效的双重检验加锁来优化。不管是哪种方法,都是为了保证单例只会被初始化一次。type network struct {...} // 单例 var instance *network // 定义互斥锁 var mutex = sync.Mutex{} // 普通加锁,缺点是每次调用 Instance() 都需要加锁 func Instance() *network { mutex.Lock() if instance == nil { instance = &network{sockets: sync.Map{}} } mutex.Unlock() return instance } // 双重检验后加锁,实例化后无需加锁 func Instance() *network { if instance == nil { mutex.Lock() if instance == nil { instance = &network{sockets: sync.Map{}} } mutex.Unlock() } return instance }对于“懒汉模式”,Go 语言还有一个更优雅的实现方式,那就是利用 sync.Once。它有一个 Do 方法,方法声明为 func (o *Once) Do(f func()),其中入参是 func() 的方法类型,Go 会保证该方法仅会被调用一次。利用这个特性,我们就能够实现单例只被初始化一次了。type network struct {...} // 单例 var instance *network // 定义 once 对象 var once = sync.Once{} // 通过once对象确保instance只被初始化一次 func Instance() *network { once.Do(func() { // 只会被调用一次 instance = &network{sockets: sync.Map{}} }) return instance }扩展提供多个实例虽然单例模式从定义上表示每个对象只能有一个实例,但是我们不应该被该定义限制住,还得从模式本身的动机来去理解它。单例模式的一大动机是限制客户端程序对对象进行实例化,至于实例有多少个其实并不重要,根据具体场景来进行建模、设计即可。比如在前面的 network 模块中,现在新增一个这样的需求,将网络拆分为互联网和局域网。那么,我们可以这么设计:type network struct {...} // 定义互联网单例 var inetInstance = &network{sockets: sync.Map{}} // 定义局域网单例 var lanInstance = &network{sockets: sync.Map{}} // 定义互联网全局可见的唯一访问方法 func Internet() *network { return inetInstance } // 定义局域网全局可见的唯一访问方法 func Lan() *network { return lanInstance }虽然上述例子中,network 结构有两个实例,但是本质上还是单例模式,因为它做到了限制客户端实例化,以及为每个单例提供了全局唯一的访问方法。提供多种实现单例模式也可以实现多态,如果你预测该单例未来可能会扩展,那么就可以将它设计成抽象的接口,让客户端依赖抽象,这样,未来扩展时就无需改动客户端程序了。比如,我们可以 network 设计为一个抽象接口:// network 抽象接口 type network interface { Listen(endpoint Endpoint, socket Socket) error Send(packet *Packet) error } // network 的实现1 type networkImpl1 struct { sockets sync.Map } func (n *networkImpl1) Listen(endpoint Endpoint, socket Socket) error {...} func (n *networkImpl1) Send(packet *Packet) error {...} // networkImpl1 实现的单例 var instance = &networkImpl1{sockets: sync.Map{}} // 定义全局可见的唯一访问方法,注意返回值时network抽象接口! func Instance() network { return instance } // 客户端使用示例 func client() { packet := network.NewPacket(srcEndpoint, destEndpoint, payload) network.Instance().Send(packet) }如果未来需要新增一种 networkImpl2 实现,那么我们只需修改 instance 的初始化逻辑即可,客户端程序无需改动:// 新增network 的实现2 type networkImpl2 struct {...} func (n *networkImpl2) Listen(endpoint Endpoint, socket Socket) error {...} func (n *networkImpl2) Send(packet *Packet) error {...} // 将单例 instance 修改为 networkImpl2 实现 var instance = &networkImpl2{...} // 单例全局访问方法无需改动 func Instance() network { return instance } // 客户端使用也无需改动 func client() { packet := network.NewPacket(srcEndpoint, destEndpoint, payload) network.Instance().Send(packet) }有时候,我们还可能需要通过读取配置来决定使用哪种单例实现,那么,我们可以通过 map 来维护所有的实现,然后根据具体配置来选取对应的实现:// network 抽象接口 type network interface { Listen(endpoint Endpoint, socket Socket) error Send(packet *Packet) error } // network 具体实现 type networkImpl1 struct {...} type networkImpl2 struct {...} type networkImpl3 struct {...} type networkImpl4 struct {...} // 单例 map var instances = make(map[string]network) // 初始化所有的单例 func init() { instances["impl1"] = &networkImpl1{...} instances["impl2"] = &networkImpl2{...} instances["impl3"] = &networkImpl3{...} instances["impl4"] = &networkImpl4{...} } // 全局单例访问方法,通过读取配置决定使用哪种实现 func Instance() network { impl := readConf() instance, ok := instances[impl] if !ok { panic("instance not found") } return instance }典型应用场景日志。每个服务通常都会需要一个全局的日志对象来记录本服务产生的日志。全局配置。对于一些全局的配置,可以通过定义一个单例来供客户端使用。唯一序列号生成。唯一序列号生成必然要求整系统只能有一个生成实例,非常合适使用单例模式。线程池、对象池、连接池等。xxx池的本质就是共享,也是单例模式的常见场景。全局缓存…优缺点优点在合适的场景,使用单例模式有如下的优点:整系统只有一个或几个实例,有效节省了内存和对象创建的开销。通过全局访问点,可以方便地扩展功能,比如新增加访问次数的统计。对客户端隐藏实现细节,可避免霰弹式修改。缺点虽然单例模式相比全局变量有诸多的优点,但它本质上还是一个“全局变量”,还是避免不了全局变量的一些缺点:函数调用的隐式耦合。通常我们都期望从函数的声明中就能知道该函数做了什么、依赖了什么、返回了什么。使用使用单例模式就意味着,无需通过函数传参,就能够在函数中使用该实例。也即将依赖/耦合隐式化了,不利于更好地理解代码。对测试不友好。通常对一个方法/函数进行测试,我们并不需要知道它的具体实现。但如果方法/函数中有使用单例对象,我们就不得不考虑单例状态的变化了,也即需要考虑方法/函数的具体实现了。并发问题。共享就意味着可能存在并发问题,我们不仅需要在初始化阶段考虑并发问题,在初始化后更是要时刻注意。因此,在高并发的场景,单例模式也可能存在锁冲突问题。单例模式虽然简单易用,但也是最容易被滥用的设计模式。它并不是“银弹”,在实际使用时,还需根据具体的业务场景谨慎使用。与其他模式的关联工厂方法模式、抽象工厂模式很多时候都会以单例模式来实现,因为工厂类通常是无状态的,而且全局只需一个实例即可,能够有效避免对象的频繁创建和销毁。
总条数:166 到第
上滑加载中