利用Go的简单性轻松开发

Go语言非常适合日益流行的面向服务的体系结构。

在过去的几年中,出现了许多好的实践来帮助解决微服务所带来的问题。 如果您不想以难以维护的,难以操作的基础架构告终,那么这些做法非常重要。 通过将它们与Go的一些更容易被忽略的功能结合起来,您可以使服务的开发和操作变得更加容易。

在这篇博客文章中,我将解释Go的这些功能,并提供专注于开发,构建和配置服务的最佳实践。 这些做法易于应用,因此,新开发人员可以更快地加入并更好地合作。 您可以更快地部署,理解和调试服务; 通常,开发会更容易。

把事情简单化

enbrite.ly ,我们揭示了在线广告中的欺诈活动。 结果,我们开发并运营了由服务组成的基础架构。 这些服务大多数都作为Go服务实现。

我们选择Go是因为它易于学习,简单且富有成效。 去年,我们与六个开发团队一起编写了20多个服务。

最初,我们使用魔术框架和库通过使用其他人编写的内容简化了开发过程。 我们开始使用Martini,后来使用Negroni添加中间件和日志记录。

我读过许多很棒的Go开发人员使用标准的HTTP库,并且我想,“男孩,这是核心。” 但是,随着您对标准HTTP库的了解越来越多,您会越发意识到它是纯粹的天才,非常适合服务。

查看有关如何创建可通过域调用的最小服务并返回IP地址的示例。

type IPMessage struct {
    IPs []net.IP
}

type ErrorMessage struct {
    Error string
}

func IPHandler(rw http.ResponseWriter, r *http.Request) {
    domain := r.URL.Query().Get("domain")
    if len(domain) == 0 {
     json.NewEncoder(rw).Encode(ErrorMessage{"No domain parameter"})
     return
    }
    ips, err := net.LookupIP(domain)
    if err != nil {
     json.NewEncoder(rw).Encode(ErrorMessage{"Invalid domain address."})
     return
    }
    json.NewEncoder(rw).Encode(IPMessage{ips})
}

func main() {
    address := ":8090"
    r := http.NewServeMux()
    r.HandleFunc("/service/ip", IPHandler)
    log.Println("IP service request at: " + address)
    log.Println(http.ListenAndServe(address, r))
}

我们定义了IPHandler函数,它是一个http.HandlerFunc 。 在主要功能中,我们将此添加到路由器中。

始终总是显式获得路由器比隐式使用默认值更好。 这样,我们可以随时与其他路由器交换。 如果我们决定我们需要的路径参数,我们可以方便地切换到大猩猩通过改变路由器http.NewServeMux()mux.NewRouter() 只有一行更改。 实际上,Gorilla路由器是我们有时在服务中使用的唯一第三方库。 它写得很好,非常适合REST服务。

我们还开始了使用端口记录侦听地址的良好做法。 对于其他开发人员而言,更容易在日志中看到该服务实际上已启动以及正在侦听的地址。 还应记录ListenAndServe函数的返回值,以便在出现任何错误时返回一条日志消息,并且该函数返回而不是阻塞。

我们可能需要的下一件事是中间件。 使用标准库本身即可轻松创建中间件。 我们所要做的就是定义一个http.Handler调用我们的基本处理程序,并添加到ListenAndServe功能,而不是基本处理程序。 同样,无需第三方库; 一个简单而优雅的解决方案。

中间件本身:

type M struct {
    handler http.Handler
}

func (m M) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    start := time.Now()
    m.handler.ServeHTTP(rw, r)
    log.Printf("%s served in %s\n", r.URL, time.Since(start))
}

func NewM(h http.Handler) http.Handler {
    return M{h}
}

如果我们想向中间件添加任何东西,可以在ServeHTTP方法中完成。 为了使我们的代码更简洁,更易读,我们可以将基本处理程序的构建提取到一个单独的函数中。

func createBaseHandler() http.Handler {
    r := http.NewServeMux()
    r.HandleFunc("/service/ip", IPHandler)
    return NewM(r)
}

定义基本处理程序后,我们将其添加到ListenAndServe函数中。

log.Println("Service request at: " + address)
log.Println(http.ListenAndServe(address, createBaseHandler()))

日志记录是中间件之类的另一个领域,人们可以使用它来访问第三方库。 缺少标准库日志记录软件包的一项功能是日志级别。

的确,有时在冗长的记录和生产日志之间切换很方便,但是我们遵循一个简单的规则。 您应该记录重要的事件或状态并携带有价值的信息:

  1. 如果没有有价值的信息,请不要记录。
  2. 如果您将该日志消息用于调试目的,请在生产中将其删除。
  3. 如果是错误,请根据错误的性质使用致命或紧急记录。
  4. 其他任何日志都与log.Println一起使用。

有了这个小规则,除了标准日志库中的日志级别外,您实际上并不需要日志级别。

错误记录的另一种最佳做法是记录错误本身(如果存在)。 不仅要记录通用的“发生错误”,还要记录返回的错误内容以及尽可能多的信息。 当发生错误时,它可能确实令人沮丧,但是错误消息没有告诉您任何特定的信息。 一个很好的例子是模板错误。 当模板解析失败时,返回的错误实例将包含确切的问题。 没有它,很难调试问题。

通过提供更详细的错误消息,您可以帮助以后进行调试。 相信我,您的团队稍后会感谢您!

您还可以使用log.SetPrefix设置日志记录前缀,以注意哪个日志消息来自哪个服务。 如果将主机信息添加到前缀,甚至可以确定哪个主机记录了该消息,这在分布式环境中非常有用。

使用此简单易行的规则,我们的日志仅包含有价值的信息。 这样可以减少噪音,但可以确保我们获得调试,遥测或验尸所需的每条有价值的信息。

要设置前缀,请将以下内容添加到您的主函数中的任何日志记录语句之前:

log.SetPrefix("[service] ")

构建过程

应用程序的构建和部署必须简单,自动化。 在尝试了不同的方法之后,我们最终使用了Makefile。

将Makefile与go命令提供的工具结合使用,可提供功能全面的构建工具。 构建,测试,测试范围,静态分析和格式化都是go命令的一部分。 通过使它们成为构建过程的一部分,开发人员不必考虑这些步骤。 如果每个服务都使用相同的构建方式,则每个开发人员都可以轻松构建其中的任何一个。

仅使用make命令,我们就可以审核,格式化,测试和构建我们的服务。 让我们来看一个如何向我们的服务添加Makefile的示例:

default: build

      build:
            go fmt
            go vet
            go build 
    
      test: build
            go test
    
      coverage-test:
            go test -coverprofile=coverage.out
            go tool cover -func=coverage.out
            go tool cover -html=coverage.out
            rm coverage.out

使用此统一的Makefile,每个团队成员都可以以相同的方式检查,测试和格式化代码。 它对代码审查有很大帮助。 无需讨论格式。 Go的兽医已经检查了代码中的常见错误。 如果构建过程通过,则可以100%确保没有未使用的变量或导入。

我个人真的很喜欢遇到未使用的变量时会失败的编译器功能。 我们不必争论未使用的变量,也不保证没有多余的结果。

要分析测试覆盖率,请使用make coverage-test命令。 它运行测试并在浏览器中以HTML格式显示结果,因此您可以查看测试涵盖的内容和缺少的内容。 由标准Go工具提供的超级简便的测试覆盖率。

没有例外

我有一个硬性规定:每个服务都必须在根URL /上提供UI。 UI应该显示内部状态和特定服务所做的任何历史。

http://<service address>/上可以访问并检查每个服务的信任为开发人员提供了信心,并为他们调试,开发或只是了解新服务提供了起点。

ui for codeship文章

使用Go,可以很容易地将UI添加到任何服务中。 模板提供了一种生成HTML的简便方法。 添加Bootstrap CSS,您将拥有一个可操作的UI。 使用goroutine启动它,您甚至不必处理并行性。 使用Go,与服务本身并行运行管理站点非常容易。

首先,使用一些自举CSS,一个查询表和datatables javascript模块创建一个基本HTML模板,该模块向普通HTML表中添加分页,搜索和排序等功能。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <link rel="stylesheet" href="http://d2fq26gzgmfal8.cloudfront.net/bootstrap.min.css" media="screen">
    <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/r/bs/dt-1.10.9/datatables.min.css" />
</head>

<body>
    <div class="container">
        <div class="row">
            <h1>Domain checker</h1>
        </div>
        <div class="row">
            <table class="table table-bordered" id="queries">
                <thead>
                    <tr>
                        <th>Domain</th>
                        <th>IPs</th>
                    </tr>
                </thead>
                <tbody>
                    {{ range .Queries }}
                    <tr>
                        <td><a href="http://{{ .Domain }}">{{ .Domain }} </a> 
                        </td>
                        <td>{{ .IPs }}</td>
                    </tr>
                    {{ end }}
                </tbody>
            </table>
        </div>
    </div>
    <script src="http://d2fq26gzgmfal8.cloudfront.net/jquery-1.10.2.min.js"></script>
    <script type="text/javascript" src="https://cdn.datatables.net/r/bs/dt-1.10.9/datatables.min.js"></script>
    <script src="http://d2fq26gzgmfal8.cloudfront.net/bootstrap.min.js"></script>
    <script>
        $(document).ready(function()
        {
            $('#queries').DataTable();
        });
    </script>
</body>
</html>

我们必须在/上公开另一个端点,该端点可以解析,执行和显示此模板。

func uiHandler(rw http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("templates/index.html")
    if err != nil {
        log.Panic("Error occured parsing the template", err)
    }
    page := PageData{
        Queries: queries,
    }
    if err = tmpl.Execute(rw, page); err != nil {
        log.Panic("Failed to write template", err)
    }
}

要进行连接,我们必须将UI端点添加到路由器中,定义我们使用的结构,并将所有传入的域查询添加到queries列表中,以便可以在UI上显示它。

我们使用一种结构来注册查询及其结果。 另一个是用于描述UI的模板。

// The Query type represents a query against our service.
type Query struct {
    Domain string
    IPs    []net.IP
}

// The data struct for our basic UI
type PageData struct {
    Queries []Query
}

createBaseHandler函数中定义了先前处理程序的createBaseHandler ,可以添加UI端点。

r.HandleFunc("/", uiHandler)

最后,我们将在IPHandler完成的每个查询附加到全局变量。 变量:

var queries = []Query{}

IPHandler函数中:

queries = append(queries, Query{domain, ips})

每个团队成员都喜欢我们为我们的服务提供UI。 当新的团队成员尝试了解特定服务的功能时,此策略会有所帮助。 UI为他们提供了服务用途的功能视图。 当您可以在浏览器中检查服务而不必查询数据库时,也更容易查看服务的情况。

令人惊讶的副作用是客户的运营和销售也开始使用它们。 易于访问的UI使我们的服务在开发团队之外可用。

均匀心跳

我们需要了解有关运行服务的一组简单指标:

  • 它在运行吗?
  • 如果正在运行,它正在运行哪个版本?

我们希望为我们编写和运行的每项服务提供此信息。 有了Go,再简单不过了。

我们提供了在给定端口上运行Web服务的 。 调用时,它将返回用于构建二进制文件,正常运行时间和状态的提交的SHA-1代码。 要使用此库,我们go heartbeat.RunHeartbeatService(portnum)就是将go heartbeat.RunHeartbeatService(portnum)添加到我们的主要函数中。

仅用一条线路,我们就在我们的服务中添加了心跳信号,任何第三方都可以使用该信号来检查我们的服务状态。 我们使用Consul进行服务发现和配置管理。 我们的Consul群集使用这些检测信号来了解服务实例是否正常。

对于DNS,我们使用AWS Route53。 它还使用此心跳信号来查看哪些IP地址对于给定的DNS记录有效。

要在构建服务时设置提交SHA-1代码,我们将Makefile中的go build命令更改为:

go build --ldflags="-X github.com/enbritely/heartbeat-golang.CommitHash`git rev-parse HEAD"

它将SHA-1哈希添加到心跳库,当调用http://<service-address/heartbeat url时,它将返回它。

另一个简单的解决方案是,任何人都容易记住每个服务都具有这种心跳。 它可用于获取运行版本或检查状态。 这不是计划中的优势,但后来在自动化方面也大有帮助。 当我们引入自动故障转移功能时,如果发生故障,其中一项服务将替换为另一项服务,则我们使用此心跳功能来检查服务的运行状况。

后来的优势的另一个例子是当我们的一个队友创建了一个网站,其中列出了所有已部署的服务。 因为有了此心跳服务,所以我们只需查询所有服务并显示结果页面即可。

要启用心跳服务侦听HEARTBEAT_ADDRESS环境变量中配置的地址,请在主函数中添加以下两行。

hAddress := os.Getenv(“HEARTBEAT_ADDRESS”)
go heartbeat.RunHeartbeatService(hAddress)

要安装心跳服务,您必须先获取它。

go get github.com/enbritely/heartbeat-golang

组态

我也喜欢保持配置简单。 我们遵循12因素应用技术,该技术描述了配置应来自环境。 这意味着环境变量。

它的简单性使其非常灵活。 在生产中设置与测试,登台或开发不同的配置很容易。 我们使用服务启动脚本在生产中设置这些变量,因此在服务启动时它们就可用。

环境变量很容易在开发机器上设置:您可以在命令行上定义它们,也可以将它们导出到.bashrc.profile文件中。 我们使它变得更加容易。

每个开发人员都在项目的根文件夹中定义一个.dotenv文件。 它包含我们的环境变量; 采购它们将设置开发配置。 它快速,简单,每个开发人员都可以调整。

例如,如果您不想在计算机上运行数据库,则可以将数据库地址设置为指向所需的任何位置。 我们中的一些人使用Docker运行本地数据库,我们中的一些人使用AWS RDS进行测试。 使用环境变量和.dotenv文件,这不是问题。

结论

软件开发应该很有趣。 如果有部分事情可以取笑,这就是整个团队的最大利益。 使代码审查,部署,测试或监视更加容易是一个很好的起点。

Go对此提供了大力支持。 使我们的开发环境在每个项目中保持一致,有助于现有或新团队成员更轻松地入职。 使服务可访问有助于调试。 用户界面为我们的服务创建了易于阅读的界面。 所有这些做法可帮助我们的团队专注于重要的事情。

您对Go的体验是什么? 您还有其他最佳做法吗? 请在评论中分享!

翻译自: https://www.javacodegeeks.com/2015/11/utilizing-the-simplicity-of-go-for-easy-development.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值