分享一个质量很高的IT行业项目资源渠道平台

回复

huixinyunit 发起了问题 • 1 人关注 • 0 个回复 • 25 次浏览 • 2 小时前 • 来自相关话题

nxlog4go Simplest: Using for Testing Environment

ccpaging 发表了文章 • 0 个评论 • 76 次浏览 • 4 天前 • 来自相关话题

nxlog4go is very simple to use without any configuring, setting. For example:

查看全部
					

nxlog4go is very simple to use without any configuring, setting. For example:



package main

import (
"time"
log "github.com/ccpaging/nxlog4go"
)

func main() {
log.Fine("This should be omitted as default.")
log.Debug("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
log.Info("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
log.Warn("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
log.Critical("The time is now: %s", time.Now().Format("15:04:05 MST 2006/01/02"))
}

Compatibility with go log


The most programmer always use go log for testing. nxlog4go aimed the replacement of go log.
































Log Function Level After
Crash, Crashf CRITICAL, panic panic
Exit, Exitf ERROR, exit exit
Stderr, Stderrf ERROR
Stdout, Stdoutf INFO

New log functions




















































Log Function Level Default
Finest(...) 0 Omit
Fine(...) 1 Omit
Debug(...) 2
Trace(...) 3
Info(...) 4
Warn(...) 5
Error(...) 6
Critical(...) 7

Set display level


For example:


    log.GetLogger().SetLevel(log.FINE)
log.Fine("This should be not omitted now.")

log.GetLogger() return the point of default Global Logger.


SetLevel(log.FINE) sets level to log.FINE.

Go语言基础,想要了解的可以阅读下

liyongjing 发表了文章 • 0 个评论 • 206 次浏览 • 2018-02-13 18:53 • 来自相关话题

Go 语言内置类型研读


编程语言底层那些事儿


内容太干,想要了解的可以阅读下

再测Golang的JSON库

qiangmzsx 发表了文章 • 3 个评论 • 298 次浏览 • 2018-02-11 18:56 • 来自相关话题

写项目一直需要进行序列化,听到了,也看到了很多同学老师对各个golang的json库进行测评。那本人为什么还要继续进行这一次测评呢? 因为实践过的知识最有说服力,也是属于自己的,我也希望看到本博文的同学老师可以修改和执行测评的代... 查看全部

写项目一直需要进行序列化,听到了,也看到了很多同学老师对各个golang的json库进行测评。那本人为什么还要继续进行这一次测评呢?
因为实践过的知识最有说服力,也是属于自己的,我也希望看到本博文的同学老师可以修改和执行测评的代码执行一遍,我相信会有不一定的体会。
本次测评我选择了类库有:


类库





































序号 类库 地址 备注
1 encoding/json Golan
2 easyjson github.com/mailru/easyjson
3 ffjson github.com/mailru/easyjson
4 iterator/json github.com/json-iterator/go

主要是针对上述的类型进行,本人采用了对不同的类库使用不同的结构体(仅仅是结构体名称不同,字段顺序和类型一样)。


环境


环境为MacBook Pro(Core i5处理器/8GB内存)go1.8.3 darwin/amd64


代码


bench代码如下:


package jsonbench

import (
"encoding/gob"

"encoding/json"
"github.com/json-iterator/go"
"github.com/mailru/easyjson"
"github.com/pquerna/ffjson/ffjson"
"testing"
)

var (
iterator = jsoniter.ConfigCompatibleWithStandardLibrary
// easyjson
as = AgentService{
ServiceName: "kaleidoscope_api",
Version: "1517558949087295000_1298498081",
ServiceId: "kaleidoscope.com_v1.2",
Address: "127.0.0.1",
Port: 80,
Metadata: map[string]string{},
ConnectTimeOut: 1000,
ConnectType: "LONG",
ReadTimeOut: 1000,
WriteTimeOut: 1000,
Protocol: "HTTP",
Balance: "Random",
Idcs: "hu,hd,hn",
Converter: "json",
Retry: 3,
}
service = as.ToService()
asBytes, _ = json.Marshal(as)
serviceBytes, _ = json.Marshal(service)
asStr = string(asBytes)
serviceStr = string(serviceBytes)
asGonBytes, _ = GobEncode(as)
serviceGonBytes, _ = GobEncode(service)
// std
asstd = AgentServiceSTD{
ServiceName: "kaleidoscope_api",
Version: "1517558949087295000_1298498081",
ServiceId: "kaleidoscope.com_v1.2",
Address: "kaleidoscope.dev.igetget.com",
Port: 80,
Metadata: map[string]string{},
ConnectTimeOut: 1000,
ConnectType: "LONG",
ReadTimeOut: 1000,
WriteTimeOut: 1000,
Protocol: "HTTP",
Balance: "Random",
Idcs: "hu,hd,hn",
Converter: "json",
Retry: 3,
}
servicestd = asstd.ToServiceSTD()
asBytesstd, _ = json.Marshal(asstd)
serviceBytesstd, _ = json.Marshal(servicestd)
asStrstd = string(asBytesstd)
serviceStrstd = string(serviceBytesstd)
asGonBytesstd, _ = GobEncode(asstd)
serviceGonBytesstd, _ = GobEncode(servicestd)
)

// go test -bench=".*"
func init() {
gob.Register(AgentService{})
}

func Benchmark_STD_Marshal1(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := json.Marshal(asstd)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_STD_Marshal2(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := json.Marshal(servicestd)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_STD_Marshal1(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := json.Marshal(as)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_STD_Marshal2(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := json.Marshal(service)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_Marshal1(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := easyjson.Marshal(as)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_Marshal2(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := easyjson.Marshal(service)
if err != nil {
b.Error(err)
}
}
}

//
func Benchmark_ITERATOR_Marshal1(b *testing.B) {

for i := 0; i < b.N*10; i++ {
_, err := iterator.Marshal(asstd)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_ITERATOR_Marshal2(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := iterator.Marshal(servicestd)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_FFJSON_Marshal1(b *testing.B) {

for i := 0; i < b.N*10; i++ {
_, err := ffjson.Marshal(asstd)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_FFJSON_Marshal2(b *testing.B) {
for i := 0; i < b.N*10; i++ {
_, err := ffjson.Marshal(servicestd)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_GOB_Encode1(b *testing.B) {
for i := 0; i < b.N*10; i++ {
as.Port = i
GobEncode(as)
}
}

func Benchmark_GOB_Encode2(b *testing.B) {
for i := 0; i < b.N*10; i++ {
GobEncode(service)
}
}

func Benchmark_STD_Unmarshal1(b *testing.B) {
tmp := AgentServiceSTD{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := json.Unmarshal(asBytesstd, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_STD_Unmarshal2(b *testing.B) {
tmp := ServiceSTD{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := json.Unmarshal(serviceBytesstd, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_STD_Unmarshal1(b *testing.B) {
tmp := AgentService{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := json.Unmarshal(asBytes, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_STD_Unmarshal2(b *testing.B) {
tmp := Service{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := json.Unmarshal(serviceBytes, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_Unmarshal1(b *testing.B) {
tmp := AgentService{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := easyjson.Unmarshal(asBytes, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_EASYJSON_Unmarshal2(b *testing.B) {
tmp := Service{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := easyjson.Unmarshal(serviceBytes, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_ITERATOR_UnMarshal1(b *testing.B) {

tmp := ServiceSTD{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := iterator.Unmarshal(serviceBytesstd, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_ITERATOR_UnMarshal2(b *testing.B) {
tmp := ServiceSTD{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := iterator.Unmarshal(serviceBytesstd, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_FFJSON_UnMarshal1(b *testing.B) {

tmp := ServiceSTD{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := ffjson.Unmarshal(serviceBytesstd, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_FFJSON_UnMarshal2(b *testing.B) {
tmp := ServiceSTD{}
for i := 0; i < b.N*10; i++ {
as.Port = i
err := ffjson.Unmarshal(serviceBytesstd, &tmp)
if err != nil {
b.Error(err)
}
}
}

func Benchmark_GOB_Decode1(b *testing.B) {
tmp := AgentService{}
for i := 0; i < b.N*10; i++ {
as.Port = i
GobDecode(asGonBytes, &tmp)
}
}

func Benchmark_GOB_Decode2(b *testing.B) {
tmp := Service{}
for i := 0; i < b.N*10; i++ {
as.Port = i
GobDecode(serviceGonBytes, &tmp)
}
}

执行命令:


go test -bench=".*"

测评结果;


$ go test -bench=".*"
Benchmark_STD_Marshal1-4 50000 31224 ns/op
Benchmark_STD_Marshal2-4 30000 49598 ns/op
Benchmark_EASYJSON_STD_Marshal1-4 30000 45778 ns/op
Benchmark_EASYJSON_STD_Marshal2-4 30000 50440 ns/op
Benchmark_EASYJSON_Marshal1-4 100000 14387 ns/op
Benchmark_EASYJSON_Marshal2-4 100000 16009 ns/op
Benchmark_ITERATOR_Marshal1-4 100000 14899 ns/op
Benchmark_ITERATOR_Marshal2-4 100000 21629 ns/op
Benchmark_FFJSON_Marshal1-4 50000 31633 ns/op
Benchmark_FFJSON_Marshal2-4 30000 51668 ns/op
Benchmark_GOB_Encode1-4 20000 97099 ns/op
Benchmark_GOB_Encode2-4 10000 153158 ns/op
Benchmark_STD_Unmarshal1-4 20000 89211 ns/op
Benchmark_STD_Unmarshal2-4 20000 76442 ns/op
Benchmark_EASYJSON_STD_Unmarshal1-4 30000 57695 ns/op
Benchmark_EASYJSON_STD_Unmarshal2-4 20000 66269 ns/op
Benchmark_EASYJSON_Unmarshal1-4 100000 19028 ns/op
Benchmark_EASYJSON_Unmarshal2-4 100000 22035 ns/op
Benchmark_ITERATOR_UnMarshal1-4 50000 35942 ns/op
Benchmark_ITERATOR_UnMarshal2-4 50000 36462 ns/op
Benchmark_FFJSON_UnMarshal1-4 20000 80290 ns/op
Benchmark_FFJSON_UnMarshal2-4 20000 78431 ns/op
Benchmark_GOB_Decode1-4 3000 377698 ns/op
Benchmark_GOB_Decode2-4 3000 463472 ns/op
PASS
ok studygo/jsonbench 49.174s

结论



  1. 哪一个类库最快?

    答:是测评类库中最快的。速度:easyjson => iterator => encoding/json => ffjson

  2. 是否存在坑?

    答:easyjson有一个坑,从代码中可以看到Benchmark_EASYJSON_STD_*的方法,是因为easyjson生成的代码中已经包含了MarshalJSONUnmarshalJSON方法,那么只要对这些结构体执行json.marshalJSONjson.UnmarshalJSON都会默认调用easyjson生成的方法。本人运行多次,都会发现调用easyjson生成的MarshalJSON方法比标准库中的慢一些达到50%左右,但是调用easyjson生成的UnmarshalJSON比标准库的快一些大概20%。

  3. 如何选择?

    答:easyjson速度虽然比较快,但也是存在一些不适合的场景,比如如果需要对interface接口进行序列化时候。所以建议采用easyjson与标准库结合。

Go 内嵌静态文件工具 packr

xfstart07 发表了文章 • 0 个评论 • 437 次浏览 • 2018-02-09 18:38 • 来自相关话题

[Go] Go 内嵌静态文件工具 packr

介绍一个简单实用的 Go 内嵌静态文件工具 packr。

安装

$ go get -... 			查看全部
					

[Go] Go 内嵌静态文件工具 packr


介绍一个简单实用的 Go 内嵌静态文件工具 packr。


安装


$ go get -u github.com/gobuffalo/packr/...

使用


使用 packr 打包静态文件非常简单,通过创建一个 box.


// templates 是相对路径,例子是在同一个目录下
box := packr.NewBox("./templates")

// 以字符串的形式获取静态文件
html := box.String("index.html")
fmt.Println("String获取:", html)

html, err := box.MustString("index.html")
if err != nil {
log.Fatal(err)
}
fmt.Println("MustString获取", html)

// 以字节数组的形式获取静态文件
htmlByte := box.Bytes("index.html")
fmt.Println("Bytes: ", htmlByte)
// 对应的还有 MustBytes 方法

packr 在查找文件时的解析规则:



  • 在二进制文件中,在内存中查找文件

  • 开发时,在本地查找文件


在 HTTP 中使用


因为 box 实现了 http.FileSystem 接口,所有可以直接用来提供静态文件访问


package main

import (
"net/http"

"github.com/gobuffalo/packr"
)

func main() {
box := packr.NewBox("./templates")

http.Handle("/", http.FileServer(box))
http.ListenAndServe(":3000", nil)
}

使用 gorilla/mux 作为路由库,mux 也有提供静态文件访问的方式


r := mux.NewRouter()

box := packr.NewBox("./css")
r.PathPrefix("/css").Handler(http.StripPrefix("/css", http.FileServer(box)))

在渲染库(render)中使用


使用 unrolled/render 库来渲染资源,在初始化渲染选项中,将静态文件加入到 Asset 中。


var boxTemp = packr.NewBox("../templates")
var ren = render.New(render.Options{
Directory: "templates",
Asset: func(name string) ([]byte, error) {
// 返回指定路径名称的文件资源
return boxTemp.Bytes("index.html"), nil
},
AssetNames: func() []string {
// 静态文件的路径名称
return []string{"templates/index.html"}
},
Extensions: []string{".html"},
})

使用


ren.HTML(w, http.StatusOK, "index.html", "")

打包命令


使用命令建立二进制文件


packr build 包括了 go build


packr install 包括了 go install


还可以使用 go generate 命令来生成静态资源文件(.go),


package main

// 打包静态文件命令,生成 packr.go 文件
//go:generate packr

func main() {
Run()
}

然后运行命令


go generate && go build

资源


https://github.com/gobuffalo/packr


https://github.com/unrolled/render


https://github.com/gorilla/mux

golang80行代码钉钉群机器人订阅百度新闻

jjjjerk 发表了文章 • 0 个评论 • 196 次浏览 • 2018-02-09 17:54 • 来自相关话题

1. 资料


1.1.第三方包


1.2.接口


2. 初始化项目变量


package main

import (
"fmt"
"log"
"github.com/PuerkitoBio/goquery"
"github.com/go-redis/redis"
"net/http"
"bytes"
"github.com/astaxie/beego/toolbox"
)

var (
redisClient *redis.Client //redis 缓存
//钉钉群机器人webhook地址
dingdingURL = "https://oapi.dingtalk.com/robot/send?access_token=dingding_talk_group_bot_webhook_token"
//百度新闻搜索关键字URL
baiduNewsUrlWithSearchKeyword = "http://news.baidu.com/ns?cl=2&rn=20&tn=news&word=%E7%89%A9%E8%81%94%E7%BD%91"
)

const (
newsFeed = "news_feed"//爬取到的百度新闻redis key
newsPost = "news_post"//已发送的百度新闻redis key
newsList = "iot_news" //储存了的百度新闻redis key
)
//实例化redis缓存
func init() {
redisClient = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "ddfrfgtre4353252", // redis password
DB: 0, // redis 数据库ID
})
}

在机器人管理页面选择“自定义”机器人,输入机器人名字并选择要发送消息的群。如果需要的话,可以为机器人设置一个头像。点击“完成添加”。


点击“复制”按钮,即可获得这个机器人对应的Webhook地址,赋值给 dingdingURl


3 func newsBot


3.1 使用goquery和网页元素选择器语法提取有用信息

func newsBot() error {
// 获取html doc
doc, err := goquery.NewDocument(baiduNewsUrlWithSearchKeyword)
if err != nil {
return nil
}
//使用redis pipeline 减少redis连接数
pipe := redisClient.Pipeline()
// 使用selector xpath 语法获取有用信息
// 储存新闻到redis中 newsList
// 储存新闻ur到redis-set 建newfeed 为以后是用sdiff 找出没有发送的新闻

doc.Find("div.result").Each(func(i int, s *goquery.Selection) {
// For each item found, get the band and title
URL, _ := s.Find("h3 > a").Attr("href")
Source := s.Find("p.c-author").Text()
Title := s.Find("h3 > a").Text()
markdown := fmt.Sprintf("- [%s](%s) _%s_", Title, URL, Source)
pipe.HSet(newsList, URL, markdown)
pipe.SAdd(newsFeed, URL)
})
//执行redis pipeline
pipe.Exec()

3.2 排除以发送的新闻,拼接markdown字符串

        //使用redis sdiff找出没有发送的新闻url
unSendNewsUrls := redisClient.SDiff(newsFeed, newsPost).Val()
//新闻按dingding文档markdonw 规范拼接

content := ""
for _, url := range unSendNewsUrls {
md := redisClient.HGet(newsList, url).Val()
content = content + " \n " + md
//记录已发送新闻的url地址
pipe.SAdd(newsPost, url)
}
pipe.Exec()

3.3 调用钉钉群机器人接口

        //如果有未发送新闻 请求钉钉webhook
if content != "" {
formt := `
{
"msgtype": "markdown",
"markdown": {
"title":"IOT每日新闻",
"text": "%s"
}
}`
body := fmt.Sprintf(formt, content)
jsonValue := []byte(body)
//发送消息到钉钉群使用webhook
//钉钉文档 https://open-doc.dingtalk.com/ ... e%3D1
resp, err := http.Post(dingdingURL, "application/json", bytes.NewBuffer(jsonValue))
if (err != nil) {
return err
}
log.Println(resp)
}
return nil
}

func newsBot函数完成


4. 设置定时任务


func main() {
//销毁redisClient
defer redisClient.Close()

//创建定时任务
//每天 8点 13点 18点 自动执行爬虫和机器人
//
dingdingNewsBot := toolbox.NewTask("dingding-news-bot", "0 0 8,13,18 * * *", newsBot)
//dingdingNewsBot := toolbox.NewTask("dingding-news-bot", "0 40 */1 * * *", newsBot)
//err := dingdingNewsBot.Run()
//检测定时任务
// if err != nil {
// log.Fatal(err)
// }
//添加定时任务
toolbox.AddTask("dingding-news-bot", dingdingNewsBot)
//启动定时任务
toolbox.StartTask()
defer toolbox.StopTask()
select {}
}


spec 格式是参照



5 最终代码



go build main.go
nohup ./main &

最终效果
dingding-webhook-bot


7 最后


golang restful 框架之 go-swagger

hatlonely 发表了文章 • 0 个评论 • 245 次浏览 • 2018-02-09 02:53 • 来自相关话题

restful 是这些年的高频词汇了,各大互联网公司也都纷纷推出了自己的 restful api,其实 restful 和 thrift,grpc 类似,就是一种协议,但是这种协议有点特殊的就是使用 http 接口,返回的对象一般是 json 格式,这样... 查看全部

restful 是这些年的高频词汇了,各大互联网公司也都纷纷推出了自己的 restful api,其实 restful 和 thrift,grpc 类似,就是一种协议,但是这种协议有点特殊的就是使用 http 接口,返回的对象一般是 json 格式,这样有个好处,就是可以供前端的 js 直接调用,使用非常方便,但 http 本身并不是一个高效的协议,后端的内部通信还是使用 grpc 或者 thrift 可以获得更高的性能


其实如果只是要用 http 返回 json 本身并不是一件很难的事情,不用任何框架,golang 本身也能很方便做到,但是当你有很多 api 的时候,这些 api 的维护和管理就会变得很复杂,你自己都无法记住这些 api 应该填什么参数,返回什么,当然你可以花很多时间去维护一份接口文档,这样不仅耗时而且很难保证文档的即时性,准确性以及一致性


swagger 有一整套规范来定义一个接口文件,类似于 thrift 和 proto 文件,定义了服务的请求内容和返回内容,同样也有工具可以生成各种不同语言的框架代码,在 golang 里面我们使用 go-swagger 这个工具,这个工具还提供了额外的功能,可以可视化显示这个接口,方便阅读


下面通过一个例子来简单介绍一下这个框架的使用,还是之前的点赞评论系统:https://github.com/hatlonely/microservices


go-swagger 使用方法


api 定义文件


首先需要写一个 api 定义文件,这里我只展示其中一个接口 countlike,请求中带有某篇文章,返回点赞的次数


paths:
/countlike:
get:
tags:
- like
summary: 有多少赞
description: ''
operationId: countLike
consumes:
- application/json
produces:
- application/json
parameters:
- name: title
in: query
description: 文章标题
required: true
type: string
responses:
'200':
description: 成功
schema:
$ref: '#/definitions/CountLikeModel'
'500':
description: 内部错误
schema:
$ref: '#/definitions/ErrorModel'
definitions:
CountLikeModel:
type: object
properties:
count:
type: integer
title:
type: string
example: golang json 性能分析
ErrorModel:
type: object
properties:
message:
type: string
example: error message
code:
type: integer
example: 400

这个是 yaml 语法,有点像去掉了括号的 json


这里完整地定义了请求方法、请求参数、正常返回接口、异常返回结果,有了这个文件只需要执行下面命令就能生成框架代码了


swagger generate server -f api/comment_like/comment_like.yaml

还可以下面这个命令可视化查看这个接口文件


swagger serve api/comment_like/comment_like.yaml

这个命令依赖 swagger 工具,可以通过下面命令获取


Mac


brew tap go-swagger/go-swagger
brew install go-swagger

Linux


go get -u github.com/go-swagger/go-swagger/cmd/swagger
export PATH=$GOPATH/bin:$PATH

执行完了之后,你发现多了几个文件夹,其中 cmd 目录里面包含 main 函数,是整个程序的入口,restapi 文件夹下面包含协议相关代码,其中 configure_xxx.go 是需要特别关注的,你需要在这个文件里面实现你具体的业务逻辑


现在你就其实已经可以运行程序了,go run cmd/comment-like-server/main.go,在浏览器里面访问一下你的 api,会返回一个错误信息,告诉你 api 还没有实现,下面就来实现一下吧


业务逻辑实现


api.LikeCountLikeHandler = like.CountLikeHandlerFunc(func(params like.CountLikeParams) middleware.Responder {
count, err := comment_like.CountLike(params.Title)
if err != nil {
return like.NewCountLikeInternalServerError().WithPayload(&models.ErrorModel{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
}
return like.NewCountLikeOK().WithPayload(&models.CountLikeModel{
Count: count,
Title: params.Title,
})
})

你只需要在这些 handler 里面实现自己的业务逻辑即可,这里对协议的封装非常好,除了业务逻辑以及打包返回,没有多余的逻辑


再次运行,现在返回已经正常了


统一处理


如果你对请求有一些操作需要统一处理,比如输出统一的日志之类的,可以重写这个函数,也在 configure_xxx.go 这个文件中


func setupGlobalMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
handler.ServeHTTP(w, r)
})
}

这里我统一设置了一下头部,解决跨域访问问题


参考链接




转载请注明出处
本文链接:http://hatlonely.github.io/2018/02/08/golang-restful-%E6%A1%86%E6%9E%B6%E4%B9%8B-go-swagger/


golang orm 框架之 gorm

hatlonely 发表了文章 • 3 个评论 • 265 次浏览 • 2018-02-08 16:34 • 来自相关话题

最近在想给这个小站增加点赞和评论功能,第三方系统又有各种限制,就想自己弄个后端,实现类似的功能,对于个人来说,数据量不是很大,单机的 mysql 足够存下所有数据,mysql 作为底层存储是个不错的选择

之前在公司是直接用的 g... 查看全部

最近在想给这个小站增加点赞和评论功能,第三方系统又有各种限制,就想自己弄个后端,实现类似的功能,对于个人来说,数据量不是很大,单机的 mysql 足够存下所有数据,mysql 作为底层存储是个不错的选择


之前在公司是直接用的 github.com/go-sql-driver/mysql 访问数据库都是直接用写 sql,取出结果然后自己拼成对象,使用上面不是很方便,可读性也不好。想起之前研究 php laravel 框架的时候,直接把数据库层屏蔽了,用户看到的只有对象,使用非常方便,java 里面这种操作方式基本上已经成了标准做法,就去 github 上找了一下 golang 里面有没有类似的东西,果然已经有非常成熟的框架了,github.com/jinzhu/gorm 已经有 7k+ 的 star 了


ORM(Object Relation Mapping),对象关系映射,实际上就是对数据库的操作进行封装,对上层开发人员屏蔽数据操作的细节,开发人员看到的就是一个个对象,大大简化了开发工作,提高了生产效率


好了,下面我以这个点赞评论系统为例,介绍一下 gorm 的简单用法,以下使用的完整代码:https://github.com/hatlonely/microservices/blob/master/internal/comment_like/comment_like.go


gorm 用法介绍


库安装


go get -u github.com/jinzhu/gorm

数据库连接


import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"


var db *gorm.DB

func init() {
var err error
db, err = gorm.Open("mysql", "<user>:<password>/<database>?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
}

连接比较简单,直接调用 gorm.Open 传入数据库地址即可


github.com/jinzhu/gorm/dialects/mysql 是 golang 的 mysql 驱动,实际上就是 github.com/go-sql-driver/mysql 作者这里为了好记,重新弄了个名字


这里我用的 mysql,实际上支持基本上所有主流的关系数据库,连接方式上略有不同


db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)

还可以使用 db.DB() 对象设置连接池信息


表定义


先来定义一个点赞表,这里面一条记录表示某个用户在某个时刻对某篇文章点了一个赞,用 ip + ua 来标识用户,title 标识文章标题


type Like struct {
ID int `gorm:"primary_key"`
Ip string `gorm:"type:varchar(20);not null;index:ip_idx"`
Ua string `gorm:"type:varchar(256);not null;"`
Title string `gorm:"type:varchar(128);not null;index:title_idx"`
Hash uint64 `gorm:"unique_index:hash_idx;"`
CreatedAt time.Time
}

gorm 用 tag 的方式来标识 mysql 里面的约束


创建索引只需要直接指定列即可,这里创建了两个索引,ip_idxtitle_idx;如果需要多列组合索引,直接让索引的名字相同即可;如果需要创建唯一索引,指定为 unique_index 即可


支持时间类型,直接使用 time.Time 即可


创建表


if !db.HasTable(&Like{}) {
if err := db.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8").CreateTable(&Like{}).Error; err != nil {
panic(err)
}
}

直接通过 db.CreateTable 就可以创建表了,非常方便,还可以通过 db.Set 设置一些额外的表属性


插入


like := &Like{
Ip: ip,
Ua: ua,
Title: title,
Hash: murmur3.Sum64([]byte(strings.Join([]string{ip, ua, title}, "-"))) >> 1,
CreatedAt: time.Now(),
}

if err := db.Create(like).Error; err != nil {
return err
}

先构造已给对象,直接调用 db.Create() 就可以插入一条记录了


删除


if err := db.Where(&Like{Hash: hash}).Delete(Like{}).Error; err != nil {
return err
}

先用 db.Where() 构造查询条件,再调用 db.Delete() 就可以删除


查询


var count int
err := db.Model(&Like{}).Where(&Like{Ip: ip, Ua: ua, Title: title}).Count(&count).Error
if err != nil {
return false, err
}

先用 db.Model() 选择一个表,再用 db.Where() 构造查询条件,后面可以使用 db.Count() 计算数量,如果要获取对象,可以使用 db.Find(&Likes) 或者只需要查一条记录 db.First(&Like)


修改


db.Model(&user).Update("name", "hello")
db.Model(&user).Updates(User{Name: "hello", Age: 18})
db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) // nothing update

我这个系统里面没有更新需求,这几个例子来自于官网,第一个是更新单条记录;第二个是更新整条记录,注意只有非空字段才会更新;第三个例子是不会更新的,在系统设计的时候要尽量避免这些空值有特殊的含义,如果一定要更新,可以使用第一种方式,设置单个值


错误处理


其实你已经看到了,这里基本上所有的函数都是链式的,全部都返回 db 对象,任何时候调用 db.Error 就能获取到错误信息,非常方便


事务


func CreateAnimals(db *gorm.DB) err {
tx := db.Begin()
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}

事务的处理也很简单,用 db.Begin() 声明开启事务,结束的时候调用 tx.Commit(),异常的时候调用 tx.Rollback()


其他


还可以使用如下方式设置日志输出级别以及改变日志输出地方


db.LogMode(true)
db.SetLogger(gorm.Logger{revel.TRACE})
db.SetLogger(log.New(os.Stdout, "\r\n", 0))

也支持普通的 sql,但是建议尽量不要使用


参考链接




转载请注明出处
本文链接:http://hatlonely.github.io/2018/02/08/golang-orm-%E6%A1%86%E6%9E%B6%E4%B9%8B-gorm/


Traefik 一个反向代理的新工具

wangxingge 发表了文章 • 0 个评论 • 252 次浏览 • 2018-02-08 14:57 • 来自相关话题

Traefik

由于工作需要最近试用了几个反向路由的开源项目,Traefik就是其中之一。

一,Traefik 是干什么用的

简单来说它就是用来作反向代理和负载均衡的,比较适用于微服务化的场景,支持多种分... 查看全部

Traefik


由于工作需要最近试用了几个反向路由的开源项目,Traefik就是其中之一。


一,Traefik 是干什么用的


简单来说它就是用来作反向代理和负载均衡的,比较适用于微服务化的场景,支持多种分布式的Key-Value存储系统,支持容器技术,下面这个图诠释了它的工作。

image




二,Traefik 的特性


这些特性都是官方自己说的,再加上个人的一点通俗唱法解释。



  • 我很快 --- 经过测试吧,我感觉是速度一般般,但对于新产品来说速度做的还不错。

  • 安装简单 --- 不需要任何依赖,只是一个单独的可执行文件。(安装Traefik真的是简单,这操作给满分。)

  • 镜像很小 --- 有一个很小的官方 Docker Image。(可执行文件43MB,镜像只有45MB,评什么扣分。)

  • RestApi --- 支持Rest-API。(中规中矩。)

  • 配置热更新 --- 无需重启即可应用最新的配置。(其实这里说的配置只是动态的路由配置。)

  • 熔断机制 --- 对于后端服务的保护上支持熔断和重试机制。

  • 负载均衡 --- 负载策略内置两种,Weighted Round-Robin(wrr)和Dynamic Round-Robin(wrr)。

  • 支持Docker --- Docker, Swarm, Kubernetes, Marathon, Mesos。

  • 支持统计 --- Rest, Prometheus, Datadog, Statd。

  • 支持KV --- 支持多种分布式K-V系统,Zookeeper, Consul, Etcd, ECS等。

  • 多种通信协议 --- HTTP/1.1, HTTP/2, Websocket, GRPC等。

  • 支持ACME和HA --- 个人感觉Traefik的HA也就在ACME的场景下才有用。

  • Web-UI --- 有个AngularJS的Web-UI




三,Traefik 的基础组件


就两个组件,就这么简单,支持自己写 middle-ware。



  • Traefik

    Traefik 的主程序,启动时可以指定配置文件,
    ## 启动方式
    ## 默认的文件名是traefik.toml
    ## 寻址文件优先级是1. /etc/traefik, 2. $HOME/.traefik/ -->, 3. working directory.
    traefik --configFile=foo/bar/myconfigfile.toml

  • Dashboard

    一个简单的Dashboard, 可以看当前的路由规则,和转发的结果统计。


四, 配置文件如何使用


Traefik 的配置分为静态配置动态配置两大类。



  1. 动态配置:用来控制路由和负载均衡策略,动态配置不需要重起Traefik就可以生效。

  2. 静态配置:简单的说吧除了动态配置的其他均为静态配置范畴,静态配置需要重启Traefik才能生效。


配置详细说明我就不写了,到官网上找你需要的配置是最明智的(我是明智的官网)。

但是在后面的的练习中会说明部分配置的意义。


注意点



  1. 动态配置可以和静态配置一起在同一个文件里,动态配置写在文件的最后。

  2. 如果想用配置文件来指定路由规则的话,需要将动态配置和静态配置文件分开,如下
    ## 在  "静态配置的最后面"  加入下面信息来指定动态配置文件
    [file]
    watch = true
    filename = "rules.toml"





五,实战


业务需求


  1. 公司有多条产品线,但是每条产品线的负载压力不同,压力由高到低排序为, Mobile--> Web --> PC。

  2. 公司控制成本考虑,要求使用实体服务器,虚拟机或者容器技术来混合部署服务。

  3. 要求可以秒级的动态增加服务和减少服务来应对业务的压力。


画个简单的图来表示

image




基础环境


回顾一下上面的缩略图,发现我们需要下列基本的元素。

我们需要有一个Docker环境,需要有一套分布式K-V系统,K-V系统选择etcd。



  • [x] Real-Machine,用虚拟机来作RealMachine。

  • [x] Docker

  • [x] Etcd-Cluster

  • [x] Traefik

  • [x] Demo-Service

    Question



    1. 手上没有docker和etcd-cluster怎么办?

      Answer: 伸手过来我教你

    2. 不想用etcd,因为公司项目没用这个K-V.

      Answer: 你可以更换到任何一款主流的K-V系统,因为Traefik支持很多种K-V。




DemoService

直接从docker-hub上 pull 已经写好的Image就好了。


docker pull wangxingge/simple-web

Traefik

直接从docker-hub 上 pull下来。


docker pull traefik

货全了,开始搭建配置环境。




Traefik启动与配置文件



  • 创建docker-compose.yml 文件。docker-compose读取yml文件来启动镜像。


#docker-compose.yml

traefik:
image: traefik
command: --logLevel=DEBUG
ports:
- "80"
- "8080"
- "443"
## 端口由docker 自由分配,但是需要指定容器需要多少端口,如果要指定宿主机端口的话就写成
## 80 默认的http端口,443 默认的https端口,8080默认的后台管理页面端口
## ports:
## - "80:80"
## - "8080:8080"
## - "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/traefik/traefik.toml:/traefik.toml
- /opt/traefik/rules.toml:/rules.toml
## 磁盘映射,主要是为了映射配置文件和让traefik读取docker的网络配置
## traefik.toml 是traefik的静态配置文件
## rules.toml 是traefik的动态配置文件,也是路由配置


  • 创建traefik.toml配置文件。

    由于需求是秒级增加服务节点,所以路由配置信息我们不使用 rules.toml的形式提供,而使用分布式K-V来提供。

    在K-V 配置中设置exposebydefault = false 目的是为了不让启动容器时自动添加路由。


#traefik.toml

################################################################
# Common configuration
################################################################
debug=true
logLevel="DEBUG"
[web]
address = ":8080"
ProvidersThrottleDuration = 100000000

################################################################
# Docker configuration backend
################################################################
[docker]
domain = "docker.local"
watch = true
exposedbydefault = false

################################################################
# kv store
################################################################
[etcd]
endpoint = "192.168.196.88:12379"
watch = true
prefix = "traefik"



  • 启动Traefik


    # 使用docker-compose 目的是方便起停批量的镜像
    docker-compose up -d


  • 验证Traefik启动成功,查看docker镜像和后面管理页面是否可用
    [root@centos7 traefik]# docker-compose ps
    Name Command State Ports
    ------------------------------------------------------------------------------------------------------------------------------
    traefik_traefik_1 /traefik --logLevel=DEBUG Up 0.0.0.0:32772->443/tcp, 0.0.0.0:32773->80/tcp, 0.0.0.0:32771->8080/tcp


Traefik后台页面


配置路由信息


前面说明了要使用分布式K-V来进行路由信息的配置,我们选择etcd-cluster。

Traefik的路由配置一共分为2大部分,FrontEnd和 BackEnd。


- Frontend

主要控制访问的路由规则,有三个主要控制方式:Header, Host, Path,都支持指定单个规则和按正则匹配。

匹配方式为某些规则的请求转发到某个Backend上。


- Backend

进行某个Backend匹配某一组服务,控制方式有:轮询控制,断路器控制(熔断),压力控制和健康检查。



  • 实战我们模拟的Frontend和Backend要求为。

  • Frontend: 分别根据Host和Url区分路由。

  • Backend: 三组服务分别是 mobile, web, pc, 轮询方式为wrr,加入健康检查,熔断控制使用最大数量限制。

  • 具体配置如下: 只需要定义frontend,backend不需要定义服务启动时会自动注册Backend.




































































































































Alias Path Value
1 /traefik_configurations/1/frontends/front_web/priority 3
1 /traefik_configurations/1/frontends/front_web/passHostHeader true
1 /traefik_configurations/1/frontends/front_web/backend backend_web
1 /traefik_configurations/1/frontends/front_web/routes/test_1/rule Host:web.eastmoney.com
1 /traefik_configurations/1/frontends/front_mobile/priority 3
1 /traefik_configurations/1/frontends/front_mobile/passHostHeader true
1 /traefik_configurations/1/frontends/front_mobile/backend backend_mobile
1 /traefik_configurations/1/frontends/front_mobile/routes/test_1/rule Host:mobile.eastmoney.com
1 /traefik_configurations/1/frontends/front_pc/priority 3
1 /traefik_configurations/1/frontends/front_pc/passHostHeader true
1 /traefik_configurations/1/frontends/front_pc/backend backend_pc
1 /traefik_configurations/1/frontends/front_pc/routes/test_1/rule Host:pc.eastmoney.com
2 /traefik_configurations/2/frontends/front_web/priority 3
2 /traefik_configurations/2/frontends/front_web/passHostHeader true
2 /traefik_configurations/2/frontends/front_web/backend backend_web
2 /traefik_configurations/2/frontends/front_web/routes/test_1/rule Path:/web_root/web/{subdomain:[a-z]+}
2 /traefik_configurations/2/frontends/front_mobile/priority 3
2 /traefik_configurations/2/frontends/front_mobile/passHostHeader true
2 /traefik_configurations/2/frontends/front_mobile/backend backend_mobile
2 /traefik_configurations/2/frontends/front_mobile/routes/test_1/rule Path:/web_root/web/{subdomain:[a-z]+}
2 /traefik_configurations/2/frontends/front_pc/priority 3
2 /traefik_configurations/2/frontends/front_pc/passHostHeader true
2 /traefik_configurations/2/frontends/front_pc/backend backend_pc
2 /traefik_configurations/2/frontends/front_pc/routes/test_1/rule Path:/web_root/web/{subdomain:[a-z]+}

启动后台服务


根据目前的需要后台服务的运行环境有两种:测试过程我们使用Docker的环境方便一些。






















宿主 启动方式 优缺点
Docker 使用docker-compose 再用labels 指定路由信息 方便动态调整服务数量,不需要修改路由配置。
实体机 直接启动直接读取K-V的路由信息 需要服务启动时到K-V中进行注册之后路由才会生效,一个服务使用一台服务器产生资源浪费。


  • 启动后台服务
    业务说明了有三条主要业务线:mobile, pc, web,为了方便那我们分别为每个业务线创建一个docker-compose要用的配置文件。

    web:
    image: wangxingge/simple-web:latest
    command: /opt/simple-web --kvaddr="http://192.168.196.88:12379" --kv=true --backend="backend_web" -watch="/traefik/alias"
    ports:
    - "80"


mobile:
image: wangxingge/simple-web:latest
command: /opt/simple-web --kvaddr="http://192.168.196.88:12379" --kv=true --backend="backend_mobile" -watch="/traefik/alias"
ports:



  • "80"


pc:
image: wangxingge/simple-web:latest
command: /opt/simple-web --kvaddr="http://192.168.196.88:12379" --kv=true --backend="backend_pc" -watch="/traefik/alias"
ports:



  • "80"

    /opt/simple-web 启动程序


    kvaddr: K-V系统的地址


    kv: 是否使用K-V系统


    backend: 当前程序使用哪些backend, backend_web 表示当前服务为WEB业务提供服务。


    watch: 把traefik.toml中的K-V项的prefix再加上/alias就可以了,目的是为了配合traefik进行动态注册


    根据当前的线上压力情况,我们启动5个Mobile, 3个Web, 2个PC的后台服务。

    docker-compose up -d
    docker-compose scale web=5
    docker-compose scale mobile=3
    docker-compose scale pc=2



    启动和配置成功以后观查一下Traefik的Web页面
    ![image](http://ogmbad78f.bkt.clouddn.c ... _1.png)


测试路由设置是否可用,测试步骤如下



  1. 在使用1号配置的时候按照Host进行路由。

  2. 使用Postman并设置相应的Host值进行请求,并观察返回结果。

  3. 切换至2号配置。

  4. 使用Postman并根据想应的URL进行请求,并观察返回结果。

  5. 动态添加和缩减容器数量,docker-compose scale serviceName=serviceCount

  6. 观察Traefik的后台管理页面,是否已经更新相应的路由。

  7. 使用Postman请求,观查是否后端相应的服务器数量是否已更新。

从零开始学golang之Bellman

回复

freedbg 发起了问题 • 1 人关注 • 0 个回复 • 267 次浏览 • 2018-02-05 23:59 • 来自相关话题

golang 网络框架之 thrift

hatlonely 发表了文章 • 1 个评论 • 277 次浏览 • 2018-02-04 16:46 • 来自相关话题

thrift 最初是 facebook 开发使用的 rpc 通信框架,后来贡献给了 apache 基金会,出来得比较早,几乎支持所有的后端语言,使用非常广泛,是不可不知的一个网络框架

查看全部

thrift 最初是 facebook 开发使用的 rpc 通信框架,后来贡献给了 apache 基金会,出来得比较早,几乎支持所有的后端语言,使用非常广泛,是不可不知的一个网络框架


grpc 一样,需要先定义通信协议,然后实现自己业务逻辑,下面还是通过一个简单示例(之前的echo程序)说明 thrift 的用法,下面示例使用的完整代码在下列地址:
实现文件:https://github.com/hatlonely/hellogolang/tree/master/cmd/thrift
协议文件:https://github.com/hatlonely/hellogolang/tree/master/api/echo_thrift


简单 echo 服务


获取 thrift


go get git.apache.org/thrift.git/lib/go

定义协议文件


namespace go echo

struct EchoReq {
1: string msg;
}

struct EchoRes {
1: string msg;
}

service Echo {
EchoRes echo(1: EchoReq req);
}

执行 thrift -r --gen go echo.thrift 命令会生成 gen-go 文件夹,这个过程其实是将上面的协议翻译成 golang 代码


这个命令依赖于 thrift 工具,可以通过下面命令获取


Mac


brew install thrift

Linux


wget http://www-us.apache.org/dist/ ... ar.gz
tar -xzvf thrift-0.11.0.tar.gz
cd thrift-0.11.0
./configure
make -j8
[sudo] make install

实现服务端


type EchoServerImp struct {
}

func (e *EchoServerImp) Echo(ctx context.Context, req *echo.EchoReq) (*echo.EchoRes, error) {
fmt.Printf("message from client: %v\n", req.GetMsg())

res := &echo.EchoRes{
Msg: req.GetMsg(),
}

return res, nil
}

func main() {
transport, err := thrift.NewTServerSocket(":3000")
if err != nil {
panic(err)
}

processor := echo.NewEchoProcessor(&EchoServerImp{})
server := thrift.NewTSimpleServer4(
processor,
transport,
thrift.NewTBufferedTransportFactory(8192),
thrift.NewTCompactProtocolFactory(),
)
if err := server.Serve(); err != nil {
panic(err)
}
}

这个过程和 grpc 类似,不同的地方在于,thrift 支持更多的服务器类型,支持不同的协议打包方式,方便用户选择,这里的 compact 协议是一种压缩的协议,使用比较多


实现客户端


func main() {
var transport thrift.TTransport
var err error
transport, err = thrift.NewTSocket("localhost:3000")
if err != nil {
fmt.Errorf("NewTSocket failed. err: [%v]\n", err)
return
}

transport, err = thrift.NewTBufferedTransportFactory(8192).GetTransport(transport)
if err != nil {
fmt.Errorf("NewTransport failed. err: [%v]\n", err)
return
}
defer transport.Close()

if err := transport.Open(); err != nil {
fmt.Errorf("Transport.Open failed. err: [%v]\n", err)
return
}

protocolFactory := thrift.NewTCompactProtocolFactory()
iprot := protocolFactory.GetProtocol(transport)
oprot := protocolFactory.GetProtocol(transport)
client := echo.NewEchoClient(thrift.NewTStandardClient(iprot, oprot))

var res *echo.EchoRes
res, err = client.Echo(context.Background(), &echo.EchoReq{
Msg: strings.Join(os.Args[1:], " "),
})
if err != nil {
fmt.Errorf("client echo failed. err: [%v]", err)
return
}

fmt.Printf("message from server: %v", res.GetMsg())
}

这个 client 相对复杂一些,需要和 server 端设置一致的打包方式,如果不一致会出现通信失败,这一点需要特别注意一下


参考链接




转载请注明出处
本文链接:http://hatlonely.github.io/2018/02/04/golang-%E7%BD%91%E7%BB%9C%E6%A1%86%E6%9E%B6%E4%B9%8B-thrift/


golang 网络框架之 grpc

hatlonely 发表了文章 • 0 个评论 • 318 次浏览 • 2018-02-03 21:59 • 来自相关话题

grpc 是 google 开源的一款网络框架,具有极好的性能,可能是目前性能最好的网络框架,支持流式 rpc,可以很方便地构建消息订阅发布系统,支持几乎所有主流的语言,使用上面也很简单,公司很多服务基于 grpc 框架构建,运行非常稳定

... 查看全部

grpc 是 google 开源的一款网络框架,具有极好的性能,可能是目前性能最好的网络框架,支持流式 rpc,可以很方便地构建消息订阅发布系统,支持几乎所有主流的语言,使用上面也很简单,公司很多服务基于 grpc 框架构建,运行非常稳定


开始之前首先你要知道网络框架为你做了哪些事情:




  1. 网络协议序列化与反序列化

  2. 网络底层通信

  3. 并发管理



以及需要你做哪些事情:




  1. 定义通信的内容(通过协议文件)

  2. 实现通信的方法(实现协议接口)



以下面两个例子来分别说明两种 rpc 服务的简单用法


下面使用的完整代码下列地址:
实现文件:https://github.com/hatlonely/hellogolang/tree/master/cmd/grpc
协议文件:https://github.com/hatlonely/hellogolang/tree/master/api


简单 echo 服务


要实现的这个服务很简单,功能和 echo 命令类似,用一个字符串请求服务器,返回相同的字符串


获取 grpc


go get google.golang.org/grpc
go get google.golang.org/genproto/

go get 上面两个库就可以了。可能被墙了,需要 vpn;如果没有 vpn,可以找一台能下载的服务器下载下来再传到本地;如果也没有服务器,可以点击这里下载,解压后放到 vendor/ 目录下即可,不过可能不是最新版本


定义协议文件


首先要定义通信的协议,grpc 使用的是 proto3 序列化协议,这是一个高效的协议,关于这个协议的跟多内容可以参考下面链接:https://developers.google.com/protocol-buffers/docs/proto3


syntax = "proto3";

package echo;

message EchoReq {
string msg = 1;
}

message EchoRes {
string msg = 1;
}

service Echo {
rpc echo (EchoReq) returns (EchoRes);
}

执行如下命令会自动生成 echo.pb.go 文件,这个过程其实是把上面这个协议翻译成 golang:


protoc --go_out=plugins=grpc:. echo.proto

实际项目中可以把这个命令放到一个 Makefile 文件中,执行 make 命令即可生成代码:


上面命令依赖 protoc 工具,以及 golang 插件 protoc-gen-go,可以通过如下命令获取


Mac


brew install grpc
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

Linux


wget https://github.com/google/prot ... ar.gz
tar -xzvf protobuf-cpp-3.2.0.tar.gz
cd protobuf-3.2.0
./configure --prefix=${output}
make -j8
[sudo] make install
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

实现协议接口


type EchoServerImp struct {

}

func (e *EchoServerImp) Echo(ctx context.Context, req *echo.EchoReq) (*echo.EchoRes, error) {
fmt.Printf("message from client: %v\n", req.GetMsg())

res := &echo.EchoRes{
Msg: req.GetMsg(),
}

return res, nil
}

首先要定义一个接口的实现类 EchoServerImp,接口的的定义可以在上面生成的文件 echo.pb.go 中找到,这个类里面也可以有一些和业务逻辑相关的成员变量,这里我们的需求比较简单,没有其他的成员


然后需要在接口函数里面实现我们具体的业务逻辑,这里仅仅把请求里面的内容读出来,再写回到响应里面


你还可以为这个类增加其他的函数,比如初始化之类的,根据你具体的业务需求就好


实现服务端


func main() {
server := grpc.NewServer()
echo.RegisterEchoServer(server, &EchoServerImp{})

address, err := net.Listen("tcp", ":3000")
if err != nil {
panic(err)
}

if err := server.Serve(address); err != nil {
panic(err)
}
}

把我们刚刚实现的类实例注册到 grpc 里,再绑定到本地的一个端口上就可以了,现在可以启动服务了 go run echo_server.go


实现客户端


func main() {
conn, err := grpc.Dial("127.0.0.1:3000", grpc.WithInsecure())
if err != nil {
fmt.Errorf("dial failed. err: [%v]\n", err)
return
}

client := echo.NewEchoClient(conn)
res, err := client.Echo(context.Background(), &echo.EchoReq{
Msg: strings.Join(os.Args[1:], " "),
})

if err != nil {
fmt.Errorf("client echo failed. err: [%v]", err)
return
}

fmt.Printf("message from server: %v", res.GetMsg())
}

创建一个 client 之后,就可以像访问本地方法一样访问我们的服务了,go run echo_client.go hellogrpc


流式 rpc 服务


实现一个 counter 服务,客户端传过来一个数字,服务端从这个数字开始,不停地向下计数返回


定义协议文件


syntax = "proto3";

package counter;

message CountReq {
int64 start = 1;
}

message CountRes {
int64 num = 1;
}

service Counter {
rpc count (CountReq) returns (stream CountRes);
}

定义一个流式的 rpc 只需要在返回的字段前加一个 stream 关键字就可以


实现服务端


type CounterServerImp struct {

}

func (c *CounterServerImp) Count(req *counter.CountReq, stream counter.Counter_CountServer) error {
fmt.Printf("request from client. start: [%v]\n", req.GetStart())

i := req.GetStart()
for {
i++
stream.Send(&counter.CountRes{
Num: i,
})
time.Sleep(time.Duration(500) * time.Millisecond)
}

return nil
}

func main() {
server := grpc.NewServer()
counter.RegisterCounterServer(server, &CounterServerImp{})

address, err := net.Listen("tcp", ":3000")
if err != nil {
panic(err)
}

if err := server.Serve(address); err != nil {
panic(err)
}
}

接口实现上需要写一个死循环,不停地调用 Send 函数返回结果即可


实现客户端


func main() {
start, _ := strconv.ParseInt(os.Args[1], 10, 64)

conn, err := grpc.Dial("127.0.0.1:3000", grpc.WithInsecure())
if err != nil {
fmt.Errorf("dial failed. err: [%v]\n", err)
return
}
client := counter.NewCounterClient(conn)

stream, err := client.Count(context.Background(), &counter.CountReq{
Start: start,
})
if err != nil {
fmt.Errorf("count failed. err: [%v]\n", err)
return
}

for {
res, err := stream.Recv()
if err != nil {
fmt.Errorf("client count failed. err: [%v]", err)
return
}

fmt.Printf("server count: %v\n", res.GetNum())
}
}

客户端的 Count 接口返回的是一个 stream,不断地调用这个 streamRecv 方法,可以不断地获取来自服务端的返回


参考链接




转载请注明出处
本文链接:http://hatlonely.github.io/2018/02/03/golang-%E7%BD%91%E7%BB%9C%E6%A1%86%E6%9E%B6%E4%B9%8B-grpc/


终于有一个靠谱的Go写GUI的库了

codinghxl 回复了问题 • 26 人关注 • 18 个回复 • 16818 次浏览 • 2018-02-02 20:11 • 来自相关话题

golang 单元测试

hatlonely 发表了文章 • 1 个评论 • 223 次浏览 • 2018-01-31 22:33 • 来自相关话题

单元测试是质量保证十分重要的一环,好的单元测试不仅能及时地发现问题,更能够方便地调试,提高生产效率,所以很多人认为写单元测试是需要额外的时间,会降低生产效率,是对单元测试最大的偏见和误解

go 语言原生支持了单元测试,使用上非常简单,测试代... 查看全部

单元测试是质量保证十分重要的一环,好的单元测试不仅能及时地发现问题,更能够方便地调试,提高生产效率,所以很多人认为写单元测试是需要额外的时间,会降低生产效率,是对单元测试最大的偏见和误解


go 语言原生支持了单元测试,使用上非常简单,测试代码只需要放到以 _test.go 结尾的文件中即可。golang的测试分为单元测试和性能测试,单元测试的测试用例以 Test 开头,性能测试以 Benchmark 开头


举个例子


实现排列组合函数对应的单元测试和性能测试


实现排列组合函数


// combination.go

package hmath

func combination(m, n int) int {
if n > m-n {
n = m - n
}

c := 1
for i := 0; i < n; i++ {
c *= m - i
c /= i + 1
}

return c
}

实现单元测试和性能测试


// combination_test.go

package hmath

import (
"math/rand"
"testing"
)

// 单元测试
// 测试全局函数,以TestFunction命名
// 测试类成员函数,以TestClass_Function命名
func TestCombination(t *testing.T) {
// 这里定义一个临时的结构体来存储测试case的参数以及期望的返回值
for _, unit := range []struct {
m int
n int
expected int
}{
{1, 0, 1},
{4, 1, 4},
{4, 2, 6},
{4, 3, 4},
{4, 4, 1},
{10, 1, 10},
{10, 3, 120},
{10, 7, 120},
} {
// 调用排列组合函数,与期望的结果比对,如果不一致输出错误
if actually := combination(unit.m, unit.n); actually != unit.expected {
t.Errorf("combination: [%v], actually: [%v]", unit, actually)
}
}
}

// 性能测试
func BenchmarkCombination(b *testing.B) {
// b.N会根据函数的运行时间取一个合适的值
for i := 0; i < b.N; i++ {
combination(i+1, rand.Intn(i+1))
}
}

// 并发性能测试
func BenchmarkCombinationParallel(b *testing.B) {
// 测试一个对象或者函数在多线程的场景下面是否安全
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m := rand.Intn(100) + 1
n := rand.Intn(m)
combination(m, n)
}
})
}

运行测试


go test combination_test.go combination.go           # 单元测试
go test --cover combination_test.go combination.go # 单元测试覆盖率
go test -bench=. combination_test.go combination.go # 性能测试

setup 和 teardown


setup 和 teardown 是在每个 case 执行前后都需要执行的操作,golang 没有直接的实现,可以通过下面这个方法实现全局的 setup 和 teardown,具体每个 case 的 setup 和 teardown 需要自己实现


func TestMain(m *testing.M) {
// setup code...
os.Exit(m.Run())
// teardown code...
}

goconvey


这个第三方工具会自动帮我们跑测试,并且以非常友好的可视化界面帮我们展示测试的结果,包括测试失败的原因,测试覆盖率等等,内部还提供了很多友好的断言,能提高测试代码的可读性


使用方法


go get github.com/smartystreets/goconvey

然后用终端在测试代码的目录下运行 goconvey 命令即可


测试例子


package package_name

import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)

func TestIntegerStuff(t *testing.T) {
Convey("Given some integer with a starting value", t, func() {
x := 1

Convey("When the integer is incremented", func() {
x++

Convey("The value should be greater by one", func() {
So(x, ShouldEqual, 2)
})
})
})
}

参考链接




转载请注明出处
本文链接:http://hatlonely.github.io/2018/01/31/golang-单元测试/


GopherChina 大会早鸟票最后一天

astaxie 发表了文章 • 0 个评论 • 654 次浏览 • 2018-01-31 09:44 • 来自相关话题

今年我们针对Gopher们的要求,早鸟票延长了两周,回馈给更多的Gopher,今天是最后一天,如果你是个人,那么抓紧时间报名,如果是公司报销的话,无所谓早晚了,后面也不着急。哈哈。

报名地址:查看全部

今年我们针对Gopher们的要求,早鸟票延长了两周,回馈给更多的Gopher,今天是最后一天,如果你是个人,那么抓紧时间报名,如果是公司报销的话,无所谓早晚了,后面也不着急。哈哈。


报名地址:https://www.bagevent.com/event/1086224