后端

go-micro v2弃用了consul作为默认的服务发现

月盾

很遗憾,go-micro v2版本不再使用consul作为服务发现中间件,官方文档也没有consul相关的文档,而是默认改用了mdns,生产推荐etcd

问题:I can’t set registry with consul

解答:《Deprecating Consul in favour of Etcd

超过4年的时间,Consul一直是Micro的默认服务发现系统之一,为我们提供了良好的服务。实际上,从一开始,它就是用于注册表的默认机制以及入门所需的唯一基础依赖项。

从那时起,世界在不断发展,原生云技术也在不断发展。我们发现了许多与使用Consul的方式有关的问题。这不是对Consul的打击,而是对我们的用例的反思,以及对继续前进的需求。

例如,我们将元数据和服务端点信息进行二进制编码,压缩和base64编码,然后再将它们存储为Consul标签,因为没有其他方法可以这样做。我们还非常严重地滥用Consul的分布式属性,这导致了许多关于raft共识的问题。

不幸的是,我们发现现在该继续前进了。

自2014年以来,Kubernetes真正成为了容器编排和基础服务平台中的一支计算力。因此,etcd成为了他们选择的键值存储的一种,它是基于raft共识构建的分布式键值存储。它已经发展到可以满足kubernetes的规模需求,并且已经以其他开源项目所没有的方式经过了实战测试。

Etcd还是用于二进制数据的非常标准的Get / Put / Delete存储,这意味着我们可以轻松地编码和存储服务元数据,而不会出现零问题。它对所存储数据的格式没有意见。

过去一周中,我们已将etcd迁移为Micro中的默认服务发现机制之一,并将在未来几周内弃用Consul。这是什么意思?好吧,我们将领事移交给我们社区维护的go-plugins存储库,并专注于支持etcd。

我们知道许多用户正在使用Consul,这可能会导致中断。对我们来说,这是通往v2的重大突破,因此我们的下一个发行版将被标记为v2。您可以放心,您的v1发行版将继续按原样运行,但希望我们发布的下一个发行版是micro v2.0.0。

参考项目:micro-service

beego httplib库使用方法

月盾

beego是一个优秀的api,web框架,不只是其丰富的功能特性,更是因为其功能的独立性,可以根据自身需要单独添加使用。 常用的模块有以下这些:

  • session 模块
  • cache 模块
  • logs 模块
  • httplib 模块
  • context 模块
  • toolbox 模块
  • config 模块
  • i18n 模块

本文要讲解的是httplib客户端请求的使用。

日常开发中不只是要接收请求,还会发起http请求,go本身提供了http库可以实现http请求,不过使用起来略微复杂一些。如果使用的框架是beego的话,那推荐使用httplib

基本使用方法

import (
    "github.com/astaxie/beego/httplib"
)

然后初始化请求方法,返回对象

req := httplib.Get("http://beego.me/")

然后我们就可以获取数据了

str, err := req.String()
if err != nil {
    t.Fatal(err)
}
fmt.Println(str)

以上是最基本的使用方法,更多文档可以查看httplib文档,本文不再做一次搬运工。 下面提供一些使用实例以供参考:

获取body信息

func RequestByAjax3(region, language string) {
    req := httplib.Get(fmt.Sprintf("https://m.lagou.com/search.json?city=%s&positionName=%s&pageNo=1&pageSize=1", url.QueryEscape(region), language))
    req.Header("Referer", "https://m.lagou.com/search.html")
    req.Header("Cookie", "JSESSIONID=ABAAAECAAHHAAFD8DC17DEB3DE2DF3C5FCAE8C3D4423759; user_trace_token=20200117101405-234d1d57-b8c1-4d66-956e-c49f35f28f75; LGSID=20200117101406-09c6fa83-38cf-11ea-b2e7-525400f775ce;  PRE_LAND=https%3A%2F%2Fm.lagou.com%2Fsearch.html; LGUID=20200117101406-09c6fc06-38cf-11ea-b2e7-525400f775ce; X_HTTP_TOKEN=8e6e6bd15763030e425822975149ec77fc62d73ec7;")
    req.Header("Host", "m.lagou.com")
    req.Header("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1")
    var resBody wapResBody//TODO 根据需要自己定义
    req.ToJSON(&resBody)
    logs.Debug(">>>>>>%+v", resBody)
    if resBody.State != 1 {
        logs.Error("获取"+language+"数据为空!", fmt.Sprint("%+v", resBody))
    }
    logs.Debug(resBody)
}

上面代码中的resBody就是接收到的body内容,其核心是req.ToJSON(&resBody)

nestjs框架中使用nunjucks模板引擎

月盾

main.ts

import { NestFactory } from '@nestjs/core';
import {
    ExpressAdapter,
    NestExpressApplication,
} from '@nestjs/platform-express';
import { AppModule } from './app.module';
import nunjucks = require('nunjucks');
import { join } from 'path';

async function bootstrap() {
    const app = await NestFactory.create<NestExpressApplication>(
        AppModule,
        new ExpressAdapter(),
    );
    app.useStaticAssets(join(__dirname, '..', 'public')); // NestFactory.create需要加泛型参数<NestExpressApplication>
    app.setBaseViewsDir(join(__dirname, '..', 'views')); // 修改模板文件后立马生效,否则需要重启服务,nunjucks watch参数也有相同作用
    nunjucks.configure('views', {
        ext:'njk',
        autoescape: true,
        express: app,
        watch: true,
    });
    await app.listen(3000, () => {
    });
}
bootstrap();

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get('articles')
    // @Render('articles.njk') // 不能使用@Render装饰器,而是使用res.render
    async findArticlesByUser(@Res() res:Response): Promise<any> {
        return res.render('articles.njk', {
            title: "标题",
            articles
        })
    }
}

articles.njk

部署golang到服务器

月盾

说起将开发好的程序部署到服务上,常用的有两种方式:

  1. 本地编译打包,上传到服务器
  2. git push到远程仓库,在服务器上拉取(编译-打包)

无论以怎样的方式发布,都只有熟悉流程才能得心应手。今天我要说的是golang的部署流程。

如果是在公司内,自然有专人负责发布事宜,也有公司暂无运维人员,这时还是由开发人员负责服务器发布工作,当然,CI/CD这类工具一般也没有搭建起来。但这并不影响我们快速发布。 得益于go的编译速度,整个发布过程可能也就2分钟,接下来说明一下我个人的发布流程:

  1. 在项目目录下执行go打包命令
GOOS=linux GOARCH=amd64 go build 

由于是要部署到Linux服务器上,所以加上GOOS=linux GOARCH=amd64就可以打包出对应系统的二进制可执行文件。可以将该命令写成脚本文件。

  1. 推送代码到git仓库,这一步并不是必须,之所以需要这一步,是因为go只打包*.go文件,并不会打包静态文件,所以还需要把相关静态文件推送的git仓库以便拉取。

  2. 上传打包好的二进制可执行文件到服务器的项目目录下。为什么是项目目录?因为还有静态文件需要使用,所以服务器上也要有同样的项目结构。可借助一些工具来上传,我使用了rz命令来上传。

  3. git pull代码,主要是拉取静态文件。

  4. 重启应用。

整个过程比较耗时的操作是上传文件和推拉代码,打包和重启应用反而很快,基本是两三秒完成。 golang相对于其他语言,在服务,器上不需要安装运行时,不像Java和nodejs都需要安装正确的运行时版本,go只需要把打包好的二进制可执行文件扔上去就可以执行。

go语言开发grpc之安装grpc

月盾

一、安装gRPC

$ go get -u google.golang.org/grpc  
package google.golang.org/grpc: unrecognized import path "google.golang.org/grpc" (https fetch: Get https://google.golang.org/grpc?go-get=1: dial tcp 216.239.37.1:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.)

grpc的源码库迁移到了github上,所以需要手动下载了。grpc-go 正常情况下按照以下方式就可安装完成

git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc

git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net

git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text

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

git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto

cd $GOPATH/src/

go install google.golang.org/grpc

但是在某些情况可能连git clone都不行。就像下面这样的:

go实现文件下载

月盾

go可以很容易实现一个文件服务器,只需要使用函数 func ServeFile(w ResponseWriter, r *Request, name string)即可。

package main

import (
	"log"
	"net/http"
	"fmt"
)

func helloHandler(res http.ResponseWriter, req *http.Request) {
	http.ServeFile(res, req, "E:/go-work/src/go-learning/foo.xlsx")
}
func main() {
	fmt.Println("web服务启动成功,可在浏览器中访问:localhost:8081")
	http.HandleFunc("/file", helloHandler)
	http.Handle("/", http.FileServer(http.Dir("E:/go-work/src/go-learning/")))
	err := http.ListenAndServe(":8081", nil)
	if err != nil {
		log.Fatal("ListenAndServe:", err.Error())
	}
}

查看go文档除了翻墙访问https://golang.org

还可以访问国内镜像:https://golang.google.cn/

最简单快速的访问,直接在本地起服务:

godoc -http=:8082

beego注解路由404

月盾

beego注解路由匹配不到,返回404页面 router.go使用了两种方式注册路由:

ns := beego.NewNamespace("/admin",
	beego.NSRouter("/", &controllers.UserController{}, "get:Welcome"),
	beego.NSInclude(
		&controllers.UserController{},
	),

controller中的路由注解设置:

// @router /admin/user/get-all-user [get]
func (c *UserGroupController) GetAllUser() {
	user := new(User)
	users, err := user.GetUserList()
	if nil != err {
		c.Data["json"] = ErrorMsg(err)
	}
	c.Data["json"] = users
	c.ServeJSON()
}

使用上面的方式注册路由后结果是nomatch

最终结果显示上面的注解路由时错误的,下面是正确的注册方式: 问题在于controller的注解写法,如果该路由在namespace下,则不能在注解中拼接命名空间前缀,框架会自动拼接。 即/admin为命名空间,注解中只需写/user/get-all-user,不能这样写/admin/user/get-all-user

// @router /user/get-all-user [get]
func (c *UserGroupController) GetAllUser() {
	user := new(User)
	users, err := user.GetUserList()
	if nil != err {
		c.Data["json"] = ErrorMsg(err)
	}
	c.Data["json"] = SuccessData(users)
	c.ServeJSON()
}

当然,两种路由注册的方式可以同时

完整项目:https://github.com/yuedun/metal

go并发获取数据

月盾

go语言可以很轻松的实现并发获取数据,就算是新手也可以按部就班的套用现成的并发模式来实现并发。以下是一个简单的测试程序,其中有串行,并行。

package main

import (
	"sync"
	"time"
	"fmt"
)
func main() {
	syncFunc()
	fmt.Println(">>>>>>>>>>>>>>>")
	asyncFunc()
	fmt.Println(">>>>>>>>>>>>>>>")
	asyncChanFunc()
}
// 串行执行
func syncFunc() {
	var n,m,x int
	start := time.Now()
	fmt.Println("syncFunc start:",start)
	func () {
		time.Sleep(time.Second*1)
		n = 1
	}()
	func () {
		time.Sleep(time.Second*2)
		m = 2
	}()
	func () {
		time.Sleep(time.Second*3)
		x  =3
	}()
	t := time.Now()
	fmt.Println(t)
	elapsed := t.Sub(start)
	fmt.Println("syncFunc end:", elapsed, n, m, x)
}
// 并行执行
func asyncFunc() {
	var n,m,x int
	var wg sync.WaitGroup
	wg.Add(3)
	start := time.Now()
	fmt.Println("asyncFunc start:", start)
	go func () {
		defer wg.Done()
		time.Sleep(time.Second*1)
		n = 1
	}()
	go func () {
		defer wg.Done()
		time.Sleep(time.Second*2)
		m = 2
	}()
	go func () {
		defer wg.Done()
		time.Sleep(time.Second*3)
		x = 3
	}()
	wg.Wait()
	t := time.Now()
	fmt.Println(t)
	elapsed := t.Sub(start)
	fmt.Println("asyncFunc end:", elapsed, n, m, x)
}

// 并行执行
func asyncChanFunc() {
	var n, m, x =make(chan int),make(chan int),make(chan int)
	start := time.Now()
	fmt.Println("asyncChanFunc start:",start)
	go func () {
		time.Sleep(time.Second*1)
		n <- 1
	}()
	go func () {
		time.Sleep(time.Second*2)
		m <- 2
	}()
	go func () {
		time.Sleep(time.Second*3)
		x <- 3
	}()

	fmt.Printf("n:%d, m:%d, x:%d\n",<-n, <-m, <-x)
	t := time.Now()
	fmt.Println(t)
	elapsed := t.Sub(start)
	fmt.Println("asyncChanFunc end:", elapsed)
}

测试结果:

go测试函数的编写及运行

月盾

go test命令是一个按照一定的约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源文件并不是go build构建包的一部分,它们是go test测试的一部分。 在\*_test.go文件中,有三种类型的函数:测试函数、基准测试函数、示例函数。一个测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确; go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。

测试函数

每个测试函数必须导入testing包。测试函数有如下的签名:

func TestName(t *testing.T) {
// ...
}

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头:

func TestSin(t *testing.T) { /* ... */ }
func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }

其中t参数用于报告测试失败和附加的日志信息。让我们定义一个实例包gopl.io/ch11/word1,其中只有一个函数IsPalindrome用于检查一个字符串是否从前向后和从后向前读都是一样的。(下面这个实现对于一个字符串是否是回文字符串前后重复测试了两次;我们稍后会再讨论这个问题。)

// gopl.io/ch11/word1
// Package word provides utilities for word games.
package word

// IsPalindrome reports whether s reads the same forward and backward.
// (Our first attempt.)
func IsPalindrome(s string) bool {
	for i := range s {
		if s[i] != s[len(s)-1-i] {
			return false
		}
	}
	return true
}

在相同的目录下,word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。每一个都是测试IsPalindrome是否给出正确的结果,并使用t.Error报告失败信息