闲话Golang

发布于 作者 shisongran一条评论

大概3个月前,离开了狼厂,跟随老大的步伐来到了某个网约车平台。新的公司主流开发语言是Golang,笔者在经历了短暂的学习和痛苦的踩坑后,基本上能用Golang完成日常工作了。最近快过年,公司封网后没有什么特别的事情,就乘着空闲写篇文章来说说Golang。

0.火不起来的Golang

记得在知乎上有看到某个提问,大意是说Golang这样好用的语言,为什么这么多年都没有火起来(问题链接)。摘取回答中的一项数据,Github的语言用户数量统计上看到,Golang用户5年时间达到了2.53%,对比Swift在2年之内达到1.34%来说(golang发布两年时用户占比为0.8%),速度的确是慢了些。

个人认为Golang特殊的语言定位,阻碍了Golang“火起来”:golang是为了取代C/C++,或者Java这样的语言而生的,目标是高并发的服务器后端语言。,在生产环境中,后端开发已经有了PHP,Nginx + Lua,Java,C++,Node.Js等等一系列较为成熟解决语言,工具链也很成熟。在成熟后端的系统中更换语言成本很高,大家宁可将就着原来的语言去维护,也不愿意使用新语言去重写这个服务。而切换到Golang这门语言,也是需要学习成本的。另外一方面,Golang也没有类似Apple提供Swift的支持,缺乏个人开发者可以直接变现的渠道,也会让初学者却步。

语言火起来,意味着能够有更多的人加入改进语言环境的工作中来,有更成熟的工具链,更活跃的社区,问题也能够及时被解决。Golang虽然没有达到Java这样火,但其本身在后端开发的优势,以及Google内部也在使用这门语言进行开发,实际上很多问题会被及时的处理,就目前来说,工具链已经比较成熟。前不久JetBrains也退出了自己的预览版的Golang IDE(Gogland),也从侧面证明Golang在逐渐被大家使用在生产环境中,是值得去学习的后端开发工具。

下面说说Golang语言中,我感触较深的几个优点和坑。

1. 方便的并发

Golang语言定位在后端开发上,目标是取代C/C++这样的语言,完成服务器后端搞并发的开发任务。在日常的开发任务中,我使用到了其中一些特性,的确很大程度上方便了开发。

go

Golang独特的关键词go,在并发编程中十分便利,极大的减少了开发并发编程的难度。看看下面的例子:

Go
// 遇到错误
func (f *Fuse) Add() int64 {
    if false == FuseGlobalSwitchOn {
        return 0
    }

    nowCounter := atomic.AddInt64(&f.counter, 1)
    if 1 == nowCounter {
        // 一定是见延迟后,清零刷新定时器
        go f.DelayRefreshCounter()
    }

    return nowCounter
}

一个简单的计数器,在计数器值为1的时候,我们启动了一个协程用来在若干秒后清零计数。这样简单有效的并发编程方式,能够让我们更专注业务逻辑本身。协程的存在,可以让我们在业务中,随意的开启异步任务,而不用担心以往新建线程所带来的开销。这在传统的Http业务后端使用起来非常方便,我们不用再依赖于异步框架来实现,让我们编程的思维更加连贯。

channel

Golang关键词Chan实际上一种自带锁的管道,使用起来相比C++的传统管道,方便不少。于是很多时候我们都会尝试使用chan来实现一个简单的消费者和生产者模式产品。下面这个函数使用了chan来实现一个简单的日志模块:

Go
// 写日志函数
func (l Logger) writeLog(logLevel uint64, logString string){
    for _, logHandle := range l.logHandleList {
        if (logHandle.flag & logLevel) != 0 {
            logHandle.logBuffer <- logString
        }
    }
}

// 日志输出及日志切分
func (logHandle *LogHandle) run(){
    LOG_LOOP:
    for {
        select {
        case logString := <-  logHandle.logBuffer:

            logHandle.file.Write([]byte(logString))

        case controlFlag := <- logHandle.logControl:

            switch controlFlag {

            case LOG_CONTROL_STOP:
                logHandle.isRunning = false
                break LOG_LOOP

            case LOG_CONTROL_ROTATE:
                logHandle.doRotate()
            }
        }
    }

    logHandle.waitGroup.Done()

}

比较简单的逻辑,实现了控制日志切分,输出停止,以及并发的日志写入。这些代码相比C++来说,开发量少了很多。

2.不支持重写(overwrite)的面向对象

刚接触golang的时候,偶尔会有这样的误解:

Go
package main

import "fmt"

type EchoInterface interface {
    Echo()
}

type EchoBase struct {
}

// 基类实现了通用的功能框架
func (e EchoBase) Echo(){
    e.doInit()

    e.doEcho()
    
    e.dotail()
}

func (e EchoBase) doInit(){
    fmt.Println("init echo framework")
}

func (e EchoBase) dotail(){
    fmt.Println("release some resource")
}

// 希望子类重写这个函数,以实现不同的功能
func (e EchoBase) doEcho(){
    fmt.Println("please implement this function to do someting")
}


type EchoHello struct {
    EchoBase
}

// 子类重写函数,但是无效
func (e EchoHello) doEcho(){
    fmt.Println("hello")
}


func main() {
    var echoInterface EchoInterface = EchoHello{}
    echoInterface.Echo()
}

上述代码的输出结果为:

init echo framework
please implement this function to do someting
release some resource

EchoHello虽然拥有了EchoBase的所有接口和实现,但是并不能通过重写其中某个函数,这可以理解为为EchoHello拥有EchoBase的匿名成员变量,即使子类重新实现了doEcho,也不会影响到EchoBase本身。以前使用C++类型面向对象的同学可能要花一点时间去习惯。

3. 好用但有坑的包管理

Golang的依赖可以很方便的使用go get命令来获取,一开始接触golang的同学,会感觉命令非常便利。几乎想要的任何组件,都可以快速使用这个命令获取到,利用一些支持go get的IDE,甚至可以做到在写代码的时候,自动导入需要的开源库,开发变得很简单。

理想总是完美的,现实却又很骨感。

我们不可能希望第三方库不更新,保持所有的接口实现不变,甚至有时候我们期待它们的更新;在某些项目中,我们可能会使用到同一个库的多个版本,同时使用多个同名库是可能的。在这些时候,golang的包组织的形式,将会带给我们一些麻烦。golang已经尝试引入了vendor文件夹来尝试满足这些要求,然而这样的组织方式,实际上已经放弃了go get这样的命令了。另外在多个项目并发开发的时候,我们不得不为了管理依赖版本,手动管理依赖库,或者变更GOPATH等环境变量。又重新回到了那个手动管理依赖的时代。

遇到这些情况,就很难优雅起来了。

4. TLV的悖论

Golang不鼓励开发者使用类似TLV(Thread Local Variable)的GLV(Goroutine Local Variable),理由对很难进行GC。甚至因为不想让大家使用GLV,而隐藏了Goroutine id。

然而并发编程中,我们往往期待从日志信息中知道,某个的协程的处理流程,而不是混合在一起的顺序混乱的日志。在以前的C++ 或者 Java中,我们尝试利用线程ID,或者线程的名字来区分日志,但这样的方法无法平移到Golang上来,因为Golang不支持Goroutine id。那总不能让日志混合在一起吧?于是我们不得不设计一个GLV的数值,随着Goroutine一起诞生,一起消亡。

网络上流传这一些方式能够获得Goroutine id,思路分为两种:一种是使用runtime包拿到堆栈,然后正则匹配出goroutine;另一种是直接修改golang的库代码,暴露出Goroutine id。个人感觉,如果仅是为了打印日志,为何要纠结与原生的ID,自己创造一个不就好了么。虽然这样创造的ID也被官方反对——highly discouraged。Who care?程序能够Work,性能还过得去,不就可以了么。

5. 体贴入微带来的困扰

Golang有很多细节做的非常细致,细致到很多时候会给业务开发带来一些困扰。

比如时间解析,从文字解析到标准的time.Time时,会根据时区进行对应夏令时的转化(如果是中国时间,86年到92年请务必注意夏令时问题)。结果是,我们花费了大概一个工作日去追查——我们的确没有想到Golang会自动处理夏令时——位于这段时间的生日展示时间全部提前了1个小时的问题。

再比如,Golang会在HTTP库中帮我们自动化大写Http Header的Key的每个单词首字母同时小写其他字母,这导致请求都丢失了Http头大小写信息——的确,我们不应该纠结于此——可是这导致请求写入的Http Head和最后服务端拿到的不一致。

如果你阅读到了这儿,那么在后续的开发中,可能要注意下这两点,不要再重蹈我们的覆辙了。


大抵如此,Golang作为服务器开发语言,的确是非常优秀的,随着一些大公司开始使用Golang作为后端开发语言,这门“火不起来”的语言,也会逐渐进入大多数开发者的视野吧。作为以前开发Android,被Java注重“面向对象”形式大于开发效率恶心到的开发者,内心还是期待Golang有一天能够代替Java成为Android的开发语言的,希望能够有机会看到Google这么做的一天。

如此。

1 则回应给 闲话Golang

  1. golang擅长处理的是互联网业务逻辑
    这方面需求不多

    加之写go得会fq,得会充分利用开源资源
    要知道中国很多做开发的连系统出了点小故障都不会处理

发表评论

电子邮件地址不会被公开。 必填项已用*标注