每日新闻

每日新闻

GoCN每日新闻资讯
有问必答

有问必答

Go相关的问题,技术相关的问题
文章分享

文章分享

技术文章分享,让知识传播给更多的人
招聘应聘

招聘应聘

为Gopher服务的招聘应聘平台

GoCN每日新闻(2019-11-05)

回复

每日新闻EasyHacking 发起了问题 • 1 人关注 • 0 个回复 • 10989 次浏览 • 2019-11-05 15:56 • 来自相关话题

grom sqlserver 批量插入问题

回复

有问必答ming123 发起了问题 • 1 人关注 • 0 个回复 • 113 次浏览 • 2019-11-05 11:49 • 来自相关话题

【新加坡】Shopee: Golang服务端或者平台开发内推

招聘应聘nerdcoder 发表了文章 • 0 个评论 • 397 次浏览 • 2019-11-05 10:18 • 来自相关话题

关于Shopee: * Shopee是做东南亚一块的电商平台,比如网站有: shopee.sg, shopee.tw等工作职责 ...查看全部
  • 关于Shopee:

  •  * Shopee是做东南亚一块的电商平台,比如网站有: shopee.sg, shopee.tw等

  • 工作职责

  • * 用Golang做服务端开发,主要是微服务,或者平台开发,就是Infrastructure工程师

  • 工作地点,薪资环境和压力 

  • * 新加坡,薪资在5000新币及以上。不用996,每天10点来晚19点就可以回去。零食水果咖啡都有。

**有意向的简历发我邮箱: nerdcoder11@gmail.com, 抱歉为了保护隐私,就不用公司邮箱了。

gochat - 纯go实现的im即时通讯系统(支持水平扩展)

开源程序gochat 发表了文章 • 0 个评论 • 482 次浏览 • 2019-11-04 15:56 • 来自相关话题

项目推荐- 项目地址:https://github.com/LockG ...查看全部

项目推荐
- 项目地址:
https://github.com/LockGit/gochat

- 类别:Go

- 项目描述:

gochat为纯go实现的即时通讯系统,支持私信消息与房间广播消息,各层之间通过rpc通讯,支持水平扩展。
使用redis作为消息存储与投递的载体,相对kafka操作起来更加方便快捷,所以十分轻量。
各层之间基于etcd服务发现,在扩容部署时将会方便很多。
由于go的交叉编译特性,编译后可以快速在各个平台上运行,gochat架构及目录结构清晰,
并且本项目还贴心的提供了docker一键构建所有环境依赖,安装起来十分便捷。

- 推荐理由:
轻量快捷不臃肿,水平可扩展,docker快速构建所有环境,迅速体验im即时通讯,
各层架构清晰,文档说明详细。

系统架构:



服务发现:


消息投递


聊天室预览:

GoCN每日新闻(2019-11-04)

回复

每日新闻smallfish1 发起了问题 • 1 人关注 • 0 个回复 • 12058 次浏览 • 2019-11-04 11:48 • 来自相关话题

内推Golang/Python程序员

招聘应聘nerdcoder 回复了问题 • 4 人关注 • 2 个回复 • 3053 次浏览 • 2019-11-03 15:26 • 来自相关话题

GoCN每日新闻(2019-11-03)

回复

每日新闻DennisMao 发起了问题 • 1 人关注 • 0 个回复 • 12575 次浏览 • 2019-11-03 14:27 • 来自相关话题

GoCN每日新闻(2019-11-02)

回复

每日新闻mahuaibo 发起了问题 • 1 人关注 • 0 个回复 • 12657 次浏览 • 2019-11-02 22:44 • 来自相关话题

各地/互联网公司招聘Go

招聘应聘mikerr 回复了问题 • 20 人关注 • 20 个回复 • 6954 次浏览 • 2019-11-01 22:59 • 来自相关话题

三分钟让你理解什么是敏捷开发,这才是敏捷开发......

每日新闻CORNERSTONE 发表了文章 • 0 个评论 • 526 次浏览 • 2019-11-01 17:48 • 来自相关话题

做为无所不能的产品经理,虽不是上知天文下知地理,但是也要对产品相关的知识领域有所涉猎。项目管理就是与产品密切相关的一个知识领域,同时也是产品经理日常工作中经常要负责的一部分内容。别问我为什么不是项目经理负责,因为很多公司没有…… ...查看全部

做为无所不能的产品经理,虽不是上知天文下知地理,但是也要对产品相关的知识领域有所涉猎。项目管理就是与产品密切相关的一个知识领域,同时也是产品经理日常工作中经常要负责的一部分内容。别问我为什么不是项目经理负责,因为很多公司没有……


本文结合实际工作实践以及亲身使用CORNERSTONE项目管理工具经验,深入浅出介绍在敏捷开发的互联网公司中一个项目从无到有所经历的各个环节,当然项目管理这门学问还有很多需要深入探索的领域,以下仅仅与各位产品/项目经理们,学习交流一下。


一、做产品还是做项目?


产品就是项目,项目就是产品。


在很多敏捷开发的互联网公司中,产品是项目制,功能也是项目制,在策划一个新功能的时候,对于产品经理来说就是在策划一个项目。


在PMBOK第六版的官方指南中,项目指为创造独特的产品、服务或成果而进行的临时性工作,项目有固定的起止时间周期。


在大一点的互联网公司,多个产品经理为同一个产品服务,每一个产品都会绞尽脑汁想一个idea去实现产品、用户的价值提升。


每一个idea就是一个项目,当项目经历了立项、启动、执行、上线、收尾,这个项目就变成了产品的一个功能或一个服务。


二、项目的生命周期


CORNERSTONE | DevOps全流程解决方案


一个产品从无到有,从生到死会经历多个需求、交互、设计、计划、开发、提测、上线、hotfix、解决线上问题、运维、运营的生命周期闭环。


而在产品生命周期当中也会包含多个项目的生命周期,每一个项目都会经历项目启动、项目执行、项目监控、项目收尾(PMP中项目的五大过程组在这里被缩减成为了四个,其中项目启动包含了项目启动和项目规划)。


1. 项目启动


1.1 需求收集


image.png


CORNERSTONE为需求生命周期搭建流程,可以自定义更改按收集、评审、排期、设计、开发、发布设立多个阶段,在不同阶段把任务分发给产品、设计或者开发人员,让需求完成无缝衔接。这个阶段其实是产品经理最擅长的领域,即为什么要做这个项目?


在这里可以参考精益创业画布中的9个要素去回答:

  1. 服务的用户群体是谁?

  2. 解决的问题是什么?

  3. 解决方案是什么?

  4. 你的优势是什么?

  5. 衡量效果的关键指标是什么?

  6. 与竞品相比,你的门槛优势是什么?

  7. 项目成本有多少(人力成本、时间成本)?

  8. 项目收益有多少(收入、用户感知)?

回答好上面的9个问题后,就可以拿着你的idea去和其他产品pk了,能不能获得老板们的资源倾斜成功立项,就看你的项目能不能真的打动老板。


在这之中,对于老板来说,往往更关心项目成本和收益:即用最少的人力、时间成本,得到更大的收入、用户价值提升。


在这个阶段,对于负责项目的产品经理来说,需要输出的是需求文档及原型,这是你用来打动老板的基础,也是需要与涉及项目团队成员沟通需求的基础。


1.2 项目启动会


74f0d1eeb4b3435b95a1305cff4b03ab.png


在立项会上顺利从老板那里获得资源后,项目可以真正开始启动了,这时就需要召开一个项目启动会,将项目涉及的各个团队召集到一起,给大家讲一个充满想象力的美好故事,让大家为了这个目标而努力。


好吧扯淡完了,具体需要做哪些呢:


1. 明确项目要做什么,其实在这个环节,就是给各团队的同学讲为什么要做这个项目,这个项目能解决什么问题,带来什么样的收益,用项目价值去打动各团队一起努力比老板说必须做这个理由更有说服力和感染力,也会让所有人全心全意去为项目努力付出


2. 明确各团队的职责,即为了这个项目需要做哪些功能的新增或对现有功能的优化。


3. 明确时间节点,即针对于上面提到的功能或优化,各团队开发、测试以及联调的时间节点,明确时间节点可以保证项目可以在计划的时间内完成。


4. 明确项目干系人:项目负责人、技术负责人、测试负责人,在遇到问题时可以找到对应负责人沟通。


这个阶段,在项目启动会完要出一份会议纪要,周知项目涉及的所有成员。


注意:不仅仅是与会人员,有时在项目启动会参与的同学也许仅仅是各团队的主要负责人,并不一定是最终参与项目开发和测试的同学。


所以在会议结束后将会议的内容整理成邮件,群发涉及各团队的所有成员。


会议纪要邮件中可以附上项目需求文档及原型,方便项目成员理解,同时还需要在会议纪要中明确项目启动会中确定的几个关键要素:

  • 项目负责人

  • 项目中各团队需要做的功能或优化的功能点

  • 项目的时间节点:开发时间、测试时间、联调时间和上线时间

CORNERSTONE里,可以同时并行管理多个项目。每个项目清晰明确可见责任⼈、任务状态、优先级、类别、时间等多维度信息,帮助企业快速⾼效的对项⽬进⾏全周期管理。


1.3 需求讨论及需求分析


image.png


作为产品经理,你可能是某一个项目的负责人,也可能是项目相关团队的产品经理。

无论哪一个,你都需要针对自己团队负责的任务进行需求整理,与自己团队的开发、交互视觉设计、测试确认需求、评估需求。CORNERSTONE讨论功能可供团队成员互相交流,共享信息,解决自己在工作中遇到的各种问题。


基于需求文档与开发、测试、设计进行沟通,确认需求并由相关人员给出工时。

在需求确认阶段要注意的是:每个迭代的人力成本和时间成本是有限的,并不是所有的需求都要在一个迭代干完,参照MVP设计原则,项目也是按照一期二期这样规划的。


所以在需求确认过程中,确认需求的优先级及排期,哪些必须一期实现,哪些需要在二期进行完善。


在进行需求优先级排序的过程中可以参考KANO模型,同时也要根据需求点的工时排列,保证在当前排期内可以完成。


这个阶段,输出的文档并非需要由产品负责,而是由具体的开发人员、测试人员、设计人员给出各自负责功能的任务项拆分,细化到天的颗粒度,保证任务是在监控的范围内。


2. 项目执行与监控


2.1 项目执行




需求确认、工时评估完成后,正式进入项目执行阶段,由相关成员进行开发、设计及测试。CORNERSTONE的甘特图功能可方便管理者弄清项目的剩余时间,评估工作进度,调整工作任务,更好地把握项目的整体。


2.2 站立会、周会


每日站立会以及周会是保证项目正常进行的手段之一,通过每天的站立会沟通,确认团队成员是否遇到了问题,针对问题进行及时沟通与解决,保证项目可以正常进行。


如果项目时间较长,通过周会可以统计周期内好的现象以及遇到的问题,通过会议总结,让各团队了解当前项目进度以及遇到的阻碍。


对于跨团队的项目,往往没有时间聚集起所有团队成员一起进行会议沟通,可以由项目负责人与各团队负责人进行周期性沟通,确认可团队的项目进度。


这个阶段,项目负责人会输出项目周报,周报的内容主要包含项目当前进度,项目遇到的问题与阻碍,项目下一阶段的计划,涉及各团队的关键里程碑节点。


2.3 联调


image.png


联调往往是跨团队项目需要考虑的问题,只要项目涉及的团队大于两个,就需要进行项目联调,保证各自团队负责的功能模块不会因为新的需求出现问题。CORNERSTONE针对这一需求,提供了全局报表(项目进度)。方便管理者了解项目分布、进度计划、质量风险等,并从中获取客观的实时数据,帮助管理人员分析、评估项目,全面了解组合内项目状况,以便作出及时决策。


如果涉及多团队涉及从前到后的流程变更,需要在联调前,召集各团队测试负责人进行沟通,明确测试范围、测试时间以及回归范围,保证项目上线时新功能模块的使用以及之前兼容功能的正常使用。


在测试联调阶段,需要每日召开团队间的站立会,确认各团队之间测试遇到的问题,如环境问题、版本问题等,提高测试效率,保证上线时间和上线质量,不要因为测试不充分出现上线后回滚的问题。


2.4 项目监控


image.png


项目监控,是保证项目进度,保证项目可以在规定时间内保质按时上线。CORNERSTONE中管理者可根据项目创建情况,可实时更新项目状态,预警项目风险。简单来说就是:对项目风险的管理——遇到项目风险如何处理,如何解决。


项目风险的可能性有很多,比如开发的delay、测试出现严重bug、业务需求方在项目进展过程中频繁变更需求导致工时无限延长等等。


image.png


CORNERSTONE在可视化的平台活动图上,任意自定义不同纬度统计卡⽚,可⼤⼤⽅便项⽬经理全⾯掌握项⽬进度和团队表现,了解每位成员⼯作产出与⼯时,提前化解潜在⻛险;同时⽀持⼀键分享卡⽚内容。


这里的沟通可能是向上沟通也可能是平行沟通,发现问题背后最本质的原因,基于此去解决问题,如果风险过大真的导致项目的delay,那么也要许沟通项目的各个相关方,保证当前线上不会出现问题。


3. 项目收尾


image.png


结束是新的开始,项目也好、产品也好,只要没有死,就一定还会有新的开始。


在产品的生命周期中,包含着无数个项目,这其中有好的项目也有不好的项目。


每一次的项目上线或收尾,都需要对项目进行一次复盘和回顾,发现项目过程中的优点与不足,优点继续保持,不足找到解决方案,在下一次项目中尽可能的避免。


在项目上线后,召集项目成员进行项目的总结与复盘,同时基于项目上线后的效果进行监控,为项目的下一期规划提供指导意见。


如果通过项目发现与市场、用户完全不契合,那么尽快放弃寻找新的方向;如果项目效果还不错,还有值得优化提高的地方,寻找可以优化的点进行新的排期与规划,通过不断的迭代提升产品价值,为用户创造更大的价值。

  三、总结

做项目与做产品一样,都是一个不断迭代不断打磨的过程,对于产品经理来说尤其是对于没有项目经理配合的产品经理来说,并不是产品需求确认后就可以坐等产品上线了。


在产品开发过程当中,不仅要考虑未来产品的规划,还要关注当前产品的进度,通过沟通、监控、协调,保证当前功能可以正常上线,否则再多的规划如果无法真的落地,也终究是规划。


做产品就是做项目,一个优秀的产品经理也必然是一个优秀的项目经理,优秀的产品/项目经理,搭配上好的项目管理工具,必然会对项目开发起到事半功倍的效果,项目管理工具我只推荐CORNERSTONE


image.png

GoCN每日新闻(2019-11-01)

回复

每日新闻lwhile 发起了问题 • 1 人关注 • 0 个回复 • 4272 次浏览 • 2019-11-01 13:15 • 来自相关话题

[ gev ] Go 语言优雅处理 TCP “粘包”

文章分享惜朝 发表了文章 • 0 个评论 • 761 次浏览 • 2019-11-01 10:48 • 来自相关话题

https://github.com/Allenxuxu/gev ...查看全部

https://github.com/Allenxuxu/gev

gev 是一个轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库,支持自定义协议,轻松快速搭建高性能服务器。

TCP 为什么会粘包


TCP 本身就是面向流的协议,就是一串没有界限的数据。所以本质上来说 TCP 粘包是一个伪命题。

TCP 底层并不关心上层业务数据,会套接字缓冲区的实际情况进行包的划分,一个完整的业务数据可能会被拆分成多次进行发送,也可能会将多个小的业务数据封装成一个大的数据包发送(Nagle算法)。

gev 如何优雅处理


gev 通过回调函数 OnMessage 通知用户数据到来,回调函数中会将用户数据缓冲区(ringbuffer)通过参数传递过来。

用户通过对 ringbuffer 操作,来进行数据解包,获取到完整用户数据后再进行业务操作。这样又一个明显的缺点,就是会让业务操作和自定义协议解析代码堆在一起。

所以,最近对 gev 进行了一次较大改动,主要是为了能够以插件的形式支持各种自定义的数据协议,让使用者可以便捷处理 TCP 粘包问题,专注于业务逻辑。



做法如下,定义一个接口 Protocol

```go
// Protocol 自定义协议编解码接口
type Protocol interface {
UnPacket(c *Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte)
Packet(c *Connection, data []byte) []byte
}
```


用户只需实现这个接口,并注册到 server 中,当客户端数据到来时,gev 会首先调用 UnPacket 方法,如果缓冲区中的数据足够组成一帧,则将数据解包,并返回真正的用户数据,然后在回调 OnMessage 函数并将数据通过参数传递。

下面,我们实现一个简单的自定义协议插件,来启动一个 Server :

```text
| 数据长度 n | payload |
| 4字节 | n 字节 |
```

```go
// protocol.go
package main

import (
"encoding/binary"
"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/ringbuffer"
"github.com/gobwas/pool/pbytes"
)

const exampleHeaderLen = 4

type ExampleProtocol struct{}

func (d *ExampleProtocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte) {
if buffer.VirtualLength() > exampleHeaderLen {
buf := pbytes.GetLen(exampleHeaderLen)
defer pbytes.Put(buf)
_, _ = buffer.VirtualRead(buf)
dataLen := binary.BigEndian.Uint32(buf)

if buffer.VirtualLength() >= int(dataLen) {
ret := make([]byte, dataLen)
_, _ = buffer.VirtualRead(ret)

buffer.VirtualFlush()
return nil, ret
} else {
buffer.VirtualRevert()
}
}
return nil, nil
}

func (d *ExampleProtocol) Packet(c *connection.Connection, data []byte) []byte {
dataLen := len(data)
ret := make([]byte, exampleHeaderLen+dataLen)
binary.BigEndian.PutUint32(ret, uint32(dataLen))
copy(ret[4:], data)
return ret
}
```

```go
// server.go
package main

import (
"flag"
"log"
"strconv"

"github.com/Allenxuxu/gev"
"github.com/Allenxuxu/gev/connection"
)

type example struct{}

func (s *example) OnConnect(c *connection.Connection) {
log.Println(" OnConnect : ", c.PeerAddr())
}
func (s *example) OnMessage(c *connection.Connection, ctx interface{}, data []byte) (out []byte) {
log.Println("OnMessage:", data)
out = data
return
}

func (s *example) OnClose(c *connection.Connection) {
log.Println("OnClose")
}

func main() {
handler := new(example)
var port int
var loops int

flag.IntVar(&port, "port", 1833, "server port")
flag.IntVar(&loops, "loops", -1, "num loops")
flag.Parse()

s, err := gev.NewServer(handler,
gev.Address(":"+strconv.Itoa(port)),
gev.NumLoops(loops),
gev.Protocol(&ExampleProtocol{}))
if err != nil {
panic(err)
}

log.Println("server start")
s.Start()
}
```
完整代码地址

当回调 `OnMessage` 函数的时候,会通过参数传递已经拆好包的用户数据。

当我们需要使用其他协议时,仅仅需要实现一个 Protocol 插件,然后只要 `gev.NewServer` 时指定即可:

```go
gev.NewServer(handler, gev.NumLoops(2), gev.Protocol(&XXXProtocol{}))
```

## 基于 Protocol Plugins 模式为 gev 实现 WebSocket 插件

得益于 Protocol Plugins 模式的引进,我可以将 WebSocket 的实现做成一个插件(WebSocket 协议构建在 TCP 之上),独立于 gev 之外。

```go
package websocket

import (
"log"

"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/gev/plugins/websocket/ws"
"github.com/Allenxuxu/ringbuffer"
)

// Protocol websocket
type Protocol struct {
upgrade *ws.Upgrader
}

// New 创建 websocket Protocol
func New(u *ws.Upgrader) *Protocol {
return &Protocol{upgrade: u}
}

// UnPacket 解析 websocket 协议,返回 header ,payload
func (p *Protocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (ctx interface{}, out []byte) {
upgraded := c.Context()
if upgraded == nil {
var err error
out, _, err = p.upgrade.Upgrade(buffer)
if err != nil {
log.Println("Websocket Upgrade :", err)
return
}
c.SetContext(true)
} else {
header, err := ws.VirtualReadHeader(buffer)
if err != nil {
log.Println(err)
return
}
if buffer.VirtualLength() >= int(header.Length) {
buffer.VirtualFlush()

payload := make([]byte, int(header.Length))
_, _ = buffer.Read(payload)

if header.Masked {
ws.Cipher(payload, header.Mask, 0)
}

ctx = &header
out = payload
} else {
buffer.VirtualRevert()
}
}
return
}

// Packet 直接返回
func (p *Protocol) Packet(c *connection.Connection, data []byte) []byte {
return data
}
```

具体的实现,可以到仓库的 [plugins/websocket](https://github.com/Allenxuxu/gev/tree/master/plugins/websocket) 查看。

## 相关文章

- [开源 gev: Go 实现基于 Reactor 模式的非阻塞 TCP 网络库](https://note.mogutou.xyz/articles/2019/09/19/1568896693634.html)
- [Go 网络库并发吞吐量测试](https://note.mogutou.xyz/articles/2019/09/22/1569146969662.html)

## 项目地址

https://github.com/Allenxuxu/gev

【go 源码】sync.Once 详解

文章分享xmgee 发表了文章 • 0 个评论 • 645 次浏览 • 2019-10-31 22:50 • 来自相关话题

# sync.Once 源码阅读## 1.Demo```package mainimport ( "fmt" "sync" "time")func m ...查看全部

# sync.Once 源码阅读

## 1.Demo

```
package main

import (
"fmt"
"sync"
"time"
)

func main() {
var once sync.Once

for i:=0;i<=10;i++{
go once.Do(func() {
fmt.Println("hello world")
})
}

time.Sleep(time.Second * 2)
}
```

## 2.介绍

sync.Once是sync包中的一个对象,它只有一个方法Do,这个方法很特殊,在程序运行过程中,无论被多少次调用,只会执行一次,就与结构体的名称一样,once(一次)。那它是如何做的呢?

## 3.使用场景

当程序运行过程中,在会被多次调用的地方却只想执行一次某代码块。就可以全局声明一个once,然后用once.Do()来之行此代码块。

## 4.源码

```
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sync

import (
"sync/atomic"
)

// Once is an object that will perform exactly one action.
type Once struct {
m Mutex
done uint32
}

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

```

## 5.源码解析

可以看到once结构体中,有两个字段,m是了保证并发安全性的,done是标志是否已经执行过此方法,如果done是1则表示执行过,0表示未执行。

Do方法中,首先通过atomic.LoadUint32(&o.done),来取得done的值,看是否为1,如果为1就表示已经执行过了,直接返回,未执行则继续执行。

代码很简单,就不啰嗦了,值得注意的是 `defer atomic.StoreUint32(&o.done, 1)`很精髓,为了防止f()方法中panic,无法为done赋值,作者特地使用defer。值得学习。

----

项目地址:github.com/xmge,更多go源码阅读文章将在公众号发布:

![img](https://gosc.oss-cn-beijing.aliyuncs.com/gosc.jpg)

GoCN每日新闻(2019-10-31)

回复

每日新闻keke001 发起了问题 • 1 人关注 • 0 个回复 • 5273 次浏览 • 2019-10-31 09:42 • 来自相关话题

【Go】高效截取字符串的一些思考

文章分享qiyin 发表了文章 • 0 个评论 • 686 次浏览 • 2019-10-31 08:33 • 来自相关话题

原文链接: ...查看全部

原文链接:https://blog.thinkeridea.com/201910/go/efficient_string_truncation.html

最近我在 Go Forum 中发现了 String size of 20 character 的问题,“hollowaykeanho” 给出了相关的答案,而我从中发现了截取字符串的方案并非最理想的方法,因此做了一系列实验并获得高效截取字符串的方法,这篇文章将逐步讲解我实践的过程。

字节切片截取

这正是 “hollowaykeanho” 给出的第一个方案,我想也是很多人想到的第一个方案,利用 go 的内置切片语法截取字符串:

s := "abcdef"
fmt.Println(s[1:4])

我们很快就了解到这是按字节截取,在处理 ASCII 单字节字符串截取,没有什么比这更完美的方案了,中文往往占多个字节,在 utf8 编码中是3个字节,如下程序我们将获得乱码数据:

s := "Go 语言"
fmt.Println(s[1:4])

杀手锏 - 类型转换 []rune

hollowaykeanho” 给出的第二个方案就是将字符串转换为 []rune,然后按切片语法截取,再把结果转成字符串。

s := "Go 语言"
rs := []rune(s)
fmt.Println(strings(rs[1:4]))

首先我们得到了正确的结果,这是最大的进步。不过我对类型转换一直比较谨慎,我担心它的性能问题,因此我尝试在搜索引擎和各大论坛查找答案,但是我得到最多的还是这个方案,似乎这已经是唯一的解。

我尝试写个性能测试评测它的性能:

package benchmark

import (
"testing"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRunes(s string, length int) string {
if utf8.RuneCountInString(s) > length {
rs := []rune(s)
return string(rs[:length])
}

return s
}

func BenchmarkSubStrRunes(b *testing.B) {
for i := 0; i < b.N; i++ {
SubStrRunes(benchmarkSubString, benchmarkSubStringLength)
}
}

我得到了让我有些吃惊的结果:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRunes-8 872253 1363 ns/op 336 B/op 2 allocs/op
PASS
ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 2.120s

对 69 个的字符串截取前 20 个字符需要大概 1.3 微秒,这极大的超出了我的心里预期,我发现因为类型转换带来了内存分配,这产生了一个新的字符串,并且类型转换需要大量的计算。

救命稻草 - utf8.DecodeRuneInString

我想改善类型转换带来的额外运算和内存分配,我仔细的梳理了一遍 strings 包,发现并没有相关的工具,这时我想到了 utf8 包,它提供了多字节计算相关的工具,实话说我对它并不熟悉,或者说没有主动(直接)使用过它,我查看了它所有的文档发现 utf8.DecodeRuneInString 函数可以转换单个字符,并给出字符占用字节的数量,我尝试了如此下的实验:

package benchmark

import (
"testing"
"unicode/utf8"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrDecodeRuneInString(s string, length int) string {
var size, n int
for i := 0; i < length && n < len(s); i++ {
_, size = utf8.DecodeRuneInString(s[n:])
n += size
}

return s[:n]
}

func BenchmarkSubStrDecodeRuneInString(b *testing.B) {
for i := 0; i < b.N; i++ {
SubStrDecodeRuneInString(benchmarkSubString, benchmarkSubStringLength)
}
}

运行它之后我得到了令我惊喜的结果:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrDecodeRuneInString-8 10774401 105 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.250s

较 []rune 类型转换效率提升了 13倍,消除了内存分配,它的确令人激动和兴奋,我迫不及待的回复了 “hollowaykeanho” 告诉他我发现了一个更好的方法,并提供了相关的性能测试。

我有些小激动,兴奋的浏览着论坛里各种有趣的问题,在查看一个问题的帮助时 (忘记是哪个问题了-_-||) ,我惊奇的发现了另一个思路。

良药不一定苦 - range 字符串迭代

许多人似乎遗忘了 range 是按字符迭代的,并非字节。使用 range 迭代字符串时返回字符起始索引和对应的字符,我立刻尝试利用这个特性编写了如下用例:

package benchmark

import (
"testing"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRange(s string, length int) string {
var n, i int
for i = range s {
if n == length {
break
}

n++
}

return s[:i]
}

func BenchmarkSubStrRange(b *testing.B) {
for i := 0; i < b.N; i++ {
SubStrRange(benchmarkSubString, benchmarkSubStringLength)
}
}

我尝试运行它,这似乎有着无穷的魔力,结果并没有令我失望。

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRange-8 12354991 91.3 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.233s

它仅仅提升了13%,但它足够的简单和易于理解,这似乎就是我苦苦寻找的那味良药。

如果你以为这就结束了,不、这对我来只是探索的开始。

终极时刻 - 自己造轮子

喝了 range 那碗甜的腻人的良药,我似乎冷静下来了,我需要造一个轮子,它需要更易用,更高效。

于是乎我仔细观察了两个优化方案,它们似乎都是为了查找截取指定长度字符的索引位置,如果我可以提供一个这样的方法,是否就可以提供用户一个简单的截取实现 s[:strIndex(20)] ,这个想法萌芽之后我就无法再度摆脱,我苦苦思索两天来如何来提供易于使用的接口。

之后我创造了 exutf8.RuneIndexInString 和 exutf8.RuneIndex 方法,分别用来计算字符串和字节切片中指定字符数量结束的索引位置。

我用 exutf8.RuneIndexInString 实现了一个字符串截取测试:

package benchmark

import (
"testing"
"unicode/utf8"

"github.com/thinkeridea/go-extend/exunicode/exutf8"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRuneIndexInString(s string, length int) string {
n, _ := exutf8.RuneIndexInString(s, length)
return s[:n]
}

func BenchmarkSubStrRuneIndexInString(b *testing.B) {
for i := 0; i < b.N; i++ {
SubStrRuneIndexInString(benchmarkSubString, benchmarkSubStringLength)
}
}

尝试运行它,我对结果感到十分欣慰:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRuneIndexInString-8 13546849 82.4 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.213s

性能较 range 提升了 10%,让我很欣慰可以再次获得新的提升,这证明它是有效的。

它足够的高效,但是却不够易用,我截取字符串需要两行代码,如果我想截取 10~20之间的字符就需要4行代码,这并不是用户易于使用的接口,我参考了其它语言的 sub_string 方法,我想我应该也设计一个这个样的接口给用户。

exutf8.RuneSubString 和 exutf8.RuneSub 是我认真思索后编写的方法:

func RuneSubString(s string, start, length int) string

它有三个参数:

  • s : 输入的字符串
  • start : 开始截取的位置,如果 start 是非负数,返回的字符串将从 string 的 start 位置开始,从 0 开始计算。例如,在字符串 “abcdef” 中,在位置 0 的字符是 “a”,位置 2 的字符串是 “c” 等等。 如果 start 是负数,返回的字符串将从 string 结尾处向前数第 start 个字符开始。 如果 string 的长度小于 start,将返回空字符串。
  • length:截取的长度,如果提供了正数的 length,返回的字符串将从 start 处开始最多包括 length 个字符(取决于 string 的长度)。 如果提供了负数的 length,那么 string 末尾处的 length 个字符将会被省略(若 start 是负数则从字符串尾部算起)。如果 start 不在这段文本中,那么将返回空字符串。 如果提供了值为 0 的 length,返回的子字符串将从 start 位置开始直到字符串结尾。

我为他们提供了别名,根据使用习惯大家更倾向去 strings 包寻找这类问题的解决方法,我创建了exstrings.SubString 和 exbytes.Sub 作为更易检索到的别名方法。

最后我需要再做一个性能测试,确保它的性能:

package benchmark

import (
"testing"

"github.com/thinkeridea/go-extend/exunicode/exutf8"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRuneSubString(s string, length int) string {
return exutf8.RuneSubString(s, 0, length)
}

func BenchmarkSubStrRuneSubString(b *testing.B) {
for i := 0; i < b.N; i++ {
SubStrRuneSubString(benchmarkSubString, benchmarkSubStringLength)
}
}

运行它,不会让我失望:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRuneSubString-8 13309082 83.9 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark 1.215s

虽然相较 exutf8.RuneIndexInString 有所下降,但它提供了易于交互和使用的接口,我认为这应该是最实用的方案,如果你追求极致仍然可以使用 exutf8.RuneIndexInString,它依然是最快的方案。

总结

当看到有疑问的代码,即使它十分的简单,依然值得深究,并不停的探索它,这并不枯燥和乏味,反而会有极多收获。

从起初 []rune 类型转换到最后自己造轮子,不仅得到了16倍的性能提升,我还学习了utf8包、加深了range 遍历字符串的特性 以及为 go-extend 仓库收录了多个实用高效的解决方案,让更多 go-extend 的用户得到成果。

go-extend 是一个收录实用、高效方法的仓库,读者们如果好的函数和通用高效的解决方案,期待你们不吝啬给我发送 Pull request,你也可以使用这个仓库加快功能实现及提升性能。

转载:

本文作者: 戚银(thinkeridea

本文链接: https://blog.thinkeridea.com/201910/go/efficient_string_truncation.html

版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处!