Dec 2, 2017 - golang ast初探:一个懒人源码分析工具

前言

实现在此

https://github.com/JodeZer/lazydog

无关

从工作以来,渐渐明白了一个作为软件工程师的哲学:在大多数公司,好的工程师定义不是学术水平、认知、涉猎这样比较务虚的东西,这些属于架构师、CTO的范围;而作为一线工程师,非常简单的好的定义就是解决问题。在linux服务端的开源世界里,所有的问题都能通过阅读源码解决。

有关

golang的源码我认为是我用过的所有编程语言里最容易读的,但还是有一个颇不爽的点就是读接口。golang使用组合interface{}的方式实现复杂功能的抽象。具体概念,在此不表。因为是隐式实现,没有诸如java、c++那样的显式继承。例如:

type I interface{
	Say()
}

type Cat struct {
}

func(this *Cat) Say(){
	println("fucking miao")
}

type Dog struct {
}

func (this *Dog) Say(){
	pringln("fucking dog")
}

例子中Dog与Cat的结构实现了I。 在复杂工程中,接口当然不会如此简单,从运行入口开始看,见到的都是更高级、组合多次的高级抽象,生扒代码有时候绕来绕去,还真的一下子不能把握某个接口到底具体的实现是什么。因为我们读源码,不是为了看设计,有时候就是想看某个功能到底是怎么实现的。尤其有时还有运行时确定接口实现的代码,例如:

func GenI(i int) I {
	if i >0 {
		return &Dog{}
	}
	return &Cat{}
}

别的不多说,类似这种代码,在mgo里是有的,有时候代码定位到这种接口,心里就有点MMP了…

简单粗暴的偷懒梦想

从刚参加工作,面对十万行计C语言的金融业务系统到阅读mongodb、mysql、nginx等著名开源项目代码,我都有一个梦想,运行的时候,输出日志能够告诉我,代码路径到底是什么。这样我就能沿着代码路径直接找到关键点,而不是在代码跳转里转圈子。想想有时候走投无路的打桩调试大法(手动滑稽),其实就是这么一件事。在做C的金融系统的时候,我就想写一个工具,在代码文本的每行代码之间都插入一条日志输出,这样所有的if…else跳到了那里,循环了多少次,递归层数,我都能通过分析日志看出来。 这个梦想在我转做了golang之后终于有了进展,因为golang的依赖简单,有runtime,更重要得是…他有一个parser…

golang-疯起来连自己都parse的语言

也许有一部分用了很久golang的程序员都不一定知道,golang标准库中自带了golang自身的parser。具体官方例子在此 功能已经非常完整了,那就是把指定的源码文件,解析成golang标准ast。有了ast结构,给想看的源码插入任意代码就成为了可能。比如:

type I interface{
	Say()
}

type Cat struct {
}

func(this *Cat) Say(){
	println("this is cat say") // 插入的代码
	println("fucking miao")
}

type Dog struct {
}

func (this *Dog) Say(){
	println("this is dog say") // 插入的代码
	pringln("fucking dog")
}

这样的代码,说白了就是在Dog.Say与Cat.Say的声明结构里的第一个语句变成一个我想要的语句。

lazydog所做的事情

  1. 备份目标目录下所有的.go源码
  2. 在每个目录下生成一个相应package的帮助文件
  3. parse源文件的ast,注入代码,生成新文件,本质是在每个函数的第一行插入一个打印日志的调用,调用的是同包下的帮助函数

重编译源代码项目后,输出的日志中就会打印出目前的调用是在哪个文件的哪一行,调用函数名称是什么。 完成后可以用相应的命令恢复源代码。

结束

用ast其实可以做很多很强大的代码生成功能,甚至想过是不是可以为此写一种支持泛型的新语言,后端编译成golang的代码。目前这个工具只是业余时间尝试一下。

Jun 18, 2017 - “远离枯燥的golang系列”之这次决定这么使用mgo

前言

之前的文章中提到mgo的使用方式是基于session对象的,关于mgo与session,参考旧文mgo的session与连接池。本文将会探讨我们在生产中是具体如何使用这个库的。不意在确定一种万用的“正确”方法,而是记录一下方便日后批评与自我批评(XD)。

当然,严格来说,这个问题跟golang语言的使用没啥关系;但我就是觉得之前的使用方法很枯燥,好气哦!

stateless的包方法使用

顾名思义,就是将mongo的各种数据库操作写成一个个方法调用,比如要根据id查找一条文档,那么就有如下的exported package function

package mongo

func FindById(id) (DocStruct, err){
	session := GETSESSION()
	defer session.Close()
	Doc := DocStruct{}
	err := session.DB.C.Find({},&Doc)
	return Doc, err
}

使用的时候就是在业务代码中,调用一下这个方法。


func () {
	doc ,err:= mongo.FindById(id)

}

这种操作,我自己称之为无状态(stateless)的数据库操作,因为这种操作只是一个动作,他也不会涉及到管理任何的连接或者数据状态。当然这种写法是没什么问题,在操作简单、固定的情况下;吹毛求疵的话,就是session的行为不太好控制。

但是当数据库操作变得复杂,一个结构的行为多样化的时候,就会产生很多这样的方法。比如需要根据id,对某个字段CRUD,那就要产生四个方法,在业务代码中进行组合。还有一些更复杂的针对mongo 嵌套文档的CURD就更是如此了。这样的问题第一是,要产生多个session对象,其实完全没必要,(session获得连接的效率问题在旧文中也提到过);另一个是操作很多,操作过于松散,总感觉不好维护。有时候甚至会忘记某个操作之前实现过,而写了个差不多得方法满足新的需要。

新项目中的尝试

首先上帝说要有对象

在目睹了一个包中,针对无数个文档有无数野生方法的惨状以后,我决定放弃这种exported package function的类C用法。我觉得使用struct抽象出对象模型,每个对象的声明周期内,都会持有一个session,在对象结束时使用session。这样的话,一个文档生命周期内所有的操作,就并不必产生多余的session对象,对mgo的并发问题也更加友好。struct拥有自己的CURD操作,感觉也更规整一些。

具体我是这么做的。

有一个以文档为单位的interface{}


type IDoc interface {
	DB () string
	Col() string
	Close()
}

假设有一个mongo collection存储着数据模型为Person的数据


type Person struct{
	Id bson.ObjectId `bson:"_id"`
	Name string `bson:"name"`
	Age int `bson:"age"`
}

首先规定所有的文档结构定义都要实现IDoc的接口,当然其实这个卵用不大,只是希望规范一下模型;具体用处下文细讲。

其实不实现也没问题,IDoc不是作为一个操作抽象存在的;但我还是会在全局定义的地方写

var _ IDoc = &Person{}

保证实现了方法。

然后上帝说要善待session

定义了一个包装mgo.Session的结构体。(我这次不希望mgo的import在我的项目里到处跑)

type SessionHelper struct {
	*mgo.Session
}

func (s *SessionHelper)CloseHelper() {
	s.Close()
}

重新定一下Person,让他持有一个SessionHelper


type Person struct{
	SessionHelper `bson:"-"`
	Id bson.ObjectId `bson:"_id"`
	Name string `bson:"name"`
	Age int `bson:"age"`
}

之后就可以基于这样的结构,实现任意的CRUD方法,比如:

func (p *Persion) GetName() ()string,error){
	p.DB(p.DB()).Col(p.Col()).Find(bson.M{...}).Select(bson.M{...})
	...

对于业务代码来说,复杂的sql在method receiver上去现实,外部不关心的操作可以匿名;对session非常友好。

当然,doc还需要New方法

func NewPerson() *Person{
	return &Person{	
		SessionHelper:GETSESSION(),
	}
}

使用时候的画风就是

func foo() {
	p := NewPerson()
	defer p.Close()
	p.LoadById(id)
	p.GetName()
	...
	p.DoXXX()

}

反正我觉得比如下这种nice很多。包方法毕竟是共享名字的,有时候不看代码也搞不太清一个方法到底是要操作哪个数据结构。


func foo() {
     p := &Person{}
     p.Name = mongo.FindName(id)
     p.xxx = mongo.Findxxx()
    ...
}

一个缺点

大概一个缺点就是,因为集成了session,每个对象在生成以后必须手动Close,否则会造成mgo的连接泄露。其实这跟每次生成session的方法是一样的,也是要成对生成释放的,也没差。

爬坑时间

golang的作用域

事情大概是这样的,有一个大文档,里面引用了某些小文档的id,(我采用的方式是手动引用,mongo其实提供了一种reference方法)我觉得没差就没用。

引用小对象的话,CURD之前先要确定这些小对象存不存在,那就有了如下的过程

func (d *Doc) Foo() {
	obj1 := NewObj1()
	defer obj1.Close()
	obj1.Load(id)
	obj1.Exists()
	
	obj2 := NewObj1()
	defer obj2.Close()
	obj2.Load(id)
	obj2.Exists()
	
}

这当然也没什么问题,但obj1,obj2的声明周期过于长了,在这个过程中,会有三条mongo的连接被使用到,其中两个释放要等到整个函数结束。显然太浪费了。

于是我一抠脚,写出了如下的代码:


func (d *Doc) Foo() {
	{
	obj1 := NewObj1()
	defer obj1.Close()
	obj1.Load(id)
	obj1.Exists()
	}
	
	{
	obj2 := NewObj1()
	defer obj2.Close()
	obj2.Load(id)
	obj2.Exists()
	}
	
}

十秒钟以后,我意识到这是没有任何意义的;因为goalng的defer是注册在函数栈上的,跟作用域没有半毛钱关系,这两个defer照样是在函数返回的时候执行的。

其实到这里,封装成两个函数,缩小一下函数栈就可以达到控制生命周期释放连接的目的;但是老师,我不服,我有看上去更酷炫的写法。

十秒钟以后,这个函数变成了这样


func (d *Doc) Foo() {
	{
	obj1 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj1.Close()
	})
	obj1.Load(id)
	obj1.Exists()
	}
	
	{
	obj2 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj2.Close()
	}
	obj2.Load(id)
	obj2.Exists()
	}
	
}

这样就看上去给力多了是不是!runtime.SetFinalizer会在运行时发现注册对象失去引用的时候调用回调方法。

但是!现实是很残酷的,这段代码其实完全不给力,因为运行时发现对象失去引用是需要一段时间的,这段时间甚至比defer版本更久~

那老师!就没有更给力的了吗。当然有!

func (d *Doc) Foo() {
	{
	obj1 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj1.Close()
	}) 
	obj1.Load(id)
	obj1.Exists()
	}
	runtime.GC()
	
	{
	obj2 := NewObj1()
	runtime.SetFinalizer(obj1,func(obj interface{}){
		obj2.Close()
	}
	obj2.Load(id)
	obj2.Exists()
	}
	
	runtime.GC()
	
}

每当我想要弄死的对象出了作用域,就调用一次强制GC,连接就会毫不犹豫立马释放。

不过最后,我还是为这个Doc实现了小函数来判断引用是否存在;因为这个世界上存在一种行为叫code review,写这种代码会被搞事情的!(逃)

bson.Unmarshal的置空行为

使用这种对象模型,在查询上面很容易地想到就是用自己的引用来反序列化

比如

func (p *Persion)Load(id) {
	p.SessionHelper.Find(...).One(p)
	 
}

这个函数的预期就是把所有字段查询出来,再从bson格式反序列化到p的各个字段。但是,这个mgo下的bson实现有一个我现在还不太理解的动作,会将传入反序列化的实例置为空,而且是无视tag的粗暴的置为空。

也就是说,假设我的结构里面有一些跟数据库字段无关的bson:"-"的辅助字段,用于保存一些计算的结果之类的。

例如:


type Person struct {
	...
	Field string `bson:"-"`
}

mgo这个bson老哥会把我的Field字段置为空。那好气哦。 内部调用的方法就是bson.Unmarshal,具体里面的实现这里就不多说了,代码很好找;这么做原因不懂,提了issue也没人理(好气QAQ)。 没人理的issue

结语

希望mongo官方早日能出自己的驱动!

Jun 15, 2017 - “远离枯燥的golang系列”之更好的base64

前言

今天开始,会经常写一些小的代码片段,放在这个“远离枯燥的golang”系列里面。众所周知,golang是一门语法非常简单的语言,其实每天用一门这样的语言写代码,功能实现很快,但是可能会渐渐失去写代码的乐趣,感觉每天都在写if err != nil。曾经就有个笑话说,连java程序员都写完代码了,golang程序员还在写if err != nil。所以就会把工作中突发奇想想到的一些我自认为有点意思的golang代码的小片段记录在这里。

base64解码

一般来说,用到base64最频繁的场景就是,有一个base64编码过的string,然后将其解码成另一个string,用最简单的方式操作大概就是这样:

package main
import "base64"
import "fmt"
func main(){
	b64Str := "dGhpcyBpcyBhIGV4YW1wbGU="
	var res string
	if bytes ,err := base64.StdEncoding.DecodeString(b64Str);err != nil {
		fmt.Println("err!")
	} else {
		res = string(res)
	}
}

这种平平无奇的代码写多就会想吐呢!今天我想了下,至少写一点审美情趣不一样的代码吧。我首先就是不想看到这个多值返回!而且他喵的返回值是个字节数组,真是讨厌

回调替代多值返回

于是,我先写出了如下代码:


package main
import "base64"
import "fmt"

func myb64Decode(f func([]bytes),b64Str string) error {
	 if bytes ,err := base64.StdEncoding.DecodeString(b64Str) ;err != nil {
		 	return error
	 } else {
		 f(bytes)
	 }
}
func main(){
	b64Str := "dGhpcyBpcyBhIGV4YW1wbGU="
	var res string
	if err := myb64Decode(func (bytes []byte){
			res = (string)bytes
		},b64Str) ;err != nil {
		fmt.Println("err!")
	}
}

看上去其实并没有太好。(但是用上了闭包就看起来厉害一点了啊!)至少不用处理我个人非常不爱的多值返回了。

改进那个闭包

再想想,其实这个回调本质只是想要一个从[]bytestring的setter函数。不如写成固定的实现好了。

package main
import "base64"
import "fmt"

func myb64Decode(f func([]bytes),b64Str string) error {
	 if bytes ,err := base64.StdEncoding.DecodeString(b64Str) ;err != nil {
		 	return error
	 } else {
		 f(bytes)
	 }
}

func setter(str *string) func([]byte) {
	return func(bytes []byte) {
		*str = string(bytes)
	}
}

func main(){
	b64Str := "dGhpcyBpcyBhIGV4YW1wbGU="
	var res string
	if err := myb64Decode(setter(&res),b64Str);err != nil {
		fmt.Println("err!")
	}
}

业务代码好像变得看起来好一点了!当然,其实setter是可以隐藏在函数内部直接复制的,让他看上去行为就是从一个base64的string,解码并复制到了另一个string的引用中去。但是我懒得打字了。

最终我的写法

其实写成上一节的话呢,已经可以达到目的了。但是我今天写的功能里面有一个特殊的地方,解码完成以后,是替换掉原先的变量的值的,也就是说,如果我写到上一节为止,我的代码会是这样的


myb64Decode(setter(&b64Str),b64Str)

作为一个重度懒癌,我拒绝同一个变量写两次!于是我又套了一层,写了一个不通用的实现。

package main
import "base64"
import "fmt"

func myb64Decode(f func([]bytes),b64Str string) error {
   if bytes ,err := base64.StdEncoding.DecodeString(b64Str) ;err != nil {
      return error
   } else {
     f(bytes)
   }
}

func setter(str *string) func([]byte) {
  return func(bytes []byte) {
    *str = string(bytes)
  }
}

func b64ToSelf(str * string) error {
  return myb64Decode(setter(str),*str)
}

func main(){
  b64Str := "dGhpcyBpcyBhIGV4YW1wbGU="
  if err := b64ToSelf(&b64Str);err != nil {
    fmt.Println("err!")
  }
}

结语

最终大概就是这样!最近新写的系统是rest api,api里解出来的参数基本都是base64编码过的,所以会频繁地操作解码,这样写大概会舒服一点!

本文的代码都是按照记忆重新写的,均没有经过测试!

结语的结语:为什么我不喜欢多值返回

首先,多值返回让原本很好用的golang类型推导运算符:=变得非常痛苦。

当我们写出


if x, err := function() ; err != nil {

}

就意味着,这里的x是一个作用域非常狭窄的变量,大多数时候我们都不希望函数返回值太过短暂;如果不是真的需要这样,可能需要另一个赋值语句把结果保存到生命周期更长的变量里,简直就是脱裤子放屁!

那么,我们就得写出

x, err := function()
if err != nil {

}

或者

var x interface{}
var err error
if x, err = function() ; err != nil {

}

这两种写法都恶心的地方在于,为了x ,我必须拥有一个生命周期一样长的error!所以多值返回的问题在于,多值返回的变量会享有同样的生命周期,我控制不了!虽然看上去没什么大不了,但是学c++出身的人!受!不!了!(教练!我想写rust!)

所以上文所说的,通过引用赋值规避多值返回,能控制住error类型变量的周期在很短的语句里!也不需要看到结果变量被赋值来赋值去了!