惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
GbyAI
GbyAI
aimingoo的专栏
aimingoo的专栏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
M
MIT News - Artificial intelligence
腾讯CDC
博客园 - Franky
Engineering at Meta
Engineering at Meta
C
Check Point Blog
T
The Blog of Author Tim Ferriss
有赞技术团队
有赞技术团队
Microsoft Azure Blog
Microsoft Azure Blog
MyScale Blog
MyScale Blog
I
InfoQ
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
N
Netflix TechBlog - Medium
Last Week in AI
Last Week in AI
S
SegmentFault 最新的问题
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
WordPress大学
WordPress大学

博客园 - 程序员老刘

SaaS多租户改造Spring项目核心代码 windows11的vmware启动报错 MySQL8的root帐号授权 centrifugal 的IM消息服务历史消息报错 支持restart的启动脚本 golang strings.Split的疑问 golang 常用的日期方法和时区的坑 golang SQLite3性能测试 golang的多协程实践 golang redis的模式订阅 golang 实现并发计算文件数量 java开发的zimg客户端 CAS单点登录实践(spring cas client配置) 81进制,用多进制方式把一个长长的整数变短 chrome浏览器自动填充失效问题 spring 登录提示 Bad credentials spring 项目tomcat 8.0.2 发布报错:Could not initialize class org.hibernate.validator.engine.ConfigurationImpl spring tiles界面为空白 activiti部署到linux后流程图不显示汉字的问题
golang的极简流式编程实现
程序员老刘 · 2018-01-29 · via 博客园 - 程序员老刘

传统的过程编码方式带来的弊端是显而易见,我们经常有这样的经验,一段时间不维护的代码或者别人的代码,突然拉回来看需要花费较长的时间,理解原来的思路,如果此时有个文档或者注释写的很好的话,可能花的时间会短一点,但是即便如此,很多调用关系也要反复确认才敢动手改动。下面是一断伪代码,描述过程编码方式:

func A(){
    B()
    C()
}

func B(){
    do something
    D()
}
func C(){
    do something    
}
func D(){
    do something
}
func main(){
    A()
}

对照流式风格的写法:

NewStream().
Next(A).
Next(B).
Next(D).
Next(C).
Go()

当过程风格的代码调用关系复杂时,程序员需要谨慎仔细行事,相比较流式风格的代码比较清爽,主干清晰,尤其是应对需求变更的时候优势明显。

java8里借用lamda表达式实现了一套比较完美的流式编程风格,golang作为一个简洁的语言还没有官方的流式风格的包(可能早就有了,可能是我孤陋寡闻了)有点可惜了。

我参考了gorequest的代码,实现了一套相对比较通用的流式风格的包,实现原理是组成一个任务链表,每一个节点都保存了首节点和下一个节点以及该节点应该执行的回调函数指针,流式任务启动后从第一个节点开始,逐个执行,遇到异常则终止流式任务,直到执行到最后一个,结束任务链。先来看看代码吧:

package Stream

import (
    "errors"
    "fmt"
)

/**
流式工作原理:
各个任务都过指针链表的方式组成一个任务链,这个任务链从第一个开始执行,直到最后一个
每一个任务节点执行完毕会将结果带入到下一级任务节点中。
每一个任务是一个Stream节点,每个任务节点都包含首节点和下一个任务节点的指针,
除了首节点,每个节都会设置一个回调函数的指针,用本节点的任务执行,
最后一个节点的nextStream为空,表示任务链结束。
**/

//定回调函数指针的类型
type CB func(interface{}) (interface{}, error)

//任务节点结构定义
type Stream struct {
    //任务链表首节点,其他非首节点此指针永远指向首节点
    firstStream *Stream
    //任务链表下一个节点,为空表示任务结束
    nextStream *Stream
    //当前任务对应的执行处理函数,首节点没有可执行任务,处理函数指针为空
    cb CB
}

/**
创建新的流
**/
func NewStream() *Stream {
    //生成新的节点
    stream := &Stream{}
    //设置第一个首节点,为自己
    //其他节点会调用run方法将从firs指针开始执行,直到next为空
    stream.firstStream = stream
    //fmt.Println("new first", stream)
    return stream
}

/**
流结束
arg为流初始参数,初始参数放在End方法中是考虑到初始参数不需在任务链中传递
**/
func (this *Stream) Go(arg interface{}) (interface{}, error) {
    //设置为任务链结束
    this.nextStream = nil
    //fmt.Println("first=", this.firstStream, "second=", this.firstStream.nextStream)
    //检查是否有任务节点存在,存在则调用run方法
    //run方法是首先执行本任务回调函数指针,然后查找下一个任务节点,并调用run方法
    if this.firstStream.nextStream != nil {
        return this.firstStream.nextStream.run(arg)
    } else {
        //流式任务终止
        return nil, errors.New("Not found execute node.")
    }
}
func (this *Stream) run(arg interface{}) (interface{}, error) {
    //fmt.Println("run,args=", args)
    //执行本节点函数指针
    result, err := this.cb(arg)
    //然后调用下一个节点的Run方法
    if this.nextStream != nil && err == nil {
        return this.nextStream.run(result)
    } else {
        //任务链终端,流式任务执行完毕
        return result, err
    }
}
func (this *Stream) Next(cb CB) *Stream {
    //创建新的Stream,将新的任务节点Stream连接在后面
    this.nextStream = &Stream{}
    //设置流式任务链的首节点
    this.nextStream.firstStream = this.firstStream
    //设置本任务的回调函数指针
    this.nextStream.cb = cb
    //fmt.Println("next=", this.nextStream)
    return this.nextStream
}

下面是一个流式的例子,这里以早上起床到出门上班的流程为例:

//起床
func GetUP(arg interface{}) (interface{}, error) {
    t, _ := arg.(string)
    fmt.Println("铃铃.......", t, "###到时间啦,再不起又要迟到了!")
    return "醒着的状态", nil
}

//蹲坑
func GetPit(arg interface{}) (interface{}, error) {
    s, _ := arg.(string)
    fmt.Println(s, "###每早必做的功课,蹲坑!")
    return "舒服啦", nil
}

//洗脸
func GetFace(arg interface{}) (interface{}, error) {
    s, _ := arg.(string)
    fmt.Println(s, "###洗脸很重要!")
    return "脸已经洗干净了,可以去见人了", nil
}

//刷牙
func GetTooth(arg interface{}) (interface{}, error) {
    s, _ := arg.(string)
    fmt.Println(s, "###刷牙也很重要!")
    return "牙也刷干净了,可以放心的大笑", nil
}

//吃早饭
func GetEat(arg interface{}) (interface{}, error) {
    s, _ := arg.(string)
    fmt.Println(s, "###吃饭是必须的(需求变更了,原来的流程里没有,这次加上)")
    return "吃饱饱了", nil
}

//换衣服
func GetCloth(arg interface{}) (interface{}, error) {
    s, _ := arg.(string)
    fmt.Println(s, "###还要增加一个换衣服的流程!")
    return "找到心仪的衣服了", nil
}

//出门
func GetOut(arg interface{}) (interface{}, error) {
    s, _ := arg.(string)
    fmt.Println(s, "###一切就绪,可以出门啦!")
    return "", nil

}
func main() {
    NewStream().
        Next(GetUP).
        Next(GetPit).
        Next(GetTooth).
        Next(GetFace).
        Next(GetEat).//需求变更了后加上的
        Next(GetCloth).
        Next(GetOut).
        Go("2018年1月28日8点10分")
}

从上面的代码看,流式编码风格对于一个大的任务被分解成多个小任务后,在代码层面是非常直观的,不在费劲心思去查找到底那个调用了那个,另外对于需求的变更更容易了,上例中的吃早饭是第一个版本没有实现的,客户说了早上要吃饭,不然容易的胆结石,第二版要加上,我们需要完成吃饭的函数,然后加到响应的位置。相对过程编码简单了不少。