golang

golang

关于 二维码 解析

技术讨论stirlingx 回复了问题 • 9 人关注 • 13 个回复 • 955 次浏览 • 2 天前 • 来自相关话题

Go能写桌面程序吗?比如winform 这样的

有问必答udbmnm 回复了问题 • 17 人关注 • 14 个回复 • 2063 次浏览 • 5 天前 • 来自相关话题

【深圳(福田)-klook.com】-Golang开发工程师(招聘)

招聘应聘poiumn12345 发表了文章 • 0 个评论 • 304 次浏览 • 2017-07-12 11:01 • 来自相关话题

简历投递:jobs@klook.com 地址:福田天安科技创业园A座

职位描述:

  1. 负责Klook各线产品的开发、测试和上线
  2. ... 查看全部

简历投递:jobs@klook.com
地址:福田天安科技创业园A座


职位描述:



  1. 负责Klook各线产品的开发、测试和上线

  2. 负责管理自身项目和需求的优先级、按时高质量交付


职位要求:



  1. 熟练掌握Golang语言,具有扎实的计算机基础和编程能力,熟悉常见的算法与数据结构

  2. 熟悉Restful接口设计,有过互联网业务系统或相关技术产品开发经验

  3. 有较强的工作责任心和良好的沟通协调能力,能在压力下独立解决问题

  4. 熟练掌握MySQL数据库,擅长SQL优化者优先

  5. 有熟悉一门静态(C/C++/Java)语言者优先

  6. 开源贡献者优先,GitHub源码者优先,技术博客者优先


【 Klook客路团队 】



  • Klook客路旅行已于2016年完成B轮融资,在香港、深圳、台北、新加坡、吉隆坡、首尔、马尼拉、曼谷开设办公室

  • 国际化团队,半数成员来自海外近十个国家及地区


【 关于Klook客路旅行 】



  • KLOOK.com是全球千万人使用的旅行体验探索及预订平台。无论是在尼泊尔与秃鹰齐飞的滑翔伞历奇,还是潜入冲绳的蔚蓝海洞,或者全家同游迪士尼乐园,甚至在新加坡的高空缆车上享受浪漫晚宴……客路帮助旅行者们体验地道个性玩法、预订优惠景点门票、以特价观看演出、享受景点快速通道。

  • 全球超过300位各具专长的Klook旅行体验师们深入目的地,挖掘日韩/港澳台/东南亚/澳新等地区80+个城市、超过一万种独具特色的旅行活动。

  • Klook APP荣获App Store 2015 年度精选;

  • Klook APP荣获Google Play “Best of 2015”年度最佳应用;

  • Klook客路旅行官网 http://www.klook.com/

  • 官方公众号:“KLOOK旅行灵感指南”

  • 知乎专栏:“一群旅行体验师”,全站最具影响力的旅行专栏


【Klook客路福利】



  1. 舒适、健康、人性化的办公环境

  2. 扁平化的团队管理,业务飞速发展,你的能力和价值很快会被大家看到

  3. 开放和充满激情的国际化团队,男女高颜值并且比例协调,每个人都有那么一点传奇

  4. 独特的创业团队氛围会让你在完成工作之余,还有自己的时间学习、成长

  5. 体验Klook各类活动,全球城市旅行的机会

  6. 水果、零食,团队活动

release的时候如何把我们的版本号打包到应用中

有问必答kid_408 回复了问题 • 14 人关注 • 6 个回复 • 868 次浏览 • 2017-07-11 14:44 • 来自相关话题

govendor管理的项目依赖冲突

有问必答astaxie 回复了问题 • 2 人关注 • 1 个回复 • 321 次浏览 • 2017-07-10 14:13 • 来自相关话题

struct 嵌套这样定义好吗

有问必答taowen 回复了问题 • 2 人关注 • 1 个回复 • 278 次浏览 • 2017-07-05 11:09 • 来自相关话题

求一些golang的教程,书籍也可以

有问必答飞雪无情 回复了问题 • 43 人关注 • 24 个回复 • 3390 次浏览 • 2017-07-03 20:55 • 来自相关话题

Go 扩展包 netutil.LimitListener 如何接入 beego?

回复

有问必答mnhkahn 发起了问题 • 1 人关注 • 0 个回复 • 231 次浏览 • 2017-06-30 17:10 • 来自相关话题

go怎么没有条件编译

有问必答maxwell92 回复了问题 • 4 人关注 • 4 个回复 • 340 次浏览 • 2017-06-29 13:23 • 来自相关话题

GOLANG使用Context实现传值、超时和取消

技术讨论winlin 发表了文章 • 0 个评论 • 284 次浏览 • 2017-06-28 16:24 • 来自相关话题

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。

Context基本的用法参考GOL... 查看全部

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。


Context基本的用法参考GOLANG使用Context管理关联goroutine


实际上,Context还有个非常重要的作用,就是设置超时。比如,如果我们有个API是这样设计的:


type Packet interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
}

type Stack struct {
}
func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
return
}

一般使用是这样使用,创建context然后调用接口:


ctx,cancel := context.WithCancel(context.Background())
stack := &Stack{}
pkt,err := stack.Read(ctx)

那么,它本身就可以支持取消和超时,也就是用户如果需要取消,比如发送了SIGINT信号,程序需要退出,可以在收到信号后调用cancel


sc := make(chan os.Signal, 0)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
go func() {
for range sc {
cancel()
}
}()

如果需要超时,这个API也不用改,只需要调用前设置超时时间:


ctx,cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
pkt,err := stack.Read(ctx)

如果一个程序在运行,比如Read在等待,那么在没有人工干预的情况下,那就应该自己运行就好了。而人工干预,也就是需要取消,比如要升级程序了,或者需要停止服务了,都属于这种取消操作。而超时,一般是系统的策略,因为不能一直等下去,就需要在一定时间没有反应时终止服务。实际上context这两个都能支持得很好,而且还不影响Read本身的逻辑,在Read中只需要关注context是否Done:


func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
select {
// case <- dataChannel: // Parse packet from data channel.
case <- ctx.Done():
return nil,ctx.Err()
}
return
}

这是为何context被接纳成为标准库的包的缘故了吧,非常之强大和好用,而又非常简单。一行context,深藏功与名。


另外,Context还可以传递上下文的Key-Value对象,比如我们希望日志中,相关的goroutine都打印一个简化的CID,那么就可以用context.WithValue,参考go-oryx-lib/logger

udp编程的那些事与golang udp的实践

文章分享sheepbao 发表了文章 • 0 个评论 • 921 次浏览 • 2017-06-26 10:30 • 来自相关话题

udp编程的那些事与golang udp的实践

tcp/ip大协议中,tcp编程大家应该比较熟,应用的场景也很多,但是udp在现实中,应用也不少,而在大部分博文中,都很少对udp的编程进行研究,最近研究了一下udp编程,正好做个记录。<... 查看全部

udp编程的那些事与golang udp的实践


tcp/ip大协议中,tcp编程大家应该比较熟,应用的场景也很多,但是udp在现实中,应用也不少,而在大部分博文中,都很少对udp的编程进行研究,最近研究了一下udp编程,正好做个记录。

sheepbao 2017.06.15


tcp Vs udp


tcp和udp都是著名的传输协议,他们都是基于ip协议,都在OSI模型中传输层。tcp我们都很清楚,它提供了可靠的数据传输,而udp我们也知道,它不提供数据传输的可靠性,只是尽力传输。
他们的特性决定了它们很大的不同,tcp提供可靠性传输,有三次握手,4次分手,相当于具有逻辑上的连接,可以知道这个tcp连接的状态,所以我们都说tcp是面向连接的socket,而udp没有握手,没有分手,也不存在逻辑上的连接,所以我们也都说udp是非面向连接的socket。

我们都畏惧不知道状态的东西,所以即使tcp的协议比udp复杂很多,但对于系统应用层的编程来说,tcp编程其实比udp编程容易。而udp相对比较灵活,所以对于udp编程反而没那么容易,但其实掌握后udp编程也并不难。


udp协议


udp的首部


        2               2       (byte)
+---+---+---+---+---+---+---+---+ -
| src port | dst port | |
+---+---+---+---+---+---+---+---+ 8(bytes)
| length | check sum | |
+---+---+---+---+---+---+---+---+ -
| |
+ data +
| |
+---+---+---+---+---+---+---+---+

udp的首部真的很简单,头2个字节表示的是原端口,后2个字节表示的是目的端口,端口是系统层区分进程的标识。接着是udp长度,最后就是校验和,这个其实很重要,现在的系统都是默认开启udp校验和的,所以我们才能确保udp消息传输的完整性。如果这个校验和关闭了,那会让我们绝对会很忧伤,因为udp不仅不能保证数据一定到达,还不能保证即使数据到了,这个数据是否是正确的。比如:我在发送端发送了“hello”,而接收端却接收到了“hell”。如果真的是这样,我们就必须自己去校验数据的正确性。还好udp默认开发了校验,我们可以保证udp的数据完整性。


udp数据的封装


                                    +---------+
| 应用数据 |
+---------+
| |
v v
+---------+---------+
| udp首部 | 应用数据 |
+---------+---------+
| |
v UDP数据报 v
+---------+---------+---------+
| ip首部 | udp首部 | 应用数据 |
+---------+---------+---------+
| |
v IP数据报 v
+---------+---------+---------+---------+---------+
|以太网首部 | ip首部 | udp首部 | 应用数据 |以太网尾部 |
+---------+---------+---------+---------+---------+
| 14 20 8 4 |
| -> 以太网帧 <- |

数据的封装和tcp是一样,应用层的数据加上udp首部,构成udp数据报,再加上ip首部构成ip数据报,最后加上以太网首部和尾部构成以太网帧,经过网卡发送出去。


Golang udp实践


实践出真知,编程就需要多实践,才能体会其中的奥妙。


echo客户端和服务端


echo服务,实现数据包的回显,这是很多人网络编程起点,因为这个服务足够简单,但又把网络的数据流都过了一遍,这里也用go udp实现一个echo服务。

实现客户端发送一个“hello”,服务端接收消息并原封不动的返回给客户度。


server.go


package main

import (
"flag"
"fmt"
"log"
"net"
)

var addr = flag.String("addr", ":10000", "udp server bing address")

func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
flag.Parse()
}

func main() {
//Resolving address
udpAddr, err := net.ResolveUDPAddr("udp", *addr)
if err != nil {
log.Fatalln("Error: ", err)
}

// Build listining connections
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Fatalln("Error: ", err)
}
defer conn.Close()

// Interacting with one client at a time
recvBuff := make([]byte, 1024)
for {
log.Println("Ready to receive packets!")
// Receiving a message
rn, rmAddr, err := conn.ReadFromUDP(recvBuff)
if err != nil {
log.Println("Error:", err)
return
}

fmt.Printf("<<< Packet received from: %s, data: %s\n", rmAddr.String(), string(recvBuff[:rn]))
// Sending the same message back to current client
_, err = conn.WriteToUDP(recvBuff[:rn], rmAddr)
if err != nil {
log.Println("Error:", err)
return
}
fmt.Println(">>> Sent packet to: ", rmAddr.String())
}
}

client1.go


package main

import (
"flag"
"fmt"
"log"
"net"
)

var raddr = flag.String("raddr", "127.0.0.1:10000", "remote server address")

func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
flag.Parse()
}

func main() {
// Resolving Address
remoteAddr, err := net.ResolveUDPAddr("udp", *raddr)
if err != nil {
log.Fatalln("Error: ", err)
}

// Make a connection
tmpAddr := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 0,
}

conn, err := net.DialUDP("udp", tmpAddr, remoteAddr)
// Exit if some error occured
if err != nil {
log.Fatalln("Error: ", err)
}
defer conn.Close()

// write a message to server
_, err = conn.Write([]byte("hello"))
if err != nil {
log.Println(err)
} else {
fmt.Println(">>> Packet sent to: ", *raddr)
}

// Receive response from server
buf := make([]byte, 1024)
rn, rmAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Println(err)
} else {
fmt.Printf("<<< %d bytes received from: %v, data: %s\n", rn, rmAddr, string(buf[:rn]))
}
}

client2.go


package main

import (
"flag"
"fmt"
"log"
"net"
)

var (
laddr = flag.String("laddr", "127.0.0.1:9000", "local server address")
raddr = flag.String("raddr", "127.0.0.1:10000", "remote server address")
)

func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
flag.Parse()
}

func main() {
// Resolving Address
localAddr, err := net.ResolveUDPAddr("udp", *laddr)
if err != nil {
log.Fatalln("Error: ", err)
}

remoteAddr, err := net.ResolveUDPAddr("udp", *raddr)
if err != nil {
log.Fatalln("Error: ", err)
}

// Build listening connections
conn, err := net.ListenUDP("udp", localAddr)
// Exit if some error occured
if err != nil {
log.Fatalln("Error: ", err)
}
defer conn.Close()

// write a message to server
_, err = conn.WriteToUDP([]byte("hello"), remoteAddr)
if err != nil {
log.Println(err)
} else {
fmt.Println(">>> Packet sent to: ", *raddr)
}

// Receive response from server
buf := make([]byte, 1024)
rn, remAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Println(err)
} else {
fmt.Printf("<<< %d bytes received from: %v, data: %s\n", rn, remAddr, string(buf[:rn]))
}
}

这里实现echo的服务端和客户端,和tcp的差不多,但是有一些小细节需要注意。

对于server端,先net.ListenUDP建立udp一个监听,返回一个udp连接,这里需要注意udp不像tcp,建立tcp监听后返回的是一个Listener,然后阻塞等待接收一个新的连接,这样区别是因为udp一个非面向连接的协议,它没有会话管理。同时也因为udp是非面向连接的协议,当接收到消息后,想把消息返回给当前的客户端时,是不能像tcp一样,直接往conn里写的,而是需要指定远端地址。

对于client端,类似tcp先Dial,返回一个连接,对于发送消息用Write,接收消息用Read,当然udp也可以用ReadFromUDP,这样可以知道从哪得到的消息。但其实client也可以用另一种方式写,如client2.go程序,先建立一个监听,返回一个连接,用这个连接发送消息给服务端和从服务器接收消息,这种方式和tcp倒是有很大的不同。


参考


golang pkg

为什么gRPC客户端不提供连接池?

技术讨论elvin5 回复了问题 • 7 人关注 • 4 个回复 • 1223 次浏览 • 2017-06-26 10:15 • 来自相关话题

GOLANG如何避免字符串转义

技术讨论winlin 发表了文章 • 0 个评论 • 216 次浏览 • 2017-06-23 17:41 • 来自相关话题

避免转义字符,例如造个json:

json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)查看全部
					

避免转义字符,例如造个json:


json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)

是不是太简单了点,但是我好像并不总是记得。

GOLANG宽泛接口在测试中的大用处

技术讨论winlin 发表了文章 • 1 个评论 • 173 次浏览 • 2017-06-23 17:26 • 来自相关话题

考虑测试一个函数:

func request(ctx context.Context, hc *http.Client, api string) (err error... 			查看全部
					

考虑测试一个函数:


func request(ctx context.Context, hc *http.Client, api string) (err error) {
var hreq *http.Request
if hreq, err = http.NewRequest("GET", api, nil); err != nil {
return nil, errors.Wrap(err, "create request")
}
var hres *http.Response
if hres, err = hc.Do(hreq.WithContext(ctx)); err != nil {
return nil, errors.Wrap(err, "do request")
}
defer hres.Body.Close()

var body []byte
if body, err = ioutil.ReadAll(hres.Body); err != nil {
return nil, errors.Wrap(err, "read body")
}

// ......
return nil
}

这个函数的参数是一个*http.Client,而不是接口,这个该如何测试?内嵌一个http.Client像这样吗?


type mockHttpClient struct {
http.Client
}

但是,问题是这样总是很恶心不是吗?就像如果是C++中,我们只能写一个mock类从要测试的类继承,但是我们只需要重写Do这个方法啊。



注意:对于C++而言,这是为何要求构造函数只是初始化,而不能包含逻辑,想象一个类在构造函数就访问了数据库,请问如何MOCK它?是做不到的,因此只能在构造函数初始化数据库的IP和账号等信息,提供connect这种函数连接数据库。


备注:上面只是拿数据库连接打个比方,实际上从MOCK角度来说,构造函数只能初始化内存对象,其他的应该啥也不干。



在GOLANG中,有个非常牛逼的方法,就是创建一个私有的接口,使用时用接口:


type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func request(ctx context.Context, hc httpDoer, api string) (err error) {
// ......

可以发现,很神奇的是,调用者也可以给*http.Client,对这个改动一无所知,这难道不是极其巧妙的设计吗?我们在mock中只需要mock这个方法就可以了。


一行代码处,深藏功与名~

GOLANG测试必须用带堆栈的errors

技术讨论winlin 发表了文章 • 2 个评论 • 243 次浏览 • 2017-06-22 18:15 • 来自相关话题

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。

GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。

先看测试序列的填充,参考查看全部

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。


GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。


先看测试序列的填充,参考tricks-2015这个文章,怕你翻不了墙,我把内容粘贴过来就是:


Anonymous structs: test cases (1/2)


These properties enable a nice way to express test cases:


func TestIndex(t *testing.T) {
var tests = []struct {
s string
sep string
out int
}{
{"", "", 0},
{"", "a", -1},
{"fo", "foo", -1},
{"foo", "foo", 0},
{"oofofoofooo", "f", 2},
// etc
}
for _, test := range tests {
actual := strings.Index(test.s, test.sep)
if actual != test.out {
t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out)
}
}
}

是的,看起来很方便,出错时也知道哪里的问题,但是实际上如果序列中有函数,那就悲剧了。让我们来试试,考虑一个包头的定义,它就是两个字段,然后序列化成[]byte


type MyHeader struct {
Version uint8
Size uint16
}

func (v MyHeader) MarshalBinary() ([]byte, error) {
return []byte{byte(v.Version),0,0},nil // Failed.
}

为了测试设置不同的值,得到不同的字节,我们用两个函数来填充测试序列:


func TestMyHeader_MarshalBinary(t *testing.T) {
mhs := []struct {
set func(h *MyHeader)
compare func(p []byte) error
}{
{func(h *MyHeader) { h.Size = 1 }, func(p []byte) error {
if p[1] != 0x01 {
return fmt.Errorf("p[1] is %v", p[1]) // line 194
}
return nil
}},
{func(h *MyHeader) { h.Size = 2 }, func(p []byte) error {
if p[1] != 0x02 {
return fmt.Errorf("p[1] is %v", p[1]) // line 200
}
return nil
}},
}
for _, mh := range mhs {
h := &MyHeader{}
mh.set(h)
if b, err := h.MarshalBinary(); err != nil {
t.Errorf("error is %+v", err)
} else if err = mh.compare(b); err != nil {
t.Errorf("invalid data, err is %+v", err) // line 211
}
}
}

结果我们就懵逼了,出现的错误行数都是在error那个地方211行是不够的,还需要知道是194还是200出问题了:


--- FAIL: TestMyHeader_MarshalBinary (0.00s)
iprouter_test.go:211: invalid data, err is p[1] is 0
iprouter_test.go:211: invalid data, err is p[1] is 0

怎么办呢?把堆栈信息带上,参考错误最佳实践,改成这样:


import oe "github.com/ossrs/go-oryx-lib/errors"

创建error时用这个package:


            if p[1] != 0x01 {
return oe.Errorf("p[1] is %v", p[1]) // line 194
}
if p[1] != 0x02 {
return oe.Errorf("p[1] is %v", p[1]) // line 200
}

结果可以看到详细的堆栈:


    iprouter_test.go:211: invalid data, err is p[1] is 0
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary.func4
/Users/winlin/git/test/src/core_test.go:200
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary
/Users/winlin/git/test/src/core_test.go:210
testing.tRunner
/usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

这样可以嵌套非常多的函数做测试了。

条新动态, 点击查看
bigwhite

bigwhite 回答了问题 • 2016-10-11 13:03 • 8 个回复 不感兴趣

怎么学习golang?

赞同来自:

记得早期接触go时,看的是Golang之父之一的Rob Pike的Go course 3部曲(3个ppt),如果你没有,可以到[这里](https://pan.baidu.com/s/1miFEDJy)下载(注意:由于rob pike的这个ppt在go 1之前... 显示全部 »
记得早期接触go时,看的是Golang之父之一的Rob Pike的Go course 3部曲(3个ppt),如果你没有,可以到[这里](https://pan.baidu.com/s/1miFEDJy)下载(注意:由于rob pike的这个ppt在go 1之前发布的,ppt上的有些语法与go 1有少许差别,注意识别)。

要想深入了解Go,写出idiomatic的golang代码,官方doc:effective go, go faq,以及go language specification也是不可或缺的。

之后再系统的学习后的资料,建议看k&a写的go圣经-the go programming language、
《Go in action》 以及国内雨痕大师的《go语言学习笔记》(尤其是代码分析的部分),学习笔记一书适合稍微有些go经验,且抱着极大热情挖掘go runtime背后的原理的人去学习。

剩下的就是不断的去用go coding, coding, coding了,无捷径。
astaxie

astaxie 回答了问题 • 2016-10-10 18:35 • 21 个回复 不感兴趣

大家推荐哪种golang包管理方式?

赞同来自:

我们目前项目中使用的是godep,但是我最近尝试迁移到glide里面来,两个的功能都差不多,但是glide更强大一点,而且是Go1.5 vendor目录支持之后出来的,所以我还是比较推荐用这个。

这里列出来一些目前支持vendor的工具

* [manul... 显示全部 »
我们目前项目中使用的是godep,但是我最近尝试迁移到glide里面来,两个的功能都差不多,但是glide更强大一点,而且是Go1.5 vendor目录支持之后出来的,所以我还是比较推荐用这个。

这里列出来一些目前支持vendor的工具

* [manul](https://github.com/kovetskiy/manul) - Vendor packages using git submodules.
* [Godep](https://github.com/tools/godep)
* [Govendor](https://github.com/kardianos/govendor)
* [godm](https://github.com/hectorj/godm)
* [vexp](https://github.com/kr/vexp)
* [gv](https://github.com/forestgiant/gv)
* [gvt](https://github.com/FiloSottile/gvt) - Recursively retrieve and vendor packages.
* [govend](https://github.com/govend/govend)
* [Glide](https://github.com/Masterminds/glide) - Manage packages like composer, npm, bundler, or other languages.
* [Vendetta](https://github.com/dpw/vendetta)
* [trash](https://github.com/rancher/trash)
* [gsv](https://github.com/toxeus/gsv)
* [gom](https://github.com/mattn/gom)
astaxie

astaxie 回答了问题 • 2016-10-11 21:41 • 9 个回复 不感兴趣

golang 的channel是否适合做消息队列?

赞同来自:

我猜你看到的文章的担心是万一程序挂了怎么办,在缓冲channel里面的数据就可能丢失了,如果这个是可以忍受的话其实是非常适合做消息队列的
我猜你看到的文章的担心是万一程序挂了怎么办,在缓冲channel里面的数据就可能丢失了,如果这个是可以忍受的话其实是非常适合做消息队列的
name5566

name5566 回答了问题 • 2016-10-12 11:36 • 11 个回复 不感兴趣

golang有没有好的开源游戏框架

赞同来自:

> 使用 Leaf 已知的上线项目:
> * 2014 年,某手游(棋牌)项目上线
> * 2016 年,某 H5 手游项目上线
> * 2016 年,某卡牌手游项目上线
> 正在研发项目 N 个,已知情况 N >= 4

... 显示全部 »
> 使用 Leaf 已知的上线项目:
> * 2014 年,某手游(棋牌)项目上线
> * 2016 年,某 H5 手游项目上线
> * 2016 年,某卡牌手游项目上线
> 正在研发项目 N 个,已知情况 N >= 4

来自:https://github.com/name5566/leaf/wiki
leoliu

leoliu 回答了问题 • 2016-10-12 13:44 • 24 个回复 不感兴趣

求一些golang的教程,书籍也可以

赞同来自:

《The Golang Programming Language》
《Golang 学习笔记》
《The Golang Programming Language》
《Golang 学习笔记》
yougg

yougg 回答了问题 • 2016-10-14 10:01 • 78 个回复 不感兴趣

大家说说看都用啥写Go

赞同来自:

# IDEA大法好
# 天灭vscode 退软保平安
# 人在做,天在看 中文乱码留祸患
# 界面卡顿天地灭 赶紧卸载保平安
# 诚心诚念IDEA好 JetBrains大法平安保
# 众生皆为IDEA来 现世险恶忘前缘
# 开源为你说真相 教你脱险莫拒绝
# ... 显示全部 »
# IDEA大法好
# 天灭vscode 退软保平安
# 人在做,天在看 中文乱码留祸患
# 界面卡顿天地灭 赶紧卸载保平安
# 诚心诚念IDEA好 JetBrains大法平安保
# 众生皆为IDEA来 现世险恶忘前缘
# 开源为你说真相 教你脱险莫拒绝
# 早日不做软粉,早日获得新生
# 上网搜索“九评纳德拉”
# 有 真 相
sryan

sryan 回答了问题 • 2016-10-13 11:44 • 7 个回复 不感兴趣

golang 如何动态创建struct

赞同来自:

静态语言貌似不能直接实现
可以自己实现个map[string]func(string)interface{}
将要动态生成的结构体的函数注册上去
通过string来调用相应的函数来获取对应的结构体
静态语言貌似不能直接实现
可以自己实现个map[string]func(string)interface{}
将要动态生成的结构体的函数注册上去
通过string来调用相应的函数来获取对应的结构体
astaxie

astaxie 回答了问题 • 2016-10-13 22:04 • 12 个回复 不感兴趣

想用golang写个分布式的监控,大神给点建议

赞同来自:

这个问题很有意思,很多场景设计都会来考虑拉和推两种方案,我分别对拉和推两种的优缺点对比以下,你自己权衡一下,欢迎大家继续补充

## 拉的方案(不写agent)
优点:
- 不需要agent,不需要再部署新的程序

缺点:
- 网络中断的情况下,就无法监控机器... 显示全部 »
这个问题很有意思,很多场景设计都会来考虑拉和推两种方案,我分别对拉和推两种的优缺点对比以下,你自己权衡一下,欢迎大家继续补充

## 拉的方案(不写agent)
优点:
- 不需要agent,不需要再部署新的程序

缺点:
- 网络中断的情况下,就无法监控机器的信息

## 推的方案(agent)
优点:
- 本地运行,在和中控机失去网络连接的时候还是可以继续保存监控数据

缺点:
- 需要部署agent,如果机器多得话将来升级也是比较麻烦

拉取和推送其实大家可以考虑,微博的follow逻辑,直播流里面也有同样的问题,很多场景都会遇到

至于说第二种方案走什么协议,这种程序我建议走tcp协议,HTTP的话相对重了一点。
jinzhu

jinzhu 回答了问题 • 2016-10-19 15:38 • 14 个回复 不感兴趣

2016 年 10 月,当前好用的 ORM 是哪个?

赞同来自:

作为作者,推荐 GORM ;)
作为作者,推荐 GORM ;)
sheepbao

sheepbao 回答了问题 • 2016-10-30 20:16 • 15 个回复 不感兴趣

字符串连接哪一种方式最高效

赞同来自:

```go
package main

import (
"bytes"
"fmt"
"strings"
"time"
)

var way map[int]string
... 显示全部 »
```go
package main

import (
"bytes"
"fmt"
"strings"
"time"
)

var way map[int]string

func benchmarkStringFunction(n int, index int) (d time.Duration) {
v := "ni shuo wo shi bu shi tai wu liao le a?"
var s string
var buf bytes.Buffer

t0 := time.Now()
for i := 0; i < n; i++ {
switch index {
case 0: // fmt.Sprintf
s = fmt.Sprintf("%s[%s]", s, v)
case 1: // string +
s = s + "[" + v + "]"
case 2: // strings.Join
s = strings.Join([]string{s, "[", v, "]"}, "")
case 3: // stable bytes.Buffer
buf.WriteString("[")
buf.WriteString(v)
buf.WriteString("]")
}

}
d = time.Since(t0)
if index == 3 {
s = buf.String()
}
fmt.Printf("string len: %d\t", len(s))
fmt.Printf("time of [%s]=\t %v\n", way[index], d)
return d
}

func main() {
way = make(map[int]string, 5)
way[0] = "fmt.Sprintf"
way[1] = "+"
way[2] = "strings.Join"
way[3] = "bytes.Buffer"

k := 4
d := [5]time.Duration{}
for i := 0; i < k; i++ {
d[i] = benchmarkStringFunction(10000, i)
}
}

```
结果:
```
string len: 410000 time of [fmt.Sprintf]= 426.001476ms
string len: 410000 time of [+]= 307.044147ms
string len: 410000 time of [strings.Join]= 738.44362ms
string len: 410000 time of [bytes.Buffer]= 742.248µs
```
* strings.Join 最慢
* fmt.Sprintf 和 string + 差不多
* bytes.Buffer又比上者快约500倍
CodyGuo

CodyGuo 回答了问题 • 2016-10-30 12:57 • 4 个回复 不感兴趣

Go里面如何写多行的字符串吗?

赞同来自:

```go
`line 1
line 2
line 3`
```
```go
`line 1
line 2
line 3`
```
philosophia14

philosophia14 回答了问题 • 2016-11-05 00:30 • 3 个回复 不感兴趣

新版本Go将会对database/sql进行大量改进

赞同来自:

但愿不要多改..
但愿不要多改..
zdreamx

zdreamx 回答了问题 • 2016-11-08 11:27 • 26 个回复 不感兴趣

beego1.8版本功能征集

赞同来自:

orm有没有考虑分表分区分库的优化或支持
orm有没有考虑分表分区分库的优化或支持
println 是把结果输出到 standard error

fmt.Println 是把结果输出到 standard output
println 是把结果输出到 standard error

fmt.Println 是把结果输出到 standard output

大家是如何处理 golang web 应用静态资源的?

技术讨论astaxie 回复了问题 • 4 人关注 • 1 个回复 • 1001 次浏览 • 2016-10-14 13:08 • 来自相关话题

Python 程序员的 Golang 学习指南(II): 开发环境搭建

文章分享Cloudinsight 发表了文章 • 0 个评论 • 1339 次浏览 • 2016-10-12 15:44 • 来自相关话题

Authors: startover

Authors: startover





上一篇文章我们已经对 Golang 有了初步的了解,这篇主要介绍如何在 Ubuntu 14.04 上搭建 Golang 开发环境。


安装 Golang


这里就按照官方文档进行安装即可,如下:



  • 下载并解压安装包到指定目录


$ wget https://storage.googleapis.com ... ar.gz
$ tar -C /usr/local -xzf go1.6.3.linux-amd64.tar.gz


  • 设置 PATH


$ echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc
$ source ~/.bashrc


  • 验证安装


$ go version
go version go1.6.3 linux/amd64

环境变量设置


$ echo "export GOROOT=/usr/local/go" >> ~/.bashrc
$ echo "export GOPATH=$HOME/go" >> ~/.bashrc
$ source ~/.bashrc

其中,GOROOT 为 Golang 的安装目录,只有当 Golang 安装到除 /usr/local 之外的路径时需要设置,反之则不用设置,GOPATH 是 Golang 的开发目录,详细可参考官方文档


开发工具


工欲善其事,必先利其器,作为一名伪 VIMer,这里主要介绍下如何在 Vim 下配置 Golang 开发环境。


由于之前一直使用 k-vim 作为 Python 开发环境,而 k-vim 已经集成了当前使用最为广泛的用于搭建 Golang 开发环境的 vim 插件 vim-go,只是默认没有开启,需要我们手动进行相关设置。


k-vim 中开启 Golang 语言的支持,非常简单,如下:



  • 修改 ~/.vimrc.bundles(开启 golang 支持,并修改 vim-go 的默认配置,增加快捷键配置等)。


let g:bundle_groups=['python', 'javascript', 'markdown', 'html', 'css', 'tmux', 'beta', 'json', 'golang']

" vimgo {{{
let g:go_highlight_functions = 1
let g:go_highlight_methods = 1
let g:go_highlight_structs = 1
let g:go_highlight_operators = 1
let g:go_highlight_build_constraints = 1

let g:go_fmt_fail_silently = 1
let g:go_fmt_command = "goimports"
let g:syntastic_go_checkers = ['golint', 'govet', 'errcheck']

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)
au FileType go nnoremap <leader>gr :GoRun %<CR>
" }}}



  • 在 Vim 内执行 :PlugInstall,安装 vim-go




  • 在 Vim 内执行 :GoInstallBinaries,下载并安装 vim-go 依赖的二进制工具,goimportsgolint 等。



  • 安装 gotags,使 tagbar 配置生效。


$ go get -u github.com/jstemmer/gotags

我们来看一下最终效果:


Image of Golang Environment in Vim


编写第一个程序


进入工作目录,新建文件 hello.go,如下:


$ cd $GOPATH
$ vim hello.go
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

运行程序:


$ go run hello.go
Hello, World!



本文章为 Cloudinsight 技术团队工程师原创,更多技术文章可访问 Cloudinsight 技术博客Cloudinsight 为可视化系统监控工具,涵盖 Windows、Linux 操作系统,用 Golang 开发的 Cloudinsight Agent 正式开源了,欢迎 fork,Github:https://github.com/cloudinsight/cloudinsight-agent


golang-for-pythonistas 系列持续更新中,欢迎关注~

Python 程序员的 Golang 学习指南(I): Go 之初体验

文章分享Cloudinsight 发表了文章 • 3 个评论 • 1306 次浏览 • 2016-10-12 15:27 • 来自相关话题

Authors: startover

Authors: startover





Go 语言简介


Go,又称 golang,是 Google 开发的一种静态强类型,编译型,并发型,并具有垃圾回收功能的编程语言。


Go 语言于2009年11月正式宣布推出,自2012年发布1.0,最新稳定版1.7。目前,Go的相关工具和生态已逐渐趋于完善,也不乏重量级项目,如 Docker, Kubernetes, Etcd, InfluxDB 等。


Go 语言能解决什么样的问题


同绝大多数通用型编程语言相比,Go 语言更多的是为了解决我们在构建大型服务器软件过程中所遇到的软件工程方面的问题而设计的。乍看上去,这么讲可能会让人感觉 Go 非常无趣且工业化,但实际上,在设计过程中就着重于清晰和简洁,以及较高的可组合性,最后得到的反而会是一门使用起来效率高而且很有趣的编程语言,很多程序员都会发现,它有极强的表达力而且功能非常强大。


总结为以下几点:



  • 清晰的依赖关系

  • 清晰的语法

  • 清晰的语义

  • 偏向组合而不是继承

  • 提供简单的编程模型(垃圾回收、并发)

  • 强大的内置工具(gofmt、godoc、gofix等)


建议有兴趣的同学看看 Go在谷歌:以软件工程为目的的语言设计


Go 语言相对 Python 有哪些优势


这里引用一段知乎上某大牛的回答,如下:




  • 部署简单。Go 编译生成的是一个静态可执行文件,除了 glibc 外没有其他外部依赖。这让部署变得异常方便:目标机器上只需要一个基础的系统和必要的管理、监控工具,完全不需要操心应用所需的各种包、库的依赖关系,大大减轻了维护的负担。这和 Python 有着巨大的区别。由于历史的原因,Python 的部署工具生态相当混乱【比如 setuptools, distutils, pip, buildout 的不同适用场合以及兼容性问题】。官方 PyPI 源又经常出问题,需要搭建私有镜像,而维护这个镜像又要花费不少时间和精力。




  • 并发性好。Goroutine 和 channel 使得编写高并发的服务端软件变得相当容易,很多情况下完全不需要考虑锁机制以及由此带来的各种问题。单个 Go 应用也能有效的利用多个 CPU 核,并行执行的性能好。这和 Python 也是天壤之比。多线程和多进程的服务端程序编写起来并不简单,而且由于全局锁 GIL 的原因,多线程的 Python 程序并不能有效利用多核,只能用多进程的方式部署;如果用标准库里的 multiprocessing 包又会对监控和管理造成不少的挑战【我们用的 supervisor 管理进程,对 fork 支持不好】。部署 Python 应用的时候通常是每个 CPU 核部署一个应用,这会造成不少资源的浪费,比如假设某个 Python 应用启动后需要占用 100MB 内存,而服务器有 32 个 CPU 核,那么留一个核给系统、运行 31 个应用副本就要浪费 3GB 的内存资源。




  • 良好的语言设计。从学术的角度讲 Go 语言其实非常平庸,不支持许多高级的语言特性;但从工程的角度讲,Go 的设计是非常优秀的:规范足够简单灵活,有其他语言基础的程序员都能迅速上手。更重要的是 Go 自带完善的工具链,大大提高了团队协作的一致性。比如 gofmt 自动排版 Go 代码,很大程度上杜绝了不同人写的代码排版风格不一致的问题。把编辑器配置成在编辑存档的时候自动运行 gofmt,这样在编写代码的时候可以随意摆放位置,存档的时候自动变成正确排版的代码。此外还有 gofix, govet 等非常有用的工具。



  • 执行性能好。虽然不如 C 和 Java,但通常比原生 Python 应用还是高一个数量级的,适合编写一些瓶颈业务。内存占用也非常省。


从个人对 Golang 的初步使用来说,体验还是相当不错的,但是也有下面几点需要注意:




  • 驼峰式命名风格(依据首字母大小写来决定其是否能被其他包引用),但我更喜欢 Python 的小写字母加下划线命名风格。




  • 没有好用的包管理器,Golang 官方也没有推荐最佳的包管理方案,目前公认的比较好用的有 Godeps, Govendor 及 Glide,而 Python 的包管理器 pip 已形成自己的一套标准。




  • 多行字符串的变量声明需要用反引号(`),Python 里是三个双引号("""),参考http://stackoverflow.com/questions/7933460/how-do-you-write-multiline-strings-in-go




  • Golang 中的类型匹配是很严格的,不同的类型之间通常需要手动转换,所以在字符串拼接时往往需要对整型进行显式转换,如 fmt.Println("num: " + strconv.Itoa(1))



  • Golang 语言语法里的语法糖并不多,如在 Python 中很流行的 map, reduce, range 等,在 Golang 里都没有得到支持。


另外,推荐阅读 Golang 新手开发者要注意的陷阱和常见错误


学习资料推荐


建议先把 Go 的官方文档过一遍,主要有以下几项:



官方文档看完后,基本也算入门了,这时候可以看看 Go 的示例代码,或者去 Project Euler 刷刷题。


当然也可以去知乎看看大牛们都是如何学习的,链接 https://www.zhihu.com/question/23486344


总结


虽然 Go 有很多被诟病的地方,比如 GC 和对错误的处理方式,但没有任何语言是完美的,从实用角度来讲,Go 有着不输于 Python 的开发效率,完善的第三方工具,以及强大的社区支持,这些就足够了。


相关链接:

https://golang.org/doc/

https://talks.golang.org/2012/splash.article

https://www.zhihu.com/question/21409296

https://www.zhihu.com/question/23486344

http://stackoverflow.com/questions/7933460/how-do-you-write-multiline-strings-in-go

http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/

http://www.oschina.net/translate/go-at-google-language-design-in-the-service-of-software-engineering




本文章为 Cloudinsight 技术团队工程师原创,更多技术文章可访问 Cloudinsight 技术博客Cloudinsight 为可视化系统监控工具,涵盖 Windows、Linux 操作系统,用 Golang 开发的 Cloudinsight Agent 正式开源了,欢迎 fork,Github:https://github.com/cloudinsight/cloudinsight-agent


golang-for-pythonistas 系列持续更新中,欢迎关注~

求一些golang的教程,书籍也可以

有问必答飞雪无情 回复了问题 • 43 人关注 • 24 个回复 • 3390 次浏览 • 2017-07-03 20:55 • 来自相关话题

golang有没有好的开源游戏框架

技术讨论chrislee 回复了问题 • 19 人关注 • 11 个回复 • 4299 次浏览 • 2017-03-25 09:59 • 来自相关话题

为什么Go里面大多数的接口返回的是int类型

有问必答negronihe 回复了问题 • 4 人关注 • 3 个回复 • 1556 次浏览 • 2016-10-16 13:06 • 来自相关话题

大家推荐哪种golang包管理方式?

有问必答tpkeeper 回复了问题 • 29 人关注 • 21 个回复 • 4145 次浏览 • 2017-06-15 15:51 • 来自相关话题

关于 二维码 解析

回复

技术讨论stirlingx 回复了问题 • 9 人关注 • 13 个回复 • 955 次浏览 • 2 天前 • 来自相关话题

Go能写桌面程序吗?比如winform 这样的

回复

有问必答udbmnm 回复了问题 • 17 人关注 • 14 个回复 • 2063 次浏览 • 5 天前 • 来自相关话题

release的时候如何把我们的版本号打包到应用中

回复

有问必答kid_408 回复了问题 • 14 人关注 • 6 个回复 • 868 次浏览 • 2017-07-11 14:44 • 来自相关话题

govendor管理的项目依赖冲突

回复

有问必答astaxie 回复了问题 • 2 人关注 • 1 个回复 • 321 次浏览 • 2017-07-10 14:13 • 来自相关话题

struct 嵌套这样定义好吗

回复

有问必答taowen 回复了问题 • 2 人关注 • 1 个回复 • 278 次浏览 • 2017-07-05 11:09 • 来自相关话题

求一些golang的教程,书籍也可以

回复

有问必答飞雪无情 回复了问题 • 43 人关注 • 24 个回复 • 3390 次浏览 • 2017-07-03 20:55 • 来自相关话题

Go 扩展包 netutil.LimitListener 如何接入 beego?

回复

有问必答mnhkahn 发起了问题 • 1 人关注 • 0 个回复 • 231 次浏览 • 2017-06-30 17:10 • 来自相关话题

go怎么没有条件编译

回复

有问必答maxwell92 回复了问题 • 4 人关注 • 4 个回复 • 340 次浏览 • 2017-06-29 13:23 • 来自相关话题

为什么gRPC客户端不提供连接池?

回复

技术讨论elvin5 回复了问题 • 7 人关注 • 4 个回复 • 1223 次浏览 • 2017-06-26 10:15 • 来自相关话题

golang 标准库里这种没有具体代码执行的函数是怎么回事?

回复

有问必答silenceshell 回复了问题 • 7 人关注 • 6 个回复 • 686 次浏览 • 2017-06-18 21:59 • 来自相关话题

大家推荐哪种golang包管理方式?

回复

有问必答tpkeeper 回复了问题 • 29 人关注 • 21 个回复 • 4145 次浏览 • 2017-06-15 15:51 • 来自相关话题

beego访问redis

回复

技术讨论yuyifeichina 回复了问题 • 2 人关注 • 3 个回复 • 384 次浏览 • 2017-06-12 16:10 • 来自相关话题

Go有好用的Scheduler框架吗?有界面管理任务的最好?

回复

有问必答artong0416 回复了问题 • 9 人关注 • 3 个回复 • 536 次浏览 • 2017-06-07 11:52 • 来自相关话题

Go指针复制问题

回复

有问必答adolphlwq 回复了问题 • 3 人关注 • 4 个回复 • 334 次浏览 • 2017-06-06 19:03 • 来自相关话题

(开源)基于vue, react, node.js, go开发的微商城(含微信小程序)

回复

开源程序shen100 回复了问题 • 6 人关注 • 3 个回复 • 699 次浏览 • 2017-06-01 14:19 • 来自相关话题

【深圳(福田)-klook.com】-Golang开发工程师(招聘)

招聘应聘poiumn12345 发表了文章 • 0 个评论 • 304 次浏览 • 2017-07-12 11:01 • 来自相关话题

简历投递:jobs@klook.com 地址:福田天安科技创业园A座

职位描述:

  1. 负责Klook各线产品的开发、测试和上线
  2. ... 查看全部

简历投递:jobs@klook.com
地址:福田天安科技创业园A座


职位描述:



  1. 负责Klook各线产品的开发、测试和上线

  2. 负责管理自身项目和需求的优先级、按时高质量交付


职位要求:



  1. 熟练掌握Golang语言,具有扎实的计算机基础和编程能力,熟悉常见的算法与数据结构

  2. 熟悉Restful接口设计,有过互联网业务系统或相关技术产品开发经验

  3. 有较强的工作责任心和良好的沟通协调能力,能在压力下独立解决问题

  4. 熟练掌握MySQL数据库,擅长SQL优化者优先

  5. 有熟悉一门静态(C/C++/Java)语言者优先

  6. 开源贡献者优先,GitHub源码者优先,技术博客者优先


【 Klook客路团队 】



  • Klook客路旅行已于2016年完成B轮融资,在香港、深圳、台北、新加坡、吉隆坡、首尔、马尼拉、曼谷开设办公室

  • 国际化团队,半数成员来自海外近十个国家及地区


【 关于Klook客路旅行 】



  • KLOOK.com是全球千万人使用的旅行体验探索及预订平台。无论是在尼泊尔与秃鹰齐飞的滑翔伞历奇,还是潜入冲绳的蔚蓝海洞,或者全家同游迪士尼乐园,甚至在新加坡的高空缆车上享受浪漫晚宴……客路帮助旅行者们体验地道个性玩法、预订优惠景点门票、以特价观看演出、享受景点快速通道。

  • 全球超过300位各具专长的Klook旅行体验师们深入目的地,挖掘日韩/港澳台/东南亚/澳新等地区80+个城市、超过一万种独具特色的旅行活动。

  • Klook APP荣获App Store 2015 年度精选;

  • Klook APP荣获Google Play “Best of 2015”年度最佳应用;

  • Klook客路旅行官网 http://www.klook.com/

  • 官方公众号:“KLOOK旅行灵感指南”

  • 知乎专栏:“一群旅行体验师”,全站最具影响力的旅行专栏


【Klook客路福利】



  1. 舒适、健康、人性化的办公环境

  2. 扁平化的团队管理,业务飞速发展,你的能力和价值很快会被大家看到

  3. 开放和充满激情的国际化团队,男女高颜值并且比例协调,每个人都有那么一点传奇

  4. 独特的创业团队氛围会让你在完成工作之余,还有自己的时间学习、成长

  5. 体验Klook各类活动,全球城市旅行的机会

  6. 水果、零食,团队活动

GOLANG使用Context实现传值、超时和取消

技术讨论winlin 发表了文章 • 0 个评论 • 284 次浏览 • 2017-06-28 16:24 • 来自相关话题

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。

Context基本的用法参考GOL... 查看全部

GO1.7之后,新增了context.Context这个package,实现goroutine的管理。


Context基本的用法参考GOLANG使用Context管理关联goroutine


实际上,Context还有个非常重要的作用,就是设置超时。比如,如果我们有个API是这样设计的:


type Packet interface {
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
}

type Stack struct {
}
func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
return
}

一般使用是这样使用,创建context然后调用接口:


ctx,cancel := context.WithCancel(context.Background())
stack := &Stack{}
pkt,err := stack.Read(ctx)

那么,它本身就可以支持取消和超时,也就是用户如果需要取消,比如发送了SIGINT信号,程序需要退出,可以在收到信号后调用cancel


sc := make(chan os.Signal, 0)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
go func() {
for range sc {
cancel()
}
}()

如果需要超时,这个API也不用改,只需要调用前设置超时时间:


ctx,cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
pkt,err := stack.Read(ctx)

如果一个程序在运行,比如Read在等待,那么在没有人工干预的情况下,那就应该自己运行就好了。而人工干预,也就是需要取消,比如要升级程序了,或者需要停止服务了,都属于这种取消操作。而超时,一般是系统的策略,因为不能一直等下去,就需要在一定时间没有反应时终止服务。实际上context这两个都能支持得很好,而且还不影响Read本身的逻辑,在Read中只需要关注context是否Done:


func (v *Stack) Read(ctx context.Context) (pkt Packet, err error) {
select {
// case <- dataChannel: // Parse packet from data channel.
case <- ctx.Done():
return nil,ctx.Err()
}
return
}

这是为何context被接纳成为标准库的包的缘故了吧,非常之强大和好用,而又非常简单。一行context,深藏功与名。


另外,Context还可以传递上下文的Key-Value对象,比如我们希望日志中,相关的goroutine都打印一个简化的CID,那么就可以用context.WithValue,参考go-oryx-lib/logger

udp编程的那些事与golang udp的实践

文章分享sheepbao 发表了文章 • 0 个评论 • 921 次浏览 • 2017-06-26 10:30 • 来自相关话题

udp编程的那些事与golang udp的实践

tcp/ip大协议中,tcp编程大家应该比较熟,应用的场景也很多,但是udp在现实中,应用也不少,而在大部分博文中,都很少对udp的编程进行研究,最近研究了一下udp编程,正好做个记录。<... 查看全部

udp编程的那些事与golang udp的实践


tcp/ip大协议中,tcp编程大家应该比较熟,应用的场景也很多,但是udp在现实中,应用也不少,而在大部分博文中,都很少对udp的编程进行研究,最近研究了一下udp编程,正好做个记录。

sheepbao 2017.06.15


tcp Vs udp


tcp和udp都是著名的传输协议,他们都是基于ip协议,都在OSI模型中传输层。tcp我们都很清楚,它提供了可靠的数据传输,而udp我们也知道,它不提供数据传输的可靠性,只是尽力传输。
他们的特性决定了它们很大的不同,tcp提供可靠性传输,有三次握手,4次分手,相当于具有逻辑上的连接,可以知道这个tcp连接的状态,所以我们都说tcp是面向连接的socket,而udp没有握手,没有分手,也不存在逻辑上的连接,所以我们也都说udp是非面向连接的socket。

我们都畏惧不知道状态的东西,所以即使tcp的协议比udp复杂很多,但对于系统应用层的编程来说,tcp编程其实比udp编程容易。而udp相对比较灵活,所以对于udp编程反而没那么容易,但其实掌握后udp编程也并不难。


udp协议


udp的首部


        2               2       (byte)
+---+---+---+---+---+---+---+---+ -
| src port | dst port | |
+---+---+---+---+---+---+---+---+ 8(bytes)
| length | check sum | |
+---+---+---+---+---+---+---+---+ -
| |
+ data +
| |
+---+---+---+---+---+---+---+---+

udp的首部真的很简单,头2个字节表示的是原端口,后2个字节表示的是目的端口,端口是系统层区分进程的标识。接着是udp长度,最后就是校验和,这个其实很重要,现在的系统都是默认开启udp校验和的,所以我们才能确保udp消息传输的完整性。如果这个校验和关闭了,那会让我们绝对会很忧伤,因为udp不仅不能保证数据一定到达,还不能保证即使数据到了,这个数据是否是正确的。比如:我在发送端发送了“hello”,而接收端却接收到了“hell”。如果真的是这样,我们就必须自己去校验数据的正确性。还好udp默认开发了校验,我们可以保证udp的数据完整性。


udp数据的封装


                                    +---------+
| 应用数据 |
+---------+
| |
v v
+---------+---------+
| udp首部 | 应用数据 |
+---------+---------+
| |
v UDP数据报 v
+---------+---------+---------+
| ip首部 | udp首部 | 应用数据 |
+---------+---------+---------+
| |
v IP数据报 v
+---------+---------+---------+---------+---------+
|以太网首部 | ip首部 | udp首部 | 应用数据 |以太网尾部 |
+---------+---------+---------+---------+---------+
| 14 20 8 4 |
| -> 以太网帧 <- |

数据的封装和tcp是一样,应用层的数据加上udp首部,构成udp数据报,再加上ip首部构成ip数据报,最后加上以太网首部和尾部构成以太网帧,经过网卡发送出去。


Golang udp实践


实践出真知,编程就需要多实践,才能体会其中的奥妙。


echo客户端和服务端


echo服务,实现数据包的回显,这是很多人网络编程起点,因为这个服务足够简单,但又把网络的数据流都过了一遍,这里也用go udp实现一个echo服务。

实现客户端发送一个“hello”,服务端接收消息并原封不动的返回给客户度。


server.go


package main

import (
"flag"
"fmt"
"log"
"net"
)

var addr = flag.String("addr", ":10000", "udp server bing address")

func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
flag.Parse()
}

func main() {
//Resolving address
udpAddr, err := net.ResolveUDPAddr("udp", *addr)
if err != nil {
log.Fatalln("Error: ", err)
}

// Build listining connections
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Fatalln("Error: ", err)
}
defer conn.Close()

// Interacting with one client at a time
recvBuff := make([]byte, 1024)
for {
log.Println("Ready to receive packets!")
// Receiving a message
rn, rmAddr, err := conn.ReadFromUDP(recvBuff)
if err != nil {
log.Println("Error:", err)
return
}

fmt.Printf("<<< Packet received from: %s, data: %s\n", rmAddr.String(), string(recvBuff[:rn]))
// Sending the same message back to current client
_, err = conn.WriteToUDP(recvBuff[:rn], rmAddr)
if err != nil {
log.Println("Error:", err)
return
}
fmt.Println(">>> Sent packet to: ", rmAddr.String())
}
}

client1.go


package main

import (
"flag"
"fmt"
"log"
"net"
)

var raddr = flag.String("raddr", "127.0.0.1:10000", "remote server address")

func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
flag.Parse()
}

func main() {
// Resolving Address
remoteAddr, err := net.ResolveUDPAddr("udp", *raddr)
if err != nil {
log.Fatalln("Error: ", err)
}

// Make a connection
tmpAddr := &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 0,
}

conn, err := net.DialUDP("udp", tmpAddr, remoteAddr)
// Exit if some error occured
if err != nil {
log.Fatalln("Error: ", err)
}
defer conn.Close()

// write a message to server
_, err = conn.Write([]byte("hello"))
if err != nil {
log.Println(err)
} else {
fmt.Println(">>> Packet sent to: ", *raddr)
}

// Receive response from server
buf := make([]byte, 1024)
rn, rmAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Println(err)
} else {
fmt.Printf("<<< %d bytes received from: %v, data: %s\n", rn, rmAddr, string(buf[:rn]))
}
}

client2.go


package main

import (
"flag"
"fmt"
"log"
"net"
)

var (
laddr = flag.String("laddr", "127.0.0.1:9000", "local server address")
raddr = flag.String("raddr", "127.0.0.1:10000", "remote server address")
)

func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
flag.Parse()
}

func main() {
// Resolving Address
localAddr, err := net.ResolveUDPAddr("udp", *laddr)
if err != nil {
log.Fatalln("Error: ", err)
}

remoteAddr, err := net.ResolveUDPAddr("udp", *raddr)
if err != nil {
log.Fatalln("Error: ", err)
}

// Build listening connections
conn, err := net.ListenUDP("udp", localAddr)
// Exit if some error occured
if err != nil {
log.Fatalln("Error: ", err)
}
defer conn.Close()

// write a message to server
_, err = conn.WriteToUDP([]byte("hello"), remoteAddr)
if err != nil {
log.Println(err)
} else {
fmt.Println(">>> Packet sent to: ", *raddr)
}

// Receive response from server
buf := make([]byte, 1024)
rn, remAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Println(err)
} else {
fmt.Printf("<<< %d bytes received from: %v, data: %s\n", rn, remAddr, string(buf[:rn]))
}
}

这里实现echo的服务端和客户端,和tcp的差不多,但是有一些小细节需要注意。

对于server端,先net.ListenUDP建立udp一个监听,返回一个udp连接,这里需要注意udp不像tcp,建立tcp监听后返回的是一个Listener,然后阻塞等待接收一个新的连接,这样区别是因为udp一个非面向连接的协议,它没有会话管理。同时也因为udp是非面向连接的协议,当接收到消息后,想把消息返回给当前的客户端时,是不能像tcp一样,直接往conn里写的,而是需要指定远端地址。

对于client端,类似tcp先Dial,返回一个连接,对于发送消息用Write,接收消息用Read,当然udp也可以用ReadFromUDP,这样可以知道从哪得到的消息。但其实client也可以用另一种方式写,如client2.go程序,先建立一个监听,返回一个连接,用这个连接发送消息给服务端和从服务器接收消息,这种方式和tcp倒是有很大的不同。


参考


golang pkg

GOLANG如何避免字符串转义

技术讨论winlin 发表了文章 • 0 个评论 • 216 次浏览 • 2017-06-23 17:41 • 来自相关话题

避免转义字符,例如造个json:

json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)查看全部
					

避免转义字符,例如造个json:


json.Unmarshal(`{"code":0, "data":{"server":"127.0.0.1:8080"}}`)

是不是太简单了点,但是我好像并不总是记得。

GOLANG宽泛接口在测试中的大用处

技术讨论winlin 发表了文章 • 1 个评论 • 173 次浏览 • 2017-06-23 17:26 • 来自相关话题

考虑测试一个函数:

func request(ctx context.Context, hc *http.Client, api string) (err error... 			查看全部
					

考虑测试一个函数:


func request(ctx context.Context, hc *http.Client, api string) (err error) {
var hreq *http.Request
if hreq, err = http.NewRequest("GET", api, nil); err != nil {
return nil, errors.Wrap(err, "create request")
}
var hres *http.Response
if hres, err = hc.Do(hreq.WithContext(ctx)); err != nil {
return nil, errors.Wrap(err, "do request")
}
defer hres.Body.Close()

var body []byte
if body, err = ioutil.ReadAll(hres.Body); err != nil {
return nil, errors.Wrap(err, "read body")
}

// ......
return nil
}

这个函数的参数是一个*http.Client,而不是接口,这个该如何测试?内嵌一个http.Client像这样吗?


type mockHttpClient struct {
http.Client
}

但是,问题是这样总是很恶心不是吗?就像如果是C++中,我们只能写一个mock类从要测试的类继承,但是我们只需要重写Do这个方法啊。



注意:对于C++而言,这是为何要求构造函数只是初始化,而不能包含逻辑,想象一个类在构造函数就访问了数据库,请问如何MOCK它?是做不到的,因此只能在构造函数初始化数据库的IP和账号等信息,提供connect这种函数连接数据库。


备注:上面只是拿数据库连接打个比方,实际上从MOCK角度来说,构造函数只能初始化内存对象,其他的应该啥也不干。



在GOLANG中,有个非常牛逼的方法,就是创建一个私有的接口,使用时用接口:


type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func request(ctx context.Context, hc httpDoer, api string) (err error) {
// ......

可以发现,很神奇的是,调用者也可以给*http.Client,对这个改动一无所知,这难道不是极其巧妙的设计吗?我们在mock中只需要mock这个方法就可以了。


一行代码处,深藏功与名~

GOLANG测试必须用带堆栈的errors

技术讨论winlin 发表了文章 • 2 个评论 • 243 次浏览 • 2017-06-22 18:15 • 来自相关话题

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。

GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。

先看测试序列的填充,参考查看全部

GOLANG测试时,可以用匿名对象填充测试序列,但是如果没有带堆栈的errors,那么会造成出现错误时无法排查。


GOLANG初始化匿名结构的方式,可以很方便的建立测试集合。


先看测试序列的填充,参考tricks-2015这个文章,怕你翻不了墙,我把内容粘贴过来就是:


Anonymous structs: test cases (1/2)


These properties enable a nice way to express test cases:


func TestIndex(t *testing.T) {
var tests = []struct {
s string
sep string
out int
}{
{"", "", 0},
{"", "a", -1},
{"fo", "foo", -1},
{"foo", "foo", 0},
{"oofofoofooo", "f", 2},
// etc
}
for _, test := range tests {
actual := strings.Index(test.s, test.sep)
if actual != test.out {
t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out)
}
}
}

是的,看起来很方便,出错时也知道哪里的问题,但是实际上如果序列中有函数,那就悲剧了。让我们来试试,考虑一个包头的定义,它就是两个字段,然后序列化成[]byte


type MyHeader struct {
Version uint8
Size uint16
}

func (v MyHeader) MarshalBinary() ([]byte, error) {
return []byte{byte(v.Version),0,0},nil // Failed.
}

为了测试设置不同的值,得到不同的字节,我们用两个函数来填充测试序列:


func TestMyHeader_MarshalBinary(t *testing.T) {
mhs := []struct {
set func(h *MyHeader)
compare func(p []byte) error
}{
{func(h *MyHeader) { h.Size = 1 }, func(p []byte) error {
if p[1] != 0x01 {
return fmt.Errorf("p[1] is %v", p[1]) // line 194
}
return nil
}},
{func(h *MyHeader) { h.Size = 2 }, func(p []byte) error {
if p[1] != 0x02 {
return fmt.Errorf("p[1] is %v", p[1]) // line 200
}
return nil
}},
}
for _, mh := range mhs {
h := &MyHeader{}
mh.set(h)
if b, err := h.MarshalBinary(); err != nil {
t.Errorf("error is %+v", err)
} else if err = mh.compare(b); err != nil {
t.Errorf("invalid data, err is %+v", err) // line 211
}
}
}

结果我们就懵逼了,出现的错误行数都是在error那个地方211行是不够的,还需要知道是194还是200出问题了:


--- FAIL: TestMyHeader_MarshalBinary (0.00s)
iprouter_test.go:211: invalid data, err is p[1] is 0
iprouter_test.go:211: invalid data, err is p[1] is 0

怎么办呢?把堆栈信息带上,参考错误最佳实践,改成这样:


import oe "github.com/ossrs/go-oryx-lib/errors"

创建error时用这个package:


            if p[1] != 0x01 {
return oe.Errorf("p[1] is %v", p[1]) // line 194
}
if p[1] != 0x02 {
return oe.Errorf("p[1] is %v", p[1]) // line 200
}

结果可以看到详细的堆栈:


    iprouter_test.go:211: invalid data, err is p[1] is 0
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary.func4
/Users/winlin/git/test/src/core_test.go:200
_/Users/winlin/git/test/src/core.TestMyHeader_MarshalBinary
/Users/winlin/git/test/src/core_test.go:210
testing.tRunner
/usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

这样可以嵌套非常多的函数做测试了。

重庆 诚聘golang工程师 12k-25k

招聘应聘酒酒 发表了文章 • 3 个评论 • 359 次浏览 • 2017-06-22 15:33 • 来自相关话题

岗位职责:

1.协助架构师完成即时通信系统的技术架构设计与实施; 2.完成服务端通信层和业务层程序的编写。 任职要求:

1.1年golang以上开发经验,熟悉Linux环境及常用命令; 2.熟悉基于TCP/IP、Rpc、Htt... 查看全部

岗位职责:


1.协助架构师完成即时通信系统的技术架构设计与实施;
2.完成服务端通信层和业务层程序的编写。
任职要求:


1.1年golang以上开发经验,熟悉Linux环境及常用命令;
2.熟悉基于TCP/IP、Rpc、Http、Socket等通信编程优先考虑;
3.熟悉C/C++/Java/ python任意一种语言开发经验优先;
4.熟练缓存和nosql数据库使用经验优先考虑;
5.具有一定的架构设计能力优先考虑;
6.有分布式系统开发设计经验,参与开发并成功运维过高并发大据项目的优先考虑;
7.有IM,XMPP,ejabberd开发经验优先。 上班时间:灵活上班时间+双休
地址:重庆市渝北区洪湖西路24号B栋20楼


联系方式:qq:983092900 联系电话:15683298185 或直接将简历发送至983092900@qq.com

wechat_pusher : 基于Golang开发的微信消息定时推送框架

开源程序HundredLee 发表了文章 • 2 个评论 • 302 次浏览 • 2017-06-14 12:55 • 来自相关话题

wechat_pusher


Github



  • https://github.com/hundredlee/wechat_pusher

  • 欢迎star && fork && watch

  • 学Golang不久,写一个开源练练手。希望大家提提建议。谢谢

    功能列表


  • 消息推送

    • 模板消息推送

      • model -> message.go

      • task -> template_task.go


    • 图片推送(TODO)

    • 文字推送(TODO)

    • 图文推送(TODO)


  • 日志存储

  • 计划任务


如何开始?


第一步:当然是go get




  • go get github.com/hundredlee/wechat_pusher.git



  • 项目结构如下:


├── README.md
├── config
│   └── config.go
├── config.conf
├── config.conf.example
├── enum
│   └── task_type.go
├── glide.lock
├── glide.yaml
├── hlog
│   ├── filelog.go
│   ├── filelog_test.go
│   └── hlog.go
├── main.go
├── main.go.example
├── models
│   ├── message.go
│   └── token.go
├── redis
│   ├── redis.go
│   └── redis_test.go
├── statics
│   └── global.go
├── task
│   ├── task.go
│   └── template_task.go
├── utils
│   ├── access_token.go
│   ├── crontab.go
│   └── push.go
└── vendor
└── github.com

第二步:创建一个项目


创建配置文件



  • 项目根目录有一个config.conf.example,重命名为config.conf即可

  • 内容如下:


[WeChat]
APPID=
SECRET=
TOKEN=

[Redis]
POOL_SIZE=
TIMEOUT=
HOST=
PASS=
DB=

[Log]
LOG_PATH=


  • WeChat部分

    • APPID && SECRET && TOKEN 这些是微信开发者必须了解的东西。不细讲


  • Redis部分

    • POOL_SIZE 连接池大小 ,整型 int

    • TIMEOUT 连接超时时间 ,整型 int

    • HOST 连接的IP 字符串 string

    • PASS 密码 字符串 string

    • DB 数据库选择 整型 int



  • Log部分



    • LOG_PATH 日志存放文件夹,例如值为wechat_log,那么完整的目录应该是 GOPATH/wechat_log



  • 调用的时候这么写:



conf := config.Instance()
//例如wechat 的 appid
appId := conf.ConMap["WeChat.APPID"]

模板怎么配置



  • 以模板消息作为例子说明:

  • message.go 是模板消息的结构

  • template_task.go 是将一个模板消息封装成任务(template_task.go 是实现了接口task.go的)


mess := models.Message{
ToUser: "openid",
TemplateId: "templateid",
Url: "url",
Data: models.Data{
First: models.Raw{"xxx", "#173177"},
Subject: models.Raw{"xxx", "#173177"},
Sender: models.Raw{"xxx", "#173177"},
Remark: models.Raw{"xxx", "#173177"}}}

//封装成一个任务,TemplateTask表示模板消息任务
task := task.TemplateTask{}
task.SetTask(mess)


  • 以上代码是模板消息的配置,这个微信开发者应该都能看懂。


如何创建一个任务



  • 例如我们要创建一个模板消息定时推送任务

    • 第一步,封装任务

    • 第二步,添加任务,并设置任务类型、并发执行的个数、失败尝试次数等。

    • 第三步,启动任务


  • 我们用示例代码演示整个完整的过程


package main

import (
"github.com/hundredlee/wechat_pusher/enum"
"github.com/hundredlee/wechat_pusher/models"
"github.com/hundredlee/wechat_pusher/task"
"github.com/hundredlee/wechat_pusher/utils"
"runtime"
)

func main() {

runtime.GOMAXPROCS(runtime.NumCPU())
var tasks []task.Task
tasks = make([]task.Task, 100)
mess := models.Message{
ToUser: "oBv9cuLU5zyI27CtzI4VhV6Xabms",
TemplateId: "UXb6s5dahNC5Zt-xQIxbLJG1BdP8mP73LGLhNXl68J8",
Url: "http://baidu.com",
Data: models.Data{
First: models.Raw{"xxx", "#173177"},
Subject: models.Raw{"xxx", "#173177"},
Sender: models.Raw{"xxx", "#173177"},
Remark: models.Raw{"xxx", "#173177"}}}
task := task.TemplateTask{}
task.SetTask(mess)

for i := 0; i < 100; i++ {
tasks[i] = &task
}

utils.NewPush(tasks).SetTaskType(enum.TASK_TYPE_TEMPLATE).SetRetries(4).SetBufferNum(10).Add("45 * * * * *")
utils.StartCron()

}


Run



  • 很简单,当你组装好所有的task以后,直接运行一句话就可以了。


  • utils.NewPush(tasks).SetTaskType(enum.TASK_TYPE_TEMPLATE).SetRetries(4).SetBufferNum(10).Add("45 * * * * *")



  • utils.StartCron()


Contributor


kcp-go源码解析

文章分享sheepbao 发表了文章 • 0 个评论 • 1333 次浏览 • 2017-06-12 15:24 • 来自相关话题

kcp-go源码解析

对kcp-go的源码解析,有错误之处,请一定告之。
sheepbao 2017.0612

概念

ARQ:自动重传请求(Automatic Repeat-reQuest,... 查看全部

kcp-go源码解析


对kcp-go的源码解析,有错误之处,请一定告之。

sheepbao 2017.0612


概念


ARQ:自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层的错误纠正协议之一.

RTO:Retransmission TimeOut

FEC:Forward Error Correction


kcp简介


kcp是一个基于udp实现快速、可靠、向前纠错的的协议,能以比TCP浪费10%-20%的带宽的代价,换取平均延迟降低30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发。查看官方文档kcp


kcp-go是用go实现了kcp协议的一个库,其实kcp类似tcp,协议的实现也很多参考tcp协议的实现,滑动窗口,快速重传,选择性重传,慢启动等。

kcp和tcp一样,也分客户端和监听端。


    +-+-+-+-+-+            +-+-+-+-+-+
| Client | | Server |
+-+-+-+-+-+ +-+-+-+-+-+
|------ kcp data ------>|
|<----- kcp data -------|

kcp协议


layer model


+----------------------+
| Session |
+----------------------+
| KCP(ARQ) |
+----------------------+
| FEC(OPTIONAL) |
+----------------------+
| CRYPTO(OPTIONAL)|
+----------------------+
| UDP(Packet) |
+----------------------+

KCP header


KCP Header Format


      4           1   1     2 (Byte)
+---+---+---+---+---+---+---+---+
| conv |cmd|frg| wnd |
+---+---+---+---+---+---+---+---+
| ts | sn |
+---+---+---+---+---+---+---+---+
| una | len |
+---+---+---+---+---+---+---+---+
| |
+ DATA +
| |
+---+---+---+---+---+---+---+---+

代码结构


src/vendor/github.com/xtaci/kcp-go/
├── LICENSE
├── README.md
├── crypt.go 加解密实现
├── crypt_test.go
├── donate.png
├── fec.go 向前纠错实现
├── frame.png
├── kcp-go.png
├── kcp.go kcp协议实现
├── kcp_test.go
├── sess.go 会话管理实现
├── sess_test.go
├── snmp.go 数据统计实现
├── updater.go 任务调度实现
├── xor.go xor封装
└── xor_test.go

着重研究两个文件kcp.gosess.go


kcp浅析


kcp是基于udp实现的,所有udp的实现这里不做介绍,kcp做的事情就是怎么封装udp的数据和怎么解析udp的数据,再加各种处理机制,为了重传,拥塞控制,纠错等。下面介绍kcp客户端和服务端整体实现的流程,只是大概介绍一下函数流,不做详细解析,详细解析看后面数据流的解析。


kcp client整体函数流


和tcp一样,kcp要连接服务端需要先拨号,但是和tcp有个很大的不同是,即使服务端没有启动,客户端一样可以拨号成功,因为实际上这里的拨号没有发送任何信息,而tcp在这里需要三次握手。


DialWithOptions(raddr string, block BlockCrypt, dataShards, parityShards int)
V
net.DialUDP("udp", nil, udpaddr)
V
NewConn()
V
newUDPSession() {初始化UDPSession}
V
NewKCP() {初始化kcp}
V
updater.addSession(sess) {管理session会话,任务管理,根据用户设置的internal参数间隔来轮流唤醒任务}
V
go sess.readLoop()
V
go s.receiver(chPacket)
V
s.kcpInput(data)
V
s.fecDecoder.decodeBytes(data)
V
s.kcp.Input(data, true, s.ackNoDelay)
V
kcp.parse_data(seg) {将分段好的数据插入kcp.rcv_buf缓冲}
V
notifyReadEvent()

客户端大体的流程如上面所示,先Dial,建立udp连接,将这个连接封装成一个会话,然后启动一个go程,接收udp的消息。


kcp server整体函数流


ListenWithOptions() 
V
net.ListenUDP()
V
ServerConn()
V
newFECDecoder()
V
go l.monitor() {从chPacket接收udp数据,写入kcp}
V
go l.receiver(chPacket) {从upd接收数据,并入队列}
V
newUDPSession()
V
updater.addSession(sess) {管理session会话,任务管理,根据用户设置的internal参数间隔来轮流唤醒任务}
V
s.kcpInput(data)`
V
s.fecDecoder.decodeBytes(data)
V
s.kcp.Input(data, true, s.ackNoDelay)
V
kcp.parse_data(seg) {将分段好的数据插入kcp.rcv_buf缓冲}
V
notifyReadEvent()

服务端的大体流程如上图所示,先Listen,启动udp监听,接着用一个go程监控udp的数据包,负责将不同session的数据写入不同的udp连接,然后解析封装将数据交给上层。


kcp 数据流详细解析


不管是kcp的客户端还是服务端,他们都有io行为,就是读与写,我们只分析一个就好了,因为它们读写的实现是一样的,这里分析客户端的读与写。


kcp client 发送消息


s.Write(b []byte) 
V
s.kcp.WaitSnd() {}
V
s.kcp.Send(b) {将数据根据mss分段,并存在kcp.snd_queue}
V
s.kcp.flush(false) [flush data to output] {
if writeDelay==true {
flush
}else{
每隔`interval`时间flush一次
}
}
V
kcp.output(buffer, size)
V
s.output(buf)
V
s.conn.WriteTo(ext, s.remote)
V
s.conn..Conn.WriteTo(buf)

读写都是在sess.go文件中实现的,Write方法:


// Write implements net.Conn
func (s *UDPSession) Write(b []byte) (n int, err error) {
for {
...

// api flow control
if s.kcp.WaitSnd() < int(s.kcp.snd_wnd) {
n = len(b)
for {
if len(b) <= int(s.kcp.mss) {
s.kcp.Send(b)
break
} else {
s.kcp.Send(b[:s.kcp.mss])
b = b[s.kcp.mss:]
}
}

if !s.writeDelay {
s.kcp.flush(false)
}
s.mu.Unlock()
atomic.AddUint64(&DefaultSnmp.BytesSent, uint64(n))
return n, nil
}

...
// wait for write event or timeout
select {
case <-s.chWriteEvent:
case <-c:
case <-s.die:
}

if timeout != nil {
timeout.Stop()
}
}
}

假设发送一个hello消息,Write方法会先判断发送窗口是否已满,满的话该函数阻塞,不满则kcp.Send("hello"),而Send函数实现根据mss的值对数据分段,当然这里的发送的hello,长度太短,只分了一个段,并把它们插入发送的队列里。


func (kcp *KCP) Send(buffer []byte) int {
...
for i := 0; i < count; i++ {
var size int
if len(buffer) > int(kcp.mss) {
size = int(kcp.mss)
} else {
size = len(buffer)
}
seg := kcp.newSegment(size)
copy(seg.data, buffer[:size])
if kcp.stream == 0 { // message mode
seg.frg = uint8(count - i - 1)
} else { // stream mode
seg.frg = 0
}
kcp.snd_queue = append(kcp.snd_queue, seg)
buffer = buffer[size:]
}
return 0
}

接着判断参数writeDelay,如果参数设置为false,则立马发送消息,否则需要任务调度后才会触发发送,发送消息是由flush函数实现的。


// flush pending data
func (kcp *KCP) flush(ackOnly bool) {
var seg Segment
seg.conv = kcp.conv
seg.cmd = IKCP_CMD_ACK
seg.wnd = kcp.wnd_unused()
seg.una = kcp.rcv_nxt

buffer := kcp.buffer
// flush acknowledges
ptr := buffer
for i, ack := range kcp.acklist {
size := len(buffer) - len(ptr)
if size+IKCP_OVERHEAD > int(kcp.mtu) {
kcp.output(buffer, size)
ptr = buffer
}
// filter jitters caused by bufferbloat
if ack.sn >= kcp.rcv_nxt || len(kcp.acklist)-1 == i {
seg.sn, seg.ts = ack.sn, ack.ts
ptr = seg.encode(ptr)

}
}
kcp.acklist = kcp.acklist[0:0]

if ackOnly { // flash remain ack segments
size := len(buffer) - len(ptr)
if size > 0 {
kcp.output(buffer, size)
}
return
}

// probe window size (if remote window size equals zero)
if kcp.rmt_wnd == 0 {
current := currentMs()
if kcp.probe_wait == 0 {
kcp.probe_wait = IKCP_PROBE_INIT
kcp.ts_probe = current + kcp.probe_wait
} else {
if _itimediff(current, kcp.ts_probe) >= 0 {
if kcp.probe_wait < IKCP_PROBE_INIT {
kcp.probe_wait = IKCP_PROBE_INIT
}
kcp.probe_wait += kcp.probe_wait / 2
if kcp.probe_wait > IKCP_PROBE_LIMIT {
kcp.probe_wait = IKCP_PROBE_LIMIT
}
kcp.ts_probe = current + kcp.probe_wait
kcp.probe |= IKCP_ASK_SEND
}
}
} else {
kcp.ts_probe = 0
kcp.probe_wait = 0
}

// flush window probing commands
if (kcp.probe & IKCP_ASK_SEND) != 0 {
seg.cmd = IKCP_CMD_WASK
size := len(buffer) - len(ptr)
if size+IKCP_OVERHEAD > int(kcp.mtu) {
kcp.output(buffer, size)
ptr = buffer
}
ptr = seg.encode(ptr)
}

// flush window probing commands
if (kcp.probe & IKCP_ASK_TELL) != 0 {
seg.cmd = IKCP_CMD_WINS
size := len(buffer) - len(ptr)
if size+IKCP_OVERHEAD > int(kcp.mtu) {
kcp.output(buffer, size)
ptr = buffer
}
ptr = seg.encode(ptr)
}

kcp.probe = 0

// calculate window size
cwnd := _imin_(kcp.snd_wnd, kcp.rmt_wnd)
if kcp.nocwnd == 0 {
cwnd = _imin_(kcp.cwnd, cwnd)
}

// sliding window, controlled by snd_nxt && sna_una+cwnd
newSegsCount := 0
for k := range kcp.snd_queue {
if _itimediff(kcp.snd_nxt, kcp.snd_una+cwnd) >= 0 {
break
}
newseg := kcp.snd_queue[k]
newseg.conv = kcp.conv
newseg.cmd = IKCP_CMD_PUSH
newseg.sn = kcp.snd_nxt
kcp.snd_buf = append(kcp.snd_buf, newseg)
kcp.snd_nxt++
newSegsCount++
kcp.snd_queue[k].data = nil
}
if newSegsCount > 0 {
kcp.snd_queue = kcp.remove_front(kcp.snd_queue, newSegsCount)
}

// calculate resent
resent := uint32(kcp.fastresend)
if kcp.fastresend <= 0 {
resent = 0xffffffff
}

// check for retransmissions
current := currentMs()
var change, lost, lostSegs, fastRetransSegs, earlyRetransSegs uint64
for k := range kcp.snd_buf {
segment := &kcp.snd_buf[k]
needsend := false
if segment.xmit == 0 { // initial transmit
needsend = true
segment.rto = kcp.rx_rto
segment.resendts = current + segment.rto
} else if _itimediff(current, segment.resendts) >= 0 { // RTO
needsend = true
if kcp.nodelay == 0 {
segment.rto += kcp.rx_rto
} else {
segment.rto += kcp.rx_rto / 2
}
segment.resendts = current + segment.rto
lost++
lostSegs++
} else if segment.fastack >= resent { // fast retransmit
needsend = true
segment.fastack = 0
segment.rto = kcp.rx_rto
segment.resendts = current + segment.rto
change++
fastRetransSegs++
} else if segment.fastack > 0 && newSegsCount == 0 { // early retransmit
needsend = true
segment.fastack = 0
segment.rto = kcp.rx_rto
segment.resendts = current + segment.rto
change++
earlyRetransSegs++
}

if needsend {
segment.xmit++
segment.ts = current
segment.wnd = seg.wnd
segment.una = seg.una

size := len(buffer) - len(ptr)
need := IKCP_OVERHEAD + len(segment.data)

if size+need > int(kcp.mtu) {
kcp.output(buffer, size)
current = currentMs() // time update for a blocking call
ptr = buffer
}

ptr = segment.encode(ptr)
copy(ptr, segment.data)
ptr = ptr[len(segment.data):]

if segment.xmit >= kcp.dead_link {
kcp.state = 0xFFFFFFFF
}
}
}

// flash remain segments
size := len(buffer) - len(ptr)
if size > 0 {
kcp.output(buffer, size)
}

// counter updates
sum := lostSegs
if lostSegs > 0 {
atomic.AddUint64(&DefaultSnmp.LostSegs, lostSegs)
}
if fastRetransSegs > 0 {
atomic.AddUint64(&DefaultSnmp.FastRetransSegs, fastRetransSegs)
sum += fastRetransSegs
}
if earlyRetransSegs > 0 {
atomic.AddUint64(&DefaultSnmp.EarlyRetransSegs, earlyRetransSegs)
sum += earlyRetransSegs
}
if sum > 0 {
atomic.AddUint64(&DefaultSnmp.RetransSegs, sum)
}

// update ssthresh
// rate halving, https://tools.ietf.org/html/rfc6937
if change > 0 {
inflight := kcp.snd_nxt - kcp.snd_una
kcp.ssthresh = inflight / 2
if kcp.ssthresh < IKCP_THRESH_MIN {
kcp.ssthresh = IKCP_THRESH_MIN
}
kcp.cwnd = kcp.ssthresh + resent
kcp.incr = kcp.cwnd * kcp.mss
}

// congestion control, https://tools.ietf.org/html/rfc5681
if lost > 0 {
kcp.ssthresh = cwnd / 2
if kcp.ssthresh < IKCP_THRESH_MIN {
kcp.ssthresh = IKCP_THRESH_MIN
}
kcp.cwnd = 1
kcp.incr = kcp.mss
}

if kcp.cwnd < 1 {
kcp.cwnd = 1
kcp.incr = kcp.mss
}
}

flush函数非常的重要,kcp的重要参数都是在调节这个函数的行为,这个函数只有一个参数ackOnly,意思就是只发送ack,如果ackOnly为true的话,该函数只遍历ack列表,然后发送,就完事了。
如果不是,也会发送真实数据。
在发送数据前先进行windSize探测,如果开启了拥塞控制nc=0,则每次发送前检测服务端的winsize,如果服务端的winsize变小了,自身的winsize也要更着变小,来避免拥塞。如果没有开启拥塞控制,就按设置的winsize进行数据发送。

接着循环每个段数据,并判断每个段数据的是否该重发,还有什么时候重发:



  1. 如果这个段数据首次发送,则直接发送数据。

  2. 如果这个段数据的当前时间大于它自身重发的时间,也就是RTO,则重传消息。

  3. 如果这个段数据的ack丢失累计超过resent次数,则重传,也就是快速重传机制。这个resent参数由resend参数决定。

  4. 如果这个段数据的ack有丢失且没有新的数据段,则触发ER,ER相关信息ER


最后通过kcp.output发送消息hello,output是个回调函数,函数的实体是sess.go的:


func (s *UDPSession) output(buf []byte) {
var ecc [][]byte

// extend buf's header space
ext := buf
if s.headerSize > 0 {
ext = s.ext[:s.headerSize+len(buf)]
copy(ext[s.headerSize:], buf)
}

// FEC stage
if s.fecEncoder != nil {
ecc = s.fecEncoder.Encode(ext)
}

// encryption stage
if s.block != nil {
io.ReadFull(rand.Reader, ext[:nonceSize])
checksum := crc32.ChecksumIEEE(ext[cryptHeaderSize:])
binary.LittleEndian.PutUint32(ext[nonceSize:], checksum)
s.block.Encrypt(ext, ext)

if ecc != nil {
for k := range ecc {
io.ReadFull(rand.Reader, ecc[k][:nonceSize])
checksum := crc32.ChecksumIEEE(ecc[k][cryptHeaderSize:])
binary.LittleEndian.PutUint32(ecc[k][nonceSize:], checksum)
s.block.Encrypt(ecc[k], ecc[k])
}
}
}

// WriteTo kernel
nbytes := 0
npkts := 0
// if mrand.Intn(100) < 50 {
for i := 0; i < s.dup+1; i++ {
if n, err := s.conn.WriteTo(ext, s.remote); err == nil {
nbytes += n
npkts++
}
}
// }

if ecc != nil {
for k := range ecc {
if n, err := s.conn.WriteTo(ecc[k], s.remote); err == nil {
nbytes += n
npkts++
}
}
}
atomic.AddUint64(&DefaultSnmp.OutPkts, uint64(npkts))
atomic.AddUint64(&DefaultSnmp.OutBytes, uint64(nbytes))
}

output函数才是真正的将数据写入内核中,在写入之前先进行了fec编码,fec编码器的实现是用了一个开源库github.com/klauspost/reedsolomon,编码以后的hello就不是和原来的hello一样了,至少多了几个字节。
fec编码器有两个重要的参数reedsolomon.New(dataShards, parityShards, reedsolomon.WithMaxGoroutines(1)),dataShardsparityShards,这两个参数决定了fec的冗余度,冗余度越大抗丢包性就越强。


kcp的任务调度器


其实这里任务调度器是一个很简单的实现,用一个全局变量updater来管理session,代码文件为updater.go。其中最主要的函数


func (h *updateHeap) updateTask() {
var timer <-chan time.Time
for {
select {
case <-timer:
case <-h.chWakeUp:
}

h.mu.Lock()
hlen := h.Len()
now := time.Now()
if hlen > 0 && now.After(h.entries[0].ts) {
for i := 0; i < hlen; i++ {
entry := heap.Pop(h).(entry)
if now.After(entry.ts) {
entry.ts = now.Add(entry.s.update())
heap.Push(h, entry)
} else {
heap.Push(h, entry)
break
}
}
}
if hlen > 0 {
timer = time.After(h.entries[0].ts.Sub(now))
}
h.mu.Unlock()
}
}

任务调度器实现了一个堆结构,每当有新的连接,session都会插入到这个堆里,接着for循环每隔interval时间,遍历这个堆,得到entry然后执行entry.s.update()。而entry.s.update()会执行s.kcp.flush(false)来发送数据。


总结


这里简单介绍了kcp的整体流程,详细介绍了发送数据的流程,但未介绍kcp接收数据的流程,其实在客户端发送数据后,服务端是需要返回ack的,而客户端也需要根据返回的ack来判断数据段是否需要重传还是在队列里清除该数据段。处理返回来的ack是在函数kcp.Input()函数实现的。具体详细流程下次再介绍。

Golang逃逸分析

文章分享sheepbao 发表了文章 • 0 个评论 • 1921 次浏览 • 2017-06-11 11:56 • 来自相关话题

Golang逃逸分析

介绍逃逸分析的概念,go怎么开启逃逸分析的log。
以下资料来自互联网,有错误之处,请一定告之。
sheepbao 2017.06.10

什么是逃逸分析

w... 查看全部

Golang逃逸分析


介绍逃逸分析的概念,go怎么开启逃逸分析的log。

以下资料来自互联网,有错误之处,请一定告之。

sheepbao 2017.06.10


什么是逃逸分析


wiki上的定义


In compiler optimization, escape analysis is a method for determining the dynamic scope of pointers - where in the program a pointer can be accessed. It is related to pointer analysis and shape analysis.


When a variable (or an object) is allocated in a subroutine, a pointer to the variable can escape to other threads of execution, or to calling subroutines. If an implementation uses tail call optimization (usually required for functional languages), objects may also be seen as escaping to called subroutines. If a language supports first-class continuations (as do Scheme and Standard ML of New Jersey), portions of the call stack may also escape.


If a subroutine allocates an object and returns a pointer to it, the object can be accessed from undetermined places in the program — the pointer has "escaped". Pointers can also escape if they are stored in global variables or other data structures that, in turn, escape the current procedure.


Escape analysis determines all the places where a pointer can be stored and whether the lifetime of the pointer can be proven to be restricted only to the current procedure and/or threa


大概的意思是在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,可以分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。
当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或者去调用子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可能逃逸到被调用的子程序中。
如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方被访问到——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,它们也可能发生逃逸,这种情况是当前程序中的指针逃逸。
逃逸分析需要确定指针所有可以存储的地方,保证指针的生命周期只在当前进程或线程中。


逃逸分析的用处(为了性能)



  • 最大的好处应该是减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。

  • 因为逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好

  • 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。


go消除了堆和栈的区别


go在一定程度消除了堆和栈的区别,因为go在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。


开启go编译时的逃逸分析日志


开启逃逸分析日志很简单,只要在编译的时候加上-gcflags '-m',但是我们为了不让编译时自动内连函数,一般会加-l参数,最终为-gcflags '-m -l'


Example:


package main

import (
"fmt"
)

func main() {
s := "hello"
fmt.Println(s)
}

go run -gcflags '-m -l' escape.go

Output:


# command-line-arguments
escape_analysis/main.go:9: s escapes to heap
escape_analysis/main.go:9: main ... argument does not escape
hello

什么时候逃逸,什么时候不逃逸


Example1:


package main

type S struct{}

func main() {
var x S
y := &x
_ = *identity(y)
}

func identity(z *S) *S {
return z
}

Output:


# command-line-arguments
escape_analysis/main.go:11: leaking param: z to result ~r1 level=0
escape_analysis/main.go:7: main &x does not escape

这里的第一行表示z变量是“流式”,因为identity这个函数仅仅输入一个变量,又将这个变量作为返回输出,但identity并没有引用z,所以这个变量没有逃逸,而x没有被引用,且生命周期也在mian里,x没有逃逸,分配在栈上。


Example2:


package main

type S struct{}

func main() {
var x S
_ = *ref(x)
}

func ref(z S) *S {
return &z
}

Output:


# command-line-arguments
escape_analysis/main.go:11: &z escapes to heap
escape_analysis/main.go:10: moved to heap: z

这里的z是逃逸了,原因很简单,go都是值传递,ref函数copy了x的值,传给z,返回z的指针,然后在函数外被引用,说明z这个变量在函数內声明,可能会被函数外的其他程序访问。所以z逃逸了,分配在堆上


对象里的变量会怎么样呢?看下面


Example3:


package main

type S struct {
M *int
}

func main() {
var i int
refStruct(i)
}

func refStruct(y int) (z S) {
z.M = &y
return z
}

Output:


# command-line-arguments
escape_analysis/main.go:13: &y escapes to heap
escape_analysis/main.go:12: moved to heap: y

看日志的输出,这里的y是逃逸了,看来在struct里好像并没有区别,有可能被函数外的程序访问就会逃逸


Example4:


package main

type S struct {
M *int
}

func main() {
var i int
refStruct(&i)
}

func refStruct(y *int) (z S) {
z.M = y
return z
}

Output:


# command-line-arguments
escape_analysis/main.go:12: leaking param: y to result z level=0
escape_analysis/main.go:9: main &i does not escape

这里的y没有逃逸,分配在栈上,原因和Example1是一样的。


Example5:


package main

type S struct {
M *int
}

func main() {
var x S
var i int
ref(&i, &x)
}

func ref(y *int, z *S) {
z.M = y
}

Output:


# command-line-arguments
escape_analysis/main.go:13: leaking param: y
escape_analysis/main.go:13: ref z does not escape
escape_analysis/main.go:10: &i escapes to heap
escape_analysis/main.go:9: moved to heap: i
escape_analysis/main.go:10: main &x does not escape

这里的z没有逃逸,而i却逃逸了,这是因为go的逃逸分析不知道z和i的关系,逃逸分析不知道参数y是z的一个成员,所以只能把它分配给堆。


参考


Go Escape Analysis Flaws

go-escape-analysis

GOLANG最容易做测试MOCK

技术讨论winlin 发表了文章 • 0 个评论 • 630 次浏览 • 2017-06-09 18:26 • 来自相关话题

测试时,一些底层的库非常难以MOCK,比如HASH摘要算法,怎么MOCK?假设有个函数,是用MD5做摘要:

func digest(data []byte, h ha... 			查看全部
					

测试时,一些底层的库非常难以MOCK,比如HASH摘要算法,怎么MOCK?假设有个函数,是用MD5做摘要:


func digest(data []byte, h hash.Hash) ([]byte, error) {
if _, err = h.Write(data); err != nil {
return nil, errors.Wrap(err, "hash write")
}

d := h.Sum(nil)
if len(d) != 16 {
return nil, errors.Errorf("digest's %v bytes", len(d))
}
return d,nil
}

难以覆盖的因素有几个:



  1. 私有函数,一般其他语言在utest中只能访问public函数,而golang的utest是和目标在同一个package,所有函数和数据都可以访问。

  2. 有些函数非常难以出错,但是不代表不出错,比如这里的Write方法,一般都是不会有问题的,但是测试如果覆盖不到,保不齐哪天跑到这一行就挂掉了。

  3. MOCK桩对象或者函数,如果总是要把目标全部实现一遍,比如hash这个接口有5个方法,对Write打桩时只需要覆盖这个函数,其他的可以不动。是的,聪明的你可能会想到继承,但是如果这个类是隐藏的呢?比如一个md5的实现是隐藏不能访问的,暴露的只有hash的接口,怎么从md5这个类继承呢?GOLANG提供了类似从实现了接口对象的接口继承的方式,实际上是组合,具体看下面的实现。

  4. 有些古怪的逻辑,比如这里判断摘要是16字节,一般情况下也不会出现错误,当然utest也必须得覆盖到,万一哪天用了一个hash算法跑到这个地方,不能出现问题。



Remark: 注意到这个地方用了一个errors的package,它可以打印出问题出现的堆栈,参考Error最佳实践.



用GOLANG就可以完美解决上面所有的覆盖问题,先上代码:


type mockMD5Write struct {
hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
return 0,fmt.Errorf("mock md5")
}

就这么简单?对的,但是不要小看这几行代码,深藏功与名~


组合接口


结构体mockMD5Write里面嵌套的不是实现md5哈希的类,而是直接嵌套的hash.Hash接口。这个有什么厉害的呢?假设用C++,看应该怎么搞:


class Hash {
public: virtual int Write(const char* data, int size) = 0;
public: virtual int Sum(const char* data, int size, char digest[16]) = 0;
public: virtual int Size() = 0;
};

class MD5 : public Hash {
// 省略了实现的代码
}

class mockMD5Write : public Hash {
private: Hash* imp;
public: mockMD5Write(Hash* v) {
imp = v;
}
public: int Write(const char* data, int size) {
return 100; // 总是返回个错误。
}
};

是么?错了,mockMD5Write编译时会报错,会提示没有实现其他的接口。应该这么写:


class mockMD5Write : public Hash {
private: Hash* imp;
public: mockMD5Write(Hash* v) {
imp = v;
}
public: int Write(const char* data, int size) {
return 100; // 总是返回个错误。
}
public: int Sum(const char* data, int size, char digest[16]) {
return imp->Sum(data, size, digest);
}
public: int Size() {
return imp->Size();
}
};

对比下够浪的接口组合,因为组合了一个hash.Hash的接口,所以它也就默认实现了,不用再把函数代理一遍了:


type mockMD5Write struct {
hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
return 0,fmt.Errorf("mock md5")
}

这个可不是少写了几行代码的区别,这是本质的区别,我鸡冻的辩解道~如果这个接口有十个函数,我们要测试100个接口呢?这个MOCK该怎么写?另外,这个实际上是OO和GOLANG的细微差异,GOLANG的接口是契约,只要满足就可以,面向的全是动作,GOLANG像很多函数组合,它没有类体系的概念,也就是它的结构体不用明显符合哪个接口和哪个接口它才是合法的,实际上它可以符合任何适配的接口,也就是Die()这个动作,是自动被所有会Die的对象适配了的,不用显式声明自己会Die,关注的不是声明和实现了接口的关系,而是关注动作或者说接口本身,!@#$%^&*()$%^&*(#$%^&*#$^&不能说了,说多了都懂了我还怎么装逼去~


复杂错误


我们用了errors这个包,用来返回复杂错误,可以看到堆栈信息,对于utest也是一样,能看到堆栈对于解决问题也很重要。可以参考Error最佳实践。比如打印信息:


--- FAIL: TestDigest (0.00s)
digest_test.go:45: digest, mock md5
hash write data
_/Users/winlin/git/test/utility.digest
/Users/winlin/git/test/utility.go:46
_/Users/winlin/git/test/TestDigest
/Users/winlin/git/test/digest_test.go:42
testing.tRunner
/usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

测试代码:


func TestDigest(t *testing.T) {
if _, err := digest(nil, &mockMD5Write{md5.New()}); err == nil {
t.Error("should failed")
} else {
t.Errorf("digest, %+v", err)
}
}

当然这个地方是主动把error打印出来,因为用例就是应该要返回错误的,一般情况是:


func TestXXX(t *testing.T) {
if err := pfn(); err != nil {
t.Errorf("failed, %+v", err)
}
}

这样就可以知道堆栈了。

GOLANG空指针崩溃时堆栈消失和解决方案

技术讨论winlin 发表了文章 • 6 个评论 • 576 次浏览 • 2017-06-07 17:04 • 来自相关话题

错误处理这个文章中,tkk提出了空指针时堆栈消失的问题,看下面的查看全部

错误处理这个文章中,tkk提出了空指针时堆栈消失的问题,看下面的例子


package main

func main() {
run() // line 4
}
func run() {
causedPanic()
}
func causedPanic() {
//defer func() {}() // line 10
//panic("Panic from user") // line 11
var p *byte
*p = 0 // line 13
}

这个程序崩溃时,打印的竟然是:


panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
main.main()
/tmp/sandbox277759147/main.go:4 +0x4

神奇的是,把第10行的defer打开,变成这样


func causedPanic() {
defer func() {}() // line 10
//panic("Panic from user") // line 11
var p *byte
*p = 0 // line 13
}

堆栈神奇的回来了:


panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
main.causedPanic()
/tmp/sandbox416089181/main.go:13 +0x48
main.run()
/tmp/sandbox416089181/main.go:7 +0x20
main.main()
/tmp/sandbox416089181/main.go:4 +0x20

而主动调用panic堆栈也是没有问题的,可以把第10行注释掉,同时打开第11行。这个问题确实很诡异,在go-nuts中发了一篇文章问,strange stack trace when panic,马上就有神回复了:


On Wednesday, June 7, 2017 at 4:25:35 PM UTC+8, Dave Cheney wrote:

Try building your program with -gcflags="-l" to disable inlining.
If that restores the stacktrace, then it's inlining.
The good news is this should be fixed with Go 1.9

果然,运行时加上这个参数(编译时加上也是可以的),禁用内联编译后,堆栈就回来了:


go run -gcflags="-l" t.go

难怪了,主动调用panic时,内联编译不会把函数怼一坨去,如果没有defer和panic这种函数,就可能把函数怼一坨,看起来像是一个函数,堆栈消失了,这样在空指针时就找不到堆栈信息。


解决方案:



  1. 编译时加参数-gcflags="-l"

  2. 可能在GO1.9会解决这个问题。


结贴。

GOLANG错误处理最佳方案

技术讨论winlin 发表了文章 • 23 个评论 • 694 次浏览 • 2017-06-05 10:05 • 来自相关话题

GOLANG的错误很简单的,用error接口,参考golang error handling:

查看全部
					

GOLANG的错误很简单的,用error接口,参考golang error handling:


if f,err := os.Open("test.txt"); err != nil {
return err
}

实际上如果习惯于C返回错误码,也是可以的,定义一个整形的error:


type errorCode int
func (v errorCode) Error() string {
return fmt.Sprintf("error code is %v", v)
}

const loadFailed errorCode = 100

func load(filename string) error {
if f,err := os.Open(filename); err != nil {
return loadFailed
}
defer f.Close()

content : = readFromFile(f);
if len(content) == 0 {
return loadFailed
}

return nil
}

这貌似没有什么难的啊?实际上,这只是error的基本单元,在实际的产品中,比如有个播放器会打印一个这个信息:


Player: Decode failed.

对的,就只有这一条信息,然后呢?就没有然后了,只知道是解码失败了,没有任何的线索,必须得调试播放器才能知道发生了什么。看我们的例子,如果load失败,也是一样的,只会打印一条信息:


error code is 100

这些信息是不够的,这是一个错误库很流行的原因,这个库是errors,它提供了一个Wrap方法:


_, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "read failed")
}

也就是加入了多个error,如果用这个库,那么上面的例子该这么写:


func load(filename string) error {
if f,err := os.Open(filename); err != nil {
return errors.Wrap(err, "open failed")
}
defer f.Close()

content : = readFromFile(f);
if len(content) == 0 {
return errors.New("content empty")
}

return nil
}

这个库给每个error可以加上额外的消息errors.WithMessage(err,msg),或者加上堆栈信息errors.WithStack(err),或者两个都加上erros.Wrap, 或者创建带堆栈信息的错误errors.Newerrors.Errorf。这样在多层函数调用时,就有足够的信息可以展现当时的情况了。


在多层函数调用中,甚至可以每层都加上自己的信息,例如:


func initialize() error {
if err := load("sys.db"); err != nil {
return errors.WithMessage(err, "init failed")
}

if f,err := os.Open("sys.log"); err != nil {
return errors.Wrap(err, "open log failed")
}
return nil
}

init函数中,调用load时因为这个err已经被Wrap过了,所以就只是加上自己的信息(如果用Wrap会导致重复的堆栈,不过也没有啥问题的了)。第二个错误用Wrap加上信息。打印日志如下:


empty content
main.load
/Users/winlin/git/test/src/demo/test/main.go:160
main.initialize
/Users/winlin/git/test/src/demo/test/main.go:167
main.main
/Users/winlin/git/test/src/demo/test/main.go:179
runtime.main
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197
load sys.db failed

这样就可以知道是加载sys.db时候出错,错误内容是empty content,堆栈也有了。遇到错误时,会非常容易解决问题。


例如,AAC的一个库,用到了ASC对象,在解析时需要判断是否数据合法,实现如下(参考code):


func (v *adts) Decode(data []byte) (raw, left []byte, err error) {
p := data
if len(p) <= 7 {
return nil, nil, errors.Errorf("requires 7+ but only %v bytes", len(p))
}

// Decode the ADTS.

if err = v.asc.validate(); err != nil {
return nil, nil, errors.WithMessage(err, "adts decode")
}
return
}

func (v *AudioSpecificConfig) validate() (err error) {
if v.Channels < ChannelMono || v.Channels > Channel7_1 {
return errors.Errorf("invalid channels %#x", uint8(v.Channels))
}
return
}

在错误发生的最原始处,加上堆栈,在外层加上额外的必要信息,这样在使用时发生错误后,可以知道问题在哪里,写一个实例程序:


func run() {
adts,_ := aac.NewADTS()
if _,_,err := adts.Decode(nil); err != nil {
fmt.Println(fmt.Sprintf("Decode failed, err is %+v", err))
}
}

func main() {
run()
}

打印详细的堆栈:


Decode failed, err is invalid object 0x0
github.com/ossrs/go-oryx-lib/aac.(*AudioSpecificConfig).validate
/Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:462
github.com/ossrs/go-oryx-lib/aac.(*adts).Decode
/Users/winlin/go/src/github.com/ossrs/go-oryx-lib/aac/aac.go:439
main.run
/Users/winlin/git/test/src/test/main.go:13
main.main
/Users/winlin/git/test/src/test/main.go:19
runtime.main
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/proc.go:185
runtime.goexit
/usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197
adts decode

错误信息包含:



  1. adts decode,由ADTS打印出。

  2. invalid object 0x00,由ASC打印出。

  3. 完整的堆栈,包含main/run/aac.Decode/asc.Decode


如果这个信息是客户端的,发送到后台后,非常容易找到问题所在,比一个简单的Decode failed有用太多了,有本质的区别。如果是服务器端,那还需要加上上下文关于连接的信息,区分出这个错误是哪个连接造成的,也非常容易找到问题。


加上堆栈会不会性能低?错误出现的概率还是比较小的,几乎不会对性能有损失。使用复杂的error对象,就可以在库中避免用logger,在应用层使用logger打印到文件或者网络中。


对于其他的语言,比如多线程程序,也可以用类似方法,返回int错误码,但是把上下文信息保存到线程的信息中,清理线程时也清理这个信息。对于协程也是一样的,例如ST的thread也可以拿到当前的ID,利用全局变量保存信息。对于goroutine这种拿不到协程ID,可以用context.Context,实际上最简单的就是在error中加入上下文,因为Context要在1.7之后才纳入标准库。


一个C++的例子,得借助于宏定义:


struct ComplexError {
int code;
ComplexError* wrapped;
string msg;

string func;
string file;
int line;
};

#define errors_new(code, fmt, ...) \
_errors_new(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__)
extern ComplexError* _errors_new(const char* func, const char* file, int line, int code, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
char buffer[1024];
size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);

ComplexError* err = new ComplexError();
err->code = code;
err->func = func;
err->file = file;
err->line = line;
err->msg.assign(buffer, size);
return err;
}

#define errors_wrap(err, fmt, ...) \
_errors_wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__)
extern ComplexError* _errors_wrap(const char* func, const char* file, int line, ComplexError* v, const char* fmt, ...) {
ComplexError* wrapped = (ComplexError*)v;

va_list ap;
va_start(ap, fmt);
char buffer[1024];
size_t size = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);

ComplexError* err = new ComplexError();
err->wrapped = wrapped;
err->code = wrapped->code;
err->func = func;
err->file = file;
err->line = line;
err->msg.assign(buffer, size);
return err;
}

使用时,和GOLANG有点类似:


ComplexError* loads(string filename) {
if (filename.empty()) {
return errors_new(100, "invalid file");
}
return NULL;
}
ComplexError* initialize() {
string filename = "sys.db";
ComplexError* err = loads(filename);
if (err) {
return errors_wrap("load system from %s failed", filename.c_str());
}
return NULL;
}
int main(int argc, char** argv) {
ComplexError* err = initialize();
// Print err stack.
return err;
}

比单纯一个code要好很多,错误发生的概率也不高,获取详细的信息比较好。


另外,logger和error是两个不同的概念,比如对于library,错误时用errors返回复杂的错误,包含丰富的信息,但是logger一样非常重要,比如对于某些特定的信息,access log能看到客户端的访问信息,还有协议一般会在关键的流程点加日志,说明目前的运行状况,此外,还可以有json格式的日志或者叫做消息,可以把这些日志发送到数据系统处理。


对于logger,支持context.Context就尤其重要了,实际上context就是一次会话比如一个http request的请求的处理过程,或者一个RTMP的连接的处理。一个典型的logger的定义应该是:


// C++ style
logger(int level, void* ctx, const char* fmt, ...)
// GOLANG style
logger(level:int, ctx:context.Context, format string, args ...interface{})

这样在文本日志,或者在消息系统中,就可以区分出哪个会话。当然在error中也可以包含context的信息,这样不仅仅可以看到出错的错误和堆栈,还可以看到之前的重要的日志。还可以记录线程信息,对于多线程和回调函数,可以记录堆栈:


[2017-06-08 09:44:10.815][Error][54417][100][60] Main: Run, code=1015 : run : callback : cycle : api=http://127.0.0.1:8080, url=rtmp://localhost/live/livestream, token=16357216378262183 : parse json={"code":0,"data":{"servers":["127.0.0.1:1935"]}} : no data.key
thread #122848: run() [src/test/main.cpp:303][errno=60]
thread #987592: do_callback() [src/test/main.cpp:346][errno=36]
thread #987592: cycle() [src/sdk/test.cpp:3332][errno=36]
thread #987592: do_cycle() [src/sdk/test.cpp:3355][errno=36]
thread #987592: gslb() [src/sdk/test.cpp:2255][errno=36]
thread #987592: gslb_parse() [src/sdk/test.cpp:2284][errno=36]

当然,在ComplexError中得加入uint64_t trdint rerrno,然后new和wrap时赋值就好了。

Go 动态汇总(持续跟进)

文章分享bingohuang 发表了文章 • 0 个评论 • 385 次浏览 • 2017-06-02 17:22 • 来自相关话题

依托 Go team 的 『State of Go』talks,持续跟进 Go 的动态.

Go 动态列表如下:

日期 | 作者 |... 查看全部


依托 Go team 的 『State of Go』talks,持续跟进 Go 的动态.



Go 动态列表如下:


日期 | 作者 | 视频 | 演讲稿


Go1.8|2017年5月 | Francesc Campoy | video | slides


Go1.7-Go1.8|2017年2月 | Francesc Campoy | video | slides


Go1.6|2016年2月 | Francesc Campoy | video | slides


Go1.5|2015年5月 | Andrew Gerrand | video | slides


Go1.4-Go1.5|2015年2月 | Andrew Gerrand | video | slides


Gopher|2014年10月 | Brad Fitzpatrick | video | slides


Go1.3-Go1.4|2014年6月 | Andrew Gerrand | video | slides


Go1.2-Go1.3|2014年2月 | Andrew Gerrand | video | slides


Go1.1|2013年5月 | Andrew Gerrand | 无 | slides


Go1|2012年7月 | Rob Pike & Andrew Gerrand | video | slides


将持续跟进最新的 『State of Go』 talks,更新列表,敬请关注



转自:http://bingohuang.com/the-state-of-go/


gRPC负载均衡的正确姿势

开源程序stirlingx 发表了文章 • 6 个评论 • 535 次浏览 • 2017-06-01 19:39 • 来自相关话题

因为官方的grpc-go客户端只实现了一个RoundRobin load balancer,所以写了一个库,增加了Random、ketama策略,并可以很方便的增... 查看全部

因为官方的grpc-go客户端只实现了一个RoundRobin load balancer,所以写了一个库,增加了Random、ketama策略,并可以很方便的增加新的策略。


项目地址: https://github.com/liyue201/grpc-lb