网易传媒于2020年底开始尝试Go语言的摸索,用于解决内存资源使用率偏高,编译速度慢等问题,本文将详细描述传媒在Go语言方面的所做的工作和获得的收益。

网易传媒于2020年将外围业务全副迁入容器,并将在线业务和离线业务混部,CPU利用率晋升到了50%以上,获得了较大的收益,但在线业务方面,接入容器后仍存在一些问题:

  • 在线业务内存使用量偏高:传媒次要开发语言是Java,应用SpringBoot框架,广泛内存使用量都在2G以上,和Go语言相比,占用内存资源很大。
  • 在线业务编译速度和启动速度偏慢,占用空间较大:因为应用Java,JVM在镜像实例都须要上百兆的空间,同时,SpringBoot在编译速度和启动速度和Go语言相比,都比较慢。

Go语言于2009年由Google推出,通过了10多年的倒退,目前曾经有很多互联网厂商都在踊跃推动Go语言利用,网易传媒于2020年底开始尝试Go语言的摸索,用于解决内存资源使用率偏高,编译速度慢等问题。本文将详细描述传媒在Go语言方面的所做的工作。

1 Go语言介绍

相比1995年公布的Java,Go语言是一个比拟年老的语言。年老带来了正反两方面的后果。从好的一方面来说,Go汲取了过来多种语言的长处,也没有C++这种悠久历史的语言向前兼容的枷锁;另一方面,Go因为呈现的工夫不算长,编译器、运行时、语法等都还在一直调整和优化,还未达到相似Java的成熟状态,而且开源类库也比不上诸如Python的老语言。

然而瑕不掩瑜,上面就来谈谈Go语言有哪些个性吸引咱们去应用。

编译速度快

从其它动态语言转到Go的开发者最先体验到的可能就是编译的速度。一般来说,Go的编译速度比Java和C++快5倍以上。很多C++大型项目可能须要编译10分钟以上,而雷同规模的Go我的项目很可能1分钟都不到。这个个性使代码编写者能够轻易用go run迅速编译测试,甚至间接开启IDE的主动后盾单测,在多人开发迭代时CI/CD的工夫基本上只够去一次厕所。

这种个性的次要起因官网文档里曾经提到了:Go编译模型让依赖剖析更简略,防止类C语言头文件和库的很多开销。不过这个也引入了一个解放——包之间无奈递归依赖,如果遇到相似的问题只能通过提取公共代码或者在内部初始化包等等形式来解决。

语法简略

Go语言起源于Google中一次C++新个性的分享会,一伙人(包含C语言创始人、UTF8发明人、V8 JS引擎开发者)感觉C++切实是太臃肿,索性发明一种语言来简化编程。因为Google外部员工次要应用类C语法的语言,所以Go也根本放弃了简直与C统一的语法,只有学过C就非常容易上手。另外因为Go在语法上汲取了各种语言多年的经验教训,各方面都有不少让人眼前一亮的小优化。

像动静语言一样开发

应用过动静语言的应该接触过上面这种Python代码:

def biu(toy):
    toy.roll()

o = new_ball()
roll(o)

roll函数能够传入任何类型的对象,这种动静语言特色使开发及其灵便不便。然而大家可能都据说过“动静一时爽,重构火葬场”的名言,相似的实现会给其它维护者造成微小的阻碍,如果不是这个起因Python3也就不会退出type hints的个性了。

那么有没有既能应用动静类型,又能限度传入的对象类型的形式呢?Go的interface就是用来解决这个问题的。interface相似一个强制性的泛化type hints,尽管它不强求特定的类型,但对象必须满足条件。上面看个简略的例子,首先申明了两种interface,并将它们组合成ReadWriteIF:

type ReadIF interface {
    Read()
}
type WriteIF interface {
    Write()
}
type ReadWriteIF interface {
    ReadIF
    WriteIF
}

接下来应用这个interface,留神只有一个对象的类型满足interface里的全副函数,就阐明匹配上了。

func rw(i ReadWriteIF) {
    i.Read()
    i.Write()
}
type File struct{}
func (*File) Read(){}
func (*File) Write(){}
rw(&File{})

能够看到rw函数基本没有固定传入参数的具体类型,只有对象满足ReadWriteIF即可。

如果心愿一个函数能像脚本语言一样承受任何类型的参数,你还能够应用interface{}作为参数类型,比方规范库的fmt.Print系列函数就是这样实现的。

资源耗费少

Go与C/C++耗费的CPU差距不大,但因为Go是垃圾回收型语言,消耗的内存会多一些。因为以后指标是应用Go取代Java,这里就将Go与同为垃圾回收型语言的Java简略比拟一下。

Java当年诞生时最大的卖点之一是“一次编写,到处运行”。这个个性在20年前很棒,因为市场上简直没有虚拟化解决方案。然而到了明天呈现了Docker之类一系列跨平台工具,这种卖点可能被看做一种短板,次要起因如下:

  • Java须要启动JVM过程来运行中间代码,程序须要预热
  • 堆内存较大时,垃圾回收器须要进行人工深刻调优,但在一些对实时性要求高的场景下,可能无解,Full GC一触发就是劫难
  • JDK体积宏大, Spring Boot jar包体积大,在微服务架构下问题最突出
  • Spring全家桶越来越重,导致应用全家桶的利用,性能较差

抛去JVM启动和预热工夫,运行一个最简略的HTTP程序,与Go比照,Java在CPU上的耗费多约20%,内存上的耗费约高两个数量级。

为并发IO而生

练习过开发网络库的读者可能都晓得Unix的epoll零碎调用,如果理解Windows应该据说过IOCP,这两种接口别离对应网络的Reactor和Proactor模式。简略来说前者是同步的事件驱动模型,后者是异步IO。不管你应用任何语言只有波及到高性能并发IO都逃不过这两种模式开发的折磨——除了Go。

为了展现应用Go开发并发IO有如许简略,我先从大家相熟的一般程序的线程模型讲起。下图是一个常见的程序线程图,一般来说一个服务过程蕴含main、日志、网络、其余内部依赖库线程,以及外围的服务解决(计算)线程,其中服务线程可能会按CPU核数配置开启多个。

服务启动后RPC申请到来,此申请的发动端可能是客户端或者另一个服务,那么它在服务线程解决过程中将阻塞并期待回复事件。留神这里的RPC蕴含狭义上的网络协议,比方HTTP、Redis、数据库读写操作都属于RPC。

此时的状况就如下图所示,服务调用端的申请要通过网络往返和服务计算的提早后能力取得后果,而且服务端很可能还须要持续调用其它服务。

大多数开发者都会想:反正调用个别也就几十毫秒嘛,最多到秒级,我开个线程去同步期待回复就行,这样开发最不便。于是状况就会变成下图这样,每个申请占用一个连贯和一个线程。如果网络和计算提早加大,要放弃服务器性能被充分利用,就须要开启更多的连贯和线程。

为了偷懒咱们偏向于防止应用Reactor和Proactor模式,甚至都懒得去理解它们,就算有人真的心愿优化并发IO,相似Jedis这种只反对同步IO的库也能阻止他。

当初有Go能援救咱们了,在Go里没有线程的概念,你只须要晓得应用go关键字就能创立一个相似线程的goroutine。Go提供了用同步的代码来写出异步接口的办法,也就是说咱们调用IO时间接像上图冀望的一样开发就行,Go在后盾会调用epoll之类的接口来实现事件或异步解决。这样就防止了把代码写得系统难懂。上面展现一个简略的RPC客户端例子,RPC调用和后续的计算解决代码能够顺畅地写在一起放入一个goroutine,而这段代码背地就是一个epoll实现的高性能并发IO解决:

func process(client *RPCClient) {
    response := client.Call() // 阻塞
    compute(response) // CPU密集型业务
}

func main() {
    client := NewRPCClient()
    for i := 0; i < 100; i++ {
        go process(client)
    }
    select {} //死等
}

服务器的代码更简略,不须要再去监听事件,当获取到一个IO对象时,只有应用go就能在后盾开启一个新的解决流程。

listener := Listen("127.0.0.1:8888")
for {
    conn := listenser.Accept() // 阻塞直至连贯到来
    go func() { // 对每个连贯启动一个goroutine做同步解决
        for {
            req := conn.Read()
            go func() { // 将耗时解决放入新的goroutine,不阻塞连贯的读取
                res := compute(req)
                conn.Write(res)
            }()
        }
    }()
}

留神go创立的goroutine相当于将IO读写和事件触发拼接起来的一个容器,耗费的内存十分小,所有goroutine被Go主动调度到无限个数的线程中,运行中切换根本是应用epoll的事件机制,因而这种协程机制能够很迅速启动成千上万个而不太耗费性能。

可运维性好

随着虚拟化技术倒退,相似JVM的服务成为了一种累赘;因为磁盘空间大小不再是问题,动静库带来的兼容问题也层出不穷,因而它也在缓缓淡出视线。

Go是一种适应分布式系统和云服务的语言,所以它间接将动态编译作为默认选项,也就是说编译之后只有将可执行文件扔到服务器上或者容器里就能没有任何提早地运行起来,不须要任何内部依赖库。

此外Go的我的项目只有在编译时批改参数,就能穿插编译出其余任意反对平台所需的二进制文件。比方我简直齐全在macOS上开发,当须要在linux服务器上测试则应用如下命令编译:

GOOS=linux GOARCH=amd64 go build ./...

Go反对android、darwin、freebsd、linux、windows等等多种零碎,包含386、amd64、arm等平台,绝大部分状况下你能够在本人的笔记本上调试任意零碎平台的程序。

与C/C++兼容

因为没有虚拟机机制,Go能够与C语言库比拟轻易地相互调用。上面是一个简略的例子,间接在Go中调用C语句:

/*
#include <stdio.h>
void myprint() {
        printf("hi~");
}
*/

import "C"
C.myprint()

如果应用Go编写一个接口,而后应用go build -buildmode=c-shared编译,这样就能失去一个动静库和一个.h头文件,怎么应用就无需再解释了吧。

对立而齐备的工具集

Go作为工程语言而设计,它的指标就是对立,即便一个团队有多种格调的开发者,他们的流程和产出最终都须要尽量保持一致,这样协同开发效率才会高。为了保障各方面的对立,Go提供了多种工具,装置当前执行go命令就能间接应用。

  • go run:间接运行go代码文件
  • go build:编译到本目录
  • go install:编译并装置到对立目录,比build快
  • go fmt:格式化代码,写完代码肯定要记得用
  • go get:下载并安装包和依赖库
  • go mod:包治理,1.11版退出
  • go test:运行单元测试
  • go doc:将代码正文输入成文档
  • go tool:实用工具集,包含动态谬误查看、测试覆盖率、性能剖析、生成汇编等等

2 Ngo框架介绍

背景

在传媒技术团队中推广Go语言,亟需一个Web框架提供给业务开发共事应用,内含业务开发罕用库,防止反复造轮子影响效率,并且须要无感知的主动监控数据上报,于是就孕育出Ngo框架。

选型

因为Go的开源Web框架没有相似Spring Boot大而全的,而最大的框架也是很受用户欢送的框架是Beego,为什么没有间接应用Beego呢?次要有以下几个起因:

  • HTTP Server的性能不现实
  • 不足大量业务所需库,比方kafka、redis、rpc等,如果在其根底上开发不如从零抉择更适宜的库
  • 大部分库无奈注入回调函数,也就难以减少无感的哨兵监控
  • 若干模块如ORM不够好用

指标

Ngo是一个相似Java Spring Boot的框架,全副应用Go语言开发,次要指标是:

  • 提供比原有Java框架更高的性能和更低的资源占用率
  • 尽量为业务开发者提供所需的全副工具库
  • 嵌入哨兵监控,主动上传监控数据
  • 主动加载配置和初始化程序环境,开发者能间接应用各种库
  • 与线上的健康检查、运维接口等运行环境匹配,无需用户手动开发配置

注:哨兵是网易杭研运维部开发的监控零碎,提供实时数据分析、丰盛的监控指标和直观的报表输入。

次要功能模块

Ngo防止反复造轮子,所有模块都是在多个开源库中比照并筛选其一,而后减少局部必须性能,使其与Java系接口更靠近。整个业务服务的架构如下图所示:

HTTP Server

高性能的gin实现了框架最重要的HTTP Server组件。用户在应用Ngo时无需关怀gin的配置和启动,只需注册http route和对应的回调函数。在gin之上Ngo还提供以下性能:

  • Url哨兵监控
  • 可跟踪的goroutine,避免goroutine泄露和不平安进行
  • 服务健康检查的全副接口,包含优雅停机
  • 用户回调函数的panic解决和上报

上面是一个简略的main函数实例,几行代码就能实现一个高性能http server。

func main() {
    s := server.Init()
    s.AddRoute(server.GET, "/hello", func(ctx *gin.Context) {
        ctx.JSON(protocol.JsonBody("hello"))
    })
    s.Start()
}

优雅停机

服务健康检查接口包含4个/health下的对外HTTP接口:

  • online:流量灰度中容器上线时调用,容许服务开始承受申请
  • offline:流量灰度中容器下线时调用,敞开服务,进行过程内所有后盾业务
  • check:提供k8s liveness探针,展现以后过程存活状态
  • status:提供k8s readiness探针,表明以后服务状态,是否能提供服务

offline接口实现了优雅停机性能,能够让过程在不进行的状况下进行服务,不影响已收到且正在解决的申请,直至最初申请处理完毕再停机。当平台告诉服务须要进行服务时,优雅停机性能会进行本过程正在运行的全副后盾业务,当所有工作都进行后,offline接口的返回值会通知平台已筹备好下线,此时才容许停机。如果服务呈现某些故障,导致解决申请的工作阻塞,此性能会在一段时间内尝试进行,如果超时才会强制敞开。

MySQL ORM

应用gorm实现MySQL ORM的性能,并在之上提供以下性能:

  • 主动读取配置并初始化MySQL ORM客户端,配置中能够蕴含多个客户端
  • mysqlCollector哨兵监控

日志

应用logrus实现日志接口,并提供以下性能:

  • 对立简洁的定制化格局输入,蕴含日志的工夫、级别、代码行数、函数、日志体
  • 可选按txt或json格局输入日志
  • access、info、error日志拆散到不同文件中
  • 提供文件轮转性能,在日志文件达到指定大小或寿命后切换到新文件

服务默认输入txt的日志格局,款式如:工夫 [级别] [代码目录/代码文件:行数] [函数名] [字段键值对] 日志体。

工夫格局相似2021-01-14 10:39:33.349。

级别蕴含以下几种:

  • panic
  • fatal
  • error
  • warning
  • info
  • debug

如果未设置级别,被被默认设置为info。非测试状态不要开启debug,防止日志过多影响性能。

另外在日志输入时能够应用WithField或WithFields来字段的key-value,在创立子日志对象时能够用来清晰地识别日志的应用范畴,但平时尽量不要应用。另外如果要输入error也尽量避免应用字段,间接应用Error()办法输入为字符串是最快的。

Redis

Redis客户端抉择go-redis实现。同样只需在配置中提供Redis服务配置,即可在运行中间接应用GetClient获取指定名字的客户端。其反对client、cluster、sentinel三种模式的Redis连贯,且都能主动上报哨兵监控数据。

Kafka

Kafka客户端在sarama根底上实现,因为原始接口比较复杂,业务需要个别用不上,Ngo中对其进行了较多的封装。在配置文件中减少kafka段,Ngo即会主动按配置生成生产者和消费者。

生产者只需调用func (p *Producer) Send(message string)传入字符串即可上报数据,无需关怀后果。此接口是异步操作,会立刻返回。如果出错,后盾会重试屡次,并将最初的后果记录上传到哨兵监控。

Kafka消费者只需这样调用Start注册处理函数即可工作:

consumer.Start(func(message *sarama.ConsumerMessage) {
    // 生产代码
})

HTTP Client

HTTP Client应用fasthttp实现,提供相当卓越的性能。思考到fasthttp提供的接口非常简单,用户必须本人格式化申请和回复的header和body,因而在其根底上做了大量开发,减少诸如Get(“xxx”).SetHead(h).SetBody(b).BindInt(i).Timeout(t).Do()的Java链式调用,包含:

  • 设置url query
  • 设置申请body,body的格局反对任意对象json序列化、[]byte、x-www-form-urlencoded的key-value模式
  • 解析回复的header
  • 解析回复的body,body格局反对任意对象json序列化、int、string、float、[]byte
  • 申请超时设置
  • service mesh的降级回调

RPC

因为gRPC的应用比较复杂,而且性能与Go规范库的RPC差距不大, 因而以后RPC库在Go规范库的根底上开发,并在之上减少连接池、连贯复用、错误处理、断开重连、多host反对等性能。在应用上接口与规范库基本一致,因而没有学习老本。

至于应用RPC而不只限度于HTTP的次要起因,一是基于TCP的RPC运行多申请复用连贯,而HTTP须要独占连贯;二是HTTP在TCP之上实现,header占据了大量overhead,特地在小申请中是不必要的开销。在Ngo的两个库下自带性能测试,运行go test -bench .就能查看后果,两者都应用20*CPU的并发量,模仿1ms、5ms、50ms的服务器网络和计算提早,具体后果如下:

  • 1连贯场景RPC性能是HTTP的100倍左右
  • 5连贯场景RPC 性能是HTTP的40-70倍
  • RPC的5连贯是HTTP的100连贯性能的3-4倍

配置

配置模块应用viper实现,但用户无需调用配置模块的接口,在每个模块如Redis、Kafka、日志中都会被Ngo主动注入配置,用户只需写好yaml文件即可。

服务须要提供-c conf参数来指定配置文件,启动时,会顺次加载以下配置:

  • 服务属性
  • 日志
  • 哨兵nss
  • 哨兵收集器
  • Redis
  • MySQL
  • Kafka
  • HTTP Client

配置文件范例如下:

service:
  serviceName: service1
  appName: testapp
  clusterName: cluster1
nss:
  sentryUrl: http://www.abc.com
httpServer:
  port: 8080
  mode: debug
log:
  path: ./log
  level: info
  errorPath: ./error
db:
  - name: test
    url: root:@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local
httpClient:
  maxConnsPerHost: 41
  maxIdleConnDuration: 50s
redis:
  - name: client1
    connType: client
    addr: 1.1.1.1
kafka:
  - name: k1
    type: consumer
    addr:
      - 10.1.1.1:123
      - 10.1.1.2:123
    topic:
      - test1
      - test2
  - name: k2
    type: producer
    addr: 10.1.2.1:123
    topic: test

哨兵

哨兵模块的目标是提供对立且易扩大的接口,适配哨兵数据的收集形式,将各类数据上报到哨兵服务器。它蕴含两局部:数据收集和数据发送。

数据发送局部在程序启动时会加载以后服务的配置,设定好上报格局,当有收集器上报数据时会调用其接口生成固定的json格局,并应用HTTP Client库上报。

数据收集局部是一个可扩大的库,能够用其创立自定义的收集器,并指定metric和上报距离,在Redis、Kafka、HTTP Client等库中都曾经内置了收集器。一般来说一个收集器的解决行为只须要一种类型的数据来触发,在后盾生成多种数据项。比方HTTP Client是每次都传入单次调用的记录,在收集器后盾解决时生成对一分钟内所有调用的全汇总、url汇总、host汇总、状态码汇总等类型的数据项。

用户能够用以下实现来创立一个一分钟上报周期的收集器,至于RawData如何去更新ItemData须要用户本人实现。

collector = metrics.NewCollector(&metrics.MetricOptions{
    Name:     metricName,
    Interval: time.Minute,
})
collector.Register(itemTypeInvocation, &RawData{}, &ItemData1{})
collector.Register(itemTypeHostInvocation, &RawData{}, &ItemData2{})
collector.Start()

后续用户只需调用collector.Push(rawData)就能将数据发送到收集器。数据处理在后盾执行,整个收集器解决都是无锁的,不会阻塞用户的调用。

当初Ngo中已内置以下哨兵监控metric:

  • httpClient4
  • Url
  • redis
  • Exception
  • mysqlCollector
  • kafkaBase

3 性能压测及线上体现

技术的转型,势必会带来性能体现的差别,这也是咱们为什么破费精力来探索的第一因。当初咱们将从以下几个维度来比照一下转型为Go之后的所带来的长处和毛病

压测比拟

压测最能体现进去在零碎的极限的具体表现。因为语言自身的实现机制不同,Java因为Jvm的存在,因而两者的启动资源最小的阈值自身就不一样。咱们压测的业务逻辑绝对简略一些,业务中首先读取缓存数据,而后再做一次http调用,并将调用后果返回到端上。Java我的项目和Go我的项目框架外部集成了哨兵监控,都会将零碎体现数据实时上报。咱们参考的数据根据也是来自于此。

第一轮压测指标:

  • 100并发
  • 10分钟

集群配置:

首先咱们先看一下整体的不同我的项目的集群整体体现

Java 集群

Go集群

TPS-RT曲线

Java集群

Go集群

因为咱们加压的过程是间接进入峰值,启动时候的体现,从TPS指标和MaxRT指标,显示Java集群有一个冷启动的过程,而Go集群没有这么一个过程。两者在经验过冷启动之后,性能体现都很稳固。

申请曲线

Java集群

Go集群

这里有一个很有意思的景象,那就是尽管Go有更大吞吐量,然而网络的建设工夫并不是很稳固,而Java启动之后,则显著处于一个稳固的状态。

机器性能指标

cpu-memory

Java 集群

Go集群

从以后的压测后果和机器性能指标来看,Go集群有更好的并发申请解决能力,申请吞吐量更大,并且在机器资源占用上有更好的劣势。应用更少的内存,做了更多的事件。

第二轮压测指标:

  • 200并发
  • 10分钟

集群配置:

首先咱们先看一下整体的不同我的项目的集群整体体现

Java 集群

Go集群

TPS-RT曲线

Java集群

Go集群

各项指标曲线和100并发状态类似,除了TPS曲线。Java 在200并发下冷起的过程变得更长了。但最终都还是趋于稳定的状态。

申请曲线

Java集群

Go集群

此时反而发现Go集群增压的状况下抖动较上次没有什么变动,反而Java集群的建设连接时间抖动变大了。

机器性能指标
cpu-memory

Java 集群

Go集群

机器资源曲线没有太大的变动。

总结:

100并发

200并发

从两次后果压测后果来看的话,Go在集群中的体现是要优于Java的。Go领有更好的并发解决能力,应用更少的机器资源。而且不存在冷启动的过程。随着压力的减少,尽管吞吐量没有下来,然而Go集群的RT90和RT99变动不是很大,然而雷同分位Java集群的体现则扩充了一倍。而且在100并发状况下,MaxRT指标Java集群和Go集群相差无几,而在200并发状况下,RT99指标Java集群则变成了Go集群的2倍。并且在200并发的状况下,Java集群的TPS有显著的降落。而且TPS的指标的曲线Java的回升曲线过程被拉的更长了。其实换一个角度来看的话,在流量激增的状况下,Java集群的反馈反而没有Go稳固。

Go集群线上接口体现

目前咱们一共革新了三个接口,业务的复杂度逐步晋升。

第一个接口是hotTag接口,该业务次要是获取文章详情页下边的热门标签。编码逻辑绝对简略,服务调用只是波及到了redis缓存的读取。目前的曾经全量上线状态。

第二个接口是获取文章的相干举荐。编码逻辑中会通过http对举荐零碎接口做申请,而后将数据缓存,优先获取缓存中的数据。目前全量上线。

第三个接口次要是获取网易号相干的tab标签。编码逻辑中会通过网易号在数据库中读取网易号的配置数据,而后做缓存,下次申请优先应用缓存。而且还须要通过http来调用大象零碎,获取与该网易号相干的tab标签,而后将数据整合后返回到端上。

hotTag接口体现

机器资源状态

举荐接口体现

机器资源状态

论断:
就目前的线上集群的状态来看的话,集群的运行状态比较稳定,而且服务的解决能力是极为高效的。当然了,目前的线上状态Go我的项目接口繁多,整个集群就只有这一个接口提供服务。Java集群因为业务关系,提供的服务接口更多,而且性能体现可能会因为零碎IO或者网络带宽问题,导致了性能的看上去没有那么丑陋,更精确的论断会在Java集群中的所有接口全副迁徙到Go集群中的时候的数据体现更具备说服力。

4 重构实际与问题

Go 协程与 Java的线程

Go为了更加正当调配计算机的算力,应用更为轻量级的协程代替线程。协程和线程之间的运行原理大家能够参考文章前边对于协程的解说,或者自行百度。此处只解说在写利用的过程中,咱们在代码级别能失去什么样的益处。

talk is cheap, show my the code!

Go 应用协程

// GoN 在后盾应用goroutine启动多个函数,并期待全副返回
func GoN(functions ...func()) {
    if len(functions) == 0 {
        return
    }

    var wg sync.WaitGroup
    for _, function := range functions {
        wg.Add(1)
        go func(f func()) {
            defer wg.Done()
            f()
        }(function)
    }
    wg.Wait()
}

// 应用协程来执行
util.GoN(
    func() {
        topicInfo = GetTopicInfoCachable(tid)
    },
)

Java 应用线程

//当然了,咱们晓得很多种java的线程实现形式,咱们就实现其中的一种
// 定义 性能类
private CompletableFuture<TopicInfo> getTopicInfoFuture(String tid) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            return articleProviderService.getTopicInfo(tid);
        } catch (Exception e) {
            log.error("SubscribeShortnewsServiceImpl.getTopicInfoFuture tid: {}", tid, e);
        }
        return null;
    }, executor);
}

// 线程应用
CompletableFuture<TopicInfo> topicInfoFuture = getTopicInfoFuture(tid);
TopicInfo topicInfo = null;
try {
    topicInfo = topicInfoFuture.get(2, TimeUnit.SECONDS);
} catch (Exception e) {
    log.error("[SubscribeShortnewsServiceImpl] getSimpleSubscribeTopicHead future error, tid = " + tid, e);
}

总结:

从上述的代码实现中,咱们能够看进去Java代码的书写过程略显冗余,而且被线程执行的过程是须要被实现为特定的类,须要被继承笼罩或者重写的形式来执行线程。想要复用曾经存在性能函数会费些周折。然而Go在语法级别反对了协程的实现,能够对曾经实现性能做到拿来即可应用,哪怕没有对这个性能做封装。

我集体了解是因为语言的实现理念导致了这种书写形式上的差别。自身Go就是类C语言,它是面向过程的编程形式,而Java又是面向对象编程的优良代表。因而在不同的设计理念下,面向过程思考更多的是性能调用,而面向对象须要设计性能自身的形象模型,而后再实现性能。思考的多必然导致编码的冗余,然而这样的形式的益处是更容易形容整个利用的状态和性能。如果了解的不正确,心愿大家指出。

革新过程中遇到的问题

在将Java我的项目中迁徙到Go的过程中也会遇到各种各样的问题,书写上的习惯,功能设计上的差别等等。我把它分为了以下几个方面:

1.万物皆指针到值和指针的管制

提到值传递和指针传递,是不是让你想起了写C或者C plus的青葱岁月。Java中只有根本类型是值传递之外(不蕴含根本类型的封装类)其余的都是援用传递,援用换句话说就是指针。传递指针的一个益处是,传递的是一个内存地址,因而在程序赋值的时候,只须要将内存地址复制一下即可,具体地址指向的内容的大小和内容是什么,基本不必关怀,只有在应用的时候再关怀即可。能够说Java自身就屏蔽了这么一个可能呈现大量复制的操作。然而Go并没有给你屏蔽这种操作,这个时候你本人就须要依据本人的利用场景抉择到底是抉择传递值还是援用。

// People 咱们定义一个车的根本信息,用来比拟车与车之间的性价比
type Car struct {
    Name         string
    Price        float32
    TopSpeed     float32
    Acceleration float32
}
// CompareVa 值传递,此时会存在Car所有的数据复制,低效
func CompareVa(a Car, b Car){
    // TODO ... compare
}

// ComparePtr 指针传递,只是复制了地址,内容不会复制,高效
func ComparePtr(a *Car, b *Car){
    // TODO ... compare
}

2.精简的语法导致的不留神引起的局部变量的创立

var dbCollector     metrics.CollectorInterface // 咱们定义了一个全局变量,数据上传的hook


// 用于初始化咱们的定义的db打点收集器
func initMetrics() {
    dbCollector := metrics.NewCollector(&metrics.MetricOptions{
        Name:     metrics.MetricTypeMyql,
        Interval: time.Minute,
    })
    dbCollector.Register(itemTypeConnection, &rawOperation{}, &itemConnection{})
     ...

    dbCollector.Start()
}
initMetrics()

3.了解nil 和 null 和空

nil只是Go语言中指针的空地址,变量没有被调配空间
null只是Java语言中援用的空地址,变量没有被调配空间
空就是调配了内存,然而没有任何内容

4.对于string

String str; // 定义了一个java变量,初始化为null
str string  // 定义了一个go变量, 初始化为空字符串,留神这里不是nil

5.没有包装类

咱们常常会在Java工程当中写这样的代码

class Model {
    public Integer minLspri;
    public Integer maxLspri;
    ...
}

public Map<String, String> generateParam(Model param) {
    Map<String, String> params = Maps.newHashMap();
    if( param.minLspri != null ){
        params.put("minLspri", param.minLspr.toString())
    }
    if( param.minLspri != null ){
        params.put("maxLspri", param.maxLspri.toString())
    }
    ...
}

那咱们在革新为Go的时候要不要间接转化为这样

type Model struct {
    minLspri *int
    maxLspri *int
    ...
}
...

遇到这种问题怎么办?我的倡议是咱们还是间接定义为

type Model struct {
    minLspri int
    maxLspri int
    ...
}

咱们还是要像Go一样去写Go,而不是Java滋味的Go我的项目。而呈现这个问题的起因我也想了一下,其实就是在java我的项目当中,咱们习惯的会把null作为一个值来了解,其实null是一种状态,而不是值。它只是通知你变量的状态是还没有被分配内存,而不是变量是null。所以在革新这种我的项目的过程中,还是要把每个字段的默认值和有效值理解分明,而后做判断即可。

6.数据库NULL字段的解决

这个其实也是因为上一条起因导致的,那就是Go中没有包装器类型,但好在sql包中提供了 sql.NullString 这样的封装器类型,让咱们更好的判断到底数据库中寄存的是一个特定的值还是保留为null

7.redis 相干的sdk原生的解决形式的不同

Java和Go在解决key不存在的时候形式不一样。Java中Key不存在就是返回一个空字符串,然而Go中如果Key不存在的话,返回的其实是一个error。因而咱们在Go中肯定要把其余的谬误和key不存在的error辨别开。

8.异样的解决和err解决

Java中的Exception记录了太多的货色,蕴含了你的异样的调用链路和打印的日志信息,在任何catch住的异样那里都很不便的把异样链路打印进去。而Go中解决形式更简洁,其实只是记录了你的异样信息,如果你要看异样堆栈须要你的非凡解决。这就须要你在任何呈现error的中央及时的打印日志和作出解决,而不是像Java一样,我在最外层catch一下,而后解决一样也能够很洒脱了。孰是孰非,只能在一直的学习和了解当中来给出答案了。


接下来咱们会在Ngo上持续减少流量跟踪标识、全链路数据上报等个性,并欠缺监控指标,陆续推动更多Java语言业务转入Go语言。
Ngo GitHub地址