BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

如何使用Docker部署Go Web应用程序

| 作者 大愚若智 关注 9 他的粉丝 发布于 2016年6月12日. 估计阅读时间: 42 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

简介

虽然大部分Go应用程序可以编译为一个单一的二进制文件,但Web应用程序可能还有自己的模板和配置文件。如果一个项目中包含大量文件,可能会因为文件不同步而导致出错并造成更多严重问题。

您将通过本文了解如何使用Docker部署Go Web应用程序,以及Docker如何帮您改善开发工作流和部署过程。各种规模的团队都能从本文内容中获益。

目标

通过阅读本文,您将能:

  • 对Docker有一些基本了解,
  • 了解Docker如何帮您开发Go应用程序,
  • 知道如何为Go应用程序创建生产用Docker容器,并
  • 知道如何使用Semaphore将Docker容器持续部署到您的服务器。

前提要求

为了完成本文,您需要:

  • 在您的计算机和服务器上安装Docker,并具备
  • 一台可以使用SSH密钥对SSH请求进行身份验证的服务器。

理解Docker

Docker可以帮您为自己的应用程序创建一个单一的可部署“单位”。这样的单位也叫做容器,其中包含了应用程序需要的一切。例如代码(或二进制文件)、运行时、系统工具,以及系统库文件。将所有这些需要的内容打包为一个单一的单位,可确保无论将应用程序部署在何处,都能提供完全相同的环境。这种技术还可以帮您维持完全一致的开发和生产环境,通常这些环境是很难被追踪的。

一旦搭建完成,容器的创建和部署将可自动进行。这本身就可以避免一系列问题。这些问题中大部分都是因为文件不同步,或开发和生产环境之间的差异导致的。Docker可以解决这些问题。

相对于虚拟机的优势

容器提供了与虚拟机类似的资源分配和隔离等好处。然而相似之处仅此而已。

虚拟机需要自己的来宾操作系统,容器则能与宿主操作系统共享内核。这意味着容器更轻,需要的资源更少。虚拟机从本质上来说,实际上就是一个操作系统内部运行的另一个操作系统。然而容器更像是操作系统内部运行的一个应用程序。通常容器需要的资源(内存、磁盘空间等)远低于虚拟机,同时启动速度也比虚拟机快很多。

在开发过程中使用Docker所获得的收益

在开发工作中使用Docker可以获得的部分收益包括:

  • 所有团队成员共同使用一个标准的开发环境,
  • 集中更新依存组件,在任何位置使用相同的容器,
  • 从开发到生产可以使用完全相同的环境,并且
  • 更易于修复只可能在生产环境中遇到的潜在问题。

为何通过Docker使用Go Web应用程序?

大部分Go应用程序都是简单的二进制文件。这就引出了另一个问题 - 为何通过Docker使用Go应用程序?通过Docker使用Go的部分原因包括:

  • Web应用程序通常包含模板和配置文件,Docker有助于确保这些文件在库中保持完全同步。
  • Docker能为开发和生产提供完全相同的环境。很多人经常遇到某个应用程序在开发环境中运行正常,但发布至生产环境中无法运行。使用Docker后将不再需要担心此类问题。
  • 在大型团队中,不同成员的计算机、操作系统,以及所安装的软件可能存在非常大的差异。Docker提供了一种确保整个开发环境保持一致的机制。团队成员可以更高效,并可减少开发过程中的冲突和其他本可避免的问题。

创建一个简单的Go Web应用程序

我们将使用Go创建一个简单的Web应用程序作为本文的范例。这个我们称之为MathApp的应用程序可以:

  • 暴露不同数学运算的过程,
  • 使用HTML模板创建视图,
  • 使用配置文件对应用程序进行定制,并
  • 针对所选功能提供测试。

访问/sum/3/6会打开一个显示了36相加后结果的页面。同理,访问/product/3/6会打开一个显示了36相乘后结果的页面。

本文中我们使用了Beego框架。请注意,您自己的应用程序可以使用任何框架(或者完全不使用)。

最终的目录结构

完成后的MathApp其目录结构应该是类似这样的:

MathApp
├── conf
│   └── app.conf
├── main.go
├── main_test.go
└── views
    ├── invalid-route.html
    └── result.html

我们会假设MathApp目录位于/app目录下。

程序主文件是位于应用程序根目录的main.go,这个文件中包含了应用的所有功能。main.go的部分功能可使用main_test.go进行测试。

views文件夹包含视图文件invalid-route.htmlresult.html。配置文件app.conf位于conf文件夹中。Beego可使用该文件对应用程序进行定制。

应用程序文件的内容

应用程序主文件(main.go)包含应用程序的所有逻辑。该文件的内容如下:

// main.go

package main

import (
    "strconv"

    "github.com/astaxie/beego"
)

// The main function defines a single route, its handler
// and starts listening on port 8080 (default port for Beego)
func main() {
    /* This would match routes like the following:
/sum/3/5
/product/6/23
...
*/
    beego.Router("/:operation/:num1:int/:num2:int", &mainController{})
    beego.Run()
}

// This is the controller that this application uses
type mainController struct {
    beego.Controller
}

// Get() handles all requests to the route defined above
func (c *mainController) Get() {
    //Obtain the values of the route parameters defined in the route above
    operation := c.Ctx.Input.Param(":operation")
    num1, _ := strconv.Atoi(c.Ctx.Input.Param(":num1"))
    num2, _ := strconv.Atoi(c.Ctx.Input.Param(":num2"))

    //Set the values for use in the template
    c.Data["operation"] = operation
    c.Data["num1"] = num1
    c.Data["num2"] = num2
    c.TplName = "result.html"

    // Perform the calculation depending on the 'operation' route parameter
    switch operation {
    case "sum":
        c.Data["result"] = add(num1, num2)
    case "product":
        c.Data["result"] = multiply(num1, num2)
    default:
        c.TplName = "invalid-route.html"
    }
}

func add(n1, n2 int) int {
    return n1 + n2
}

func multiply(n1, n2 int) int {
    return n1 * n2
}

在您的应用程序中,这些内容可能分散保存在多个文件中。但是出于演示的用途,我们希望尽量确保内容足够简单。

测试文件的内容

main.go文件包含一些需要测试的功能。对这些功能的测试位于main_test.go,这个文件的内容如下:

// main_test.go

package main

import "testing"

func TestSum(t *testing.T) {
    if add(2, 5) != 7 {
        t.Fail()
    }
    if add(2, 100) != 102 {
        t.Fail()
    }
    if add(222, 100) != 322 {
        t.Fail()
    }
}

func TestProduct(t *testing.T) {
    if multiply(2, 5) != 10 {
        t.Fail()
    }
    if multiply(2, 100) != 200 {
        t.Fail()
    }
    if multiply(222, 3) != 666 {
        t.Fail()
    }
}

如果希望进行持续部署,应用程序的测试就显得尤为有用。如果您已经准备好相应的测试,即可在无需担心为应用程序引入错误的情况下进行持续部署。

视图文件的内容

视图文件其实是HTML模板。应用程序可以使用这些文件显示对请求做出的回应。result.html的内容如下:

<!-- result.html -->
<!-- This file is used to display the result of calculations -->
<!doctype html>
<html>

    <head>
        <title>MathApp - {{.operation}}</title>
    </head>

    <body>
        The {{.operation}} of {{.num1}} and {{.num2}} is {{.result}}
    </body>

</html>

invalid-route.html的内容如下:

<!-- invalid-route.html -->
<!-- This file is used when an invalid operation is specified in the route -->
<!doctype html>
<html>

    <head>
        <title>MathApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta charset="UTF-8">
    </head>

    <body>
        Invalid operation
    </body>

</html>

配置文件的内容

Beego可以使用app.conf文件配置应用程序,该文件的内容如下:

; app.conf
appname = MathApp
httpport = 8080
runmode = dev

在这个文件中,

  • appname是运行该应用程序的进程对应的名称,
  • httpport是应用程序使用的端口,而
  • runmode决定了应用程序的运行模式。可用值包括对应开发环境的dev,以及对应生产环境的prod

在开发过程中使用Docker

本节将介绍在开发过程中使用Docker所能获得的收益,并会指导您完成在开发过程中使用Docker的步骤。

针对开发工作配置Docker

我们会使用一个Dockerfile配置开发工作所用的Docker。配置工作需要满足的开发环境要求如下:

  • 我们会使用上文提到的应用程序,
  • 文件必须能够从容器内部和外部访问,
  • 我们将使用beego包含的bee工具。该工具可用于在开发过程中实时重载(Live reload)应用(应用位于Docker容器内部),
  • Docker将通过8080端口暴露该应用程序,
  • 在我们的计算机上,该应用程序位于/app/MathApp
  • 在Docker容器中,该应用程序位于/go/src/MathApp
  • 我们为开发工作创建的Docker映像名为ma-image,并且
  • 开发过程中所运行的Docker容器的名称为ma-instance

第1步 - 创建Dockerfile

满足上述要求的Dockerfile内容如下:

FROM golang:1.6

# Install beego and the bee dev tool
RUN go get github.com/astaxie/beego && go get github.com/beego/bee

# Expose the application on port 8080
EXPOSE 8080

# Set the entry point of the container to the bee command that runs the
# application and watches for changes
CMD ["bee", "run"]

第一行,

FROM golang:1.6

使用Go的官方映像作为基础映像。这个映像是Go 1.6预安装的。该映像的$GOPATH值已被设置为/go。所有安装在/go/src的程序包都能通过go命令访问。

第二行,

RUN go get github.com/astaxie/beego && go get github.com/beego/bee

安装beego程序包和bee工具。beego程序包将在应用程序内部使用,bee工具将用于在开发过程中实时重载代码。

第三行,

EXPOSE 8080

通过开发计算机上容器的8080端口暴露该应用程序。最后一行,

CMD ["bee", "run"]

使用bee命令开始对我们的应用程序进行实时重载。

第2步 - 构建映像

创建好Docker文件之后,可运行下列命令创建映像:

docker build -t ma-image .

执行上述命令可创建一个名为ma-image的映像。随后所有负责开发这个应用程序的人都可以使用这个映像。这样即可确保整个团队获得完全一致的开发环境。

要查看您系统中的映像列表,请运行下列命令:

docker images

执行该命令可以看到类似下面的内容:

REPOSITORY  TAG     IMAGE ID      CREATED         SIZE
ma-image    latest  8d53aa0dd0cb  31 seconds ago  784.7 MB
golang      1.6     22a6ecf1f7cc  5 days ago      743.9 MB

请注意,实际的映像名称和数量可能各不相同,不过您至少应该可以在列表中看到golangma-image映像。

第3步 - 运行容器

准备好ma-image之后,即可使用下列命令启动一个容器:

docker run -it --rm --name ma-instance -p 8080:8080 \
   -v /app/MathApp:/go/src/MathApp -w /go/src/MathApp ma-image

分别来看看上述命令的作用。

  • docker run命令可用于通过映像运行容器,
  • -it标记可以用交互式模式启动该容器,
  • --rm标记可以在容器关闭后清理其中的内容,
  • --name ma-instance可以将容器命名为ma-instance
  • -p 8080:8080标记使得容器可以通过8080端口访问,
  • -v /app/MathApp:/go/src/MathApp略微复杂,可以将/app/MathApp从计算机映射至容器的/go/src/MathApp目录。这样可以确保在容器内部和外部均可访问这些开发文件,并且
  • ma-image部分指定了容器内部使用的映像名称。

执行上述命令可启动Docker容器。这个容器可以将您的应用程序暴露到8080端口,还可以在您修改代码后自动重新构建您的应用程序。您将在控制台中看到下列输出结果:

bee   :1.4.1
beego :1.6.1
Go    :go version go1.6 linux/amd64

2016/04/10 13:04:15 [INFO] Uses 'MathApp' as 'appname'
2016/04/10 13:04:15 [INFO] Initializing watcher...
2016/04/10 13:04:15 [TRAC] Directory(/go/src/MathApp)
2016/04/10 13:04:15 [INFO] Start building...
2016/04/10 13:04:18 [SUCC] Build was successful
2016/04/10 13:04:18 [INFO] Restarting MathApp ...
2016/04/10 13:04:18 [INFO] ./MathApp is running...
2016/04/10 13:04:18 [asm_amd64.s:1998][I] http server Running on :8080

若要检查整个环境,请通过浏览器访问http://localhost:8080/sum/4/5。您应该能看到类似下图的结果:

注意:上述步骤假设您在自己的本地计算机上执行操作。

第4步 - 开发应用程序

随后一起来看看这种做法能对开发工作起到什么帮助。执行下文操作的过程中请确保容器始终处于运行状态。在main.go文件中,将第#34行从

c.Data["operation"] = operation

修改为

c.Data["operation"] =  "real " + operation

保存改动的同时,您应该能看到类似下面的内容:

2016/04/10 13:17:51 [EVEN] "/go/src/MathApp/main.go": MODIFY
2016/04/10 13:17:51 [SKIP] "/go/src/MathApp/main.go": MODIFY
2016/04/10 13:17:52 [INFO] Start building...
2016/04/10 13:17:56 [SUCC] Build was successful
2016/04/10 13:17:56 [INFO] Restarting MathApp ...
2016/04/10 13:17:56 [INFO] ./MathApp is running...
2016/04/10 13:17:56 [asm_amd64.s:1998][I] http server Running on :8080

要查看这些改动,请用浏览器访问http://localhost:8080/sum/4/5。您应该能看到类似下图的结果:

如您所见,保存改动后,您的应用程序可以自动构建并开始提供服务。

在生产环境中使用Docker

本节将介绍如何在Docker容器中部署Go应用程序。我们将使用Semaphore实现下列目标:

  • 改动的代码推送到Git代码库后自动进行构建,
  • 自动运行测试,
  • 如果构建成功并通过测试,创建一个Docker映像,
  • 将Docker映像推送至Docker Hub,并
  • 更新服务器以使用最新的Docker映像。

为生产环境创建Dockerfile

在开发过程中,我们的目录结构如下:

MathApp
├── conf
│   └── app.conf
├── main.go
├── main_test.go
└── views
    ├── invalid-route.html
    └── result.html

由于我们希望为这个项目构建Docker映像,因此需要创建一个在生产环境中使用的Dockerfile。请在项目的根目录下创建Dockerfile,随后新的目录结构应该是类似下面这样的:

MathApp
├── conf
│   └── app.conf
├── Dockerfile
├── main.go
├── main_test.go
└── views
    ├── invalid-route.html
    └── result.html

在Dockerfile中输入下列内容:

FROM golang:1.6

# Create the directory where the application will reside
RUN mkdir /app

# Copy the application files (needed for production)
ADD MathApp /app/MathApp
ADD views /app/views
ADD conf /app/conf

# Set the working directory to the app directory
WORKDIR /app

# Expose the application on port 8080.
# This should be the same as in the app.conf file
EXPOSE 8080

# Set the entry point of the container to the application executable
ENTRYPOINT /app/MathApp

详细看看上述每条命令的作用。第一条命令,

FROM golang:1.6

指定了在开发过程中所用的同一个golang:1.6映像基础之上构建映像。第二条命令,

RUN mkdir /app

可以在容器的根目录下创建名为app的目录。这个目录将用于保存项目文件。第三组命令,

ADD MathApp /app/MathApp
ADD views /app/views
ADD conf /app/conf

可以将库、视图文件夹,以及配置文件夹从计算机复制到映像内的应用程序文件夹中。第四条命令,

WORKDIR /app

可将映像的工作目录设置为/app。第五条命令,

EXPOSE 8080

可以从容器中暴露8080端口。这个端口应该与应用程序的app.conf文件中指定的端口完全一致。最后一条命令,

ENTRYPOINT /app/MathApp

可将映像的入口点(Entry point)设置为应用程序的二进制文件。这样即可启动该二进制文件,并开始在8080端口提供服务。

自动构建和测试

一旦将改动的代码推送至代码库,Semaphore就可以帮您轻松地自动构建并测试代码。这里介绍了如何添加您的GitHub或Bitbucket项目,以及在Semaphore上设置Golang项目的方法。

Go项目的默认配置可解决下列问题:

  • 抓取依存项,
  • 构建项目,并
  • 运行测试。

完成上述过程后,便可通过Semaphore仪表板查看最新构建的状态并对其进行测试。如果构建或测试失败,整个过程将暂停,且不部署任何内容。

通过Semaphore为自动化开发创建初始环境

设置好构建过程后,下一步需要配置部署过程。若要部署应用程序,您需要:

  1. 构建Docker映像,
  2. 将Docker映像推送至Docker Hub,并
  3. 更新服务器以拉取新的映像,随后据此启动一个新的Docker容器。

首先我们需要在Semaphore上设置项目以进行持续部署

前三个步骤相对较为简单:

  • 选择部署方法,
  • 选择部署策略,并
  • 选择部署过程所使用的代码库分支。

第4步(设置部署命令)我们将使用下一节介绍的命令。目前可以留空并执行下一个步骤。

第5步,输入服务器上用户的SSH密钥。这样即可在无需密码的情况下,以安全的方式在服务器上执行某些部署命令。

第6步,可以为服务器命名。如果不指定名称,Semaphore会为服务器分配类似server-1234这样的随机名称。

在服务器上设置更新脚本

接下来需要设置部署过程,这样Semaphore即可构建新映像并将其上传至Docker Hub。设置完成后,Semaphore将通过一条命令在服务器上执行脚本,发起更新过程。

为此我们需要将下列文件以update.sh为名放置在服务器上。

#!/bin/bash

docker pull $1/ma-prod:latest
if docker stop ma-app; then docker rm ma-app; fi
docker run -d -p 8080:8080 --name ma-app $1/ma-prod
docker rmi $(docker images --filter "dangling=true" -q --no-trunc)

使用下列命令使得该文件可以执行:

chmod +x update.sh

随后来看看这个文件是如何生效的。这个脚本可以接受单一参数,并在自己的命令中使用这个参数。这个参数可以是您在Docker Hub上的用户名。例如可以通过下面这样的格式使用该命令:

./update.sh docker_hub_username

为了理解具体的用途,随后再来看看文件中的每条命令。

第一条命令,

docker pull $1/ma-prod:latest

将最新映像从Docker Hub拉取到服务器上。如果您在Docker Hub上的用户名是demo_user,这条命令将从Docker Hub拉取名为demo_user/ma-prod,且被标记为latest的映像。

第二条命令,

if docker stop ma-app; then docker rm ma-app; fi

可以停止并移除任何曾以ma-app为名启动的容器。

第三条命令,

docker run -d -p 8080:8080 --name ma-app $1/ma-prod

使用能够反映最新构建中所有变动的最新映像启动一个新的容器(名为ma-app)。

最后一条命令,

docker rmi $(docker images --filter "dangling=true" -q --no-trunc)

可从服务器上删除任何未使用的映像。这个清理工作可以确保服务器整洁,并能降低磁盘空间的使用。

注意:这个文件必须放置在上一步骤中所用SSH密钥对应用户的根目录下。如果文件位置有变化,下文使用的部署命令也需要酌情更新。

设置项目配合Docker工作

默认情况下,Semaphore上的新项目会使用Ubuntu 14.04 LTS v1603平台。这个平台并非Docker自带的。因为我们感兴趣的是Docker的使用,因此需要更改Semaphore的平台设置以使用Ubuntu 14.04 LTS v1603(支持Docker的Beta测试版)平台。

设置环境变量

为了在开发过程中以安全的方式使用Docker Hub,我们需要将自己的凭据存储在Semaphore自动初始化出来的环境变量内。

我们需要存储下列变量:

  • DH_USERNAME - Docker Hub用户名
  • DH_PASSWORD - Docker Hub密码
  • DH_EMAIL - Docker Hub邮件地址

可以参考这里了解如何用安全的方式设置环境变量

设置部署命令

虽然已经完成初始设置,但目前还无法进行部署。原因在于还没有配置需要运行的命令。

首先我们需要输入完成部署过程所需的命令,为此请在Semaphore上打开您的项目首页。

">

在这个页面上,点击Servers选项下的服务器名称,随后可以看到下列界面:

点击页面右侧,页头下方的Edit server按钮。

在下列页面上,我们需要注意底部名为Deploy commands的选项。点击其中的Change deploy commands链接,开始编辑要运行的命令。

在编辑框中输入下列命令,并点击Save Deploy Commands按钮:

go get -v -d ./
go build -v -o MathApp
docker login -u $DH_USERNAME -p $DH_PASSWORD -e $DH_EMAIL
docker build -t ma-prod .
docker tag ma-prod:latest $DH_USERNAME/ma-prod:latest
docker push $DH_USERNAME/ma-prod:latest
ssh -oStrictHostKeyChecking=no your_server_username@your_ip_address "~/update.sh $DH_USERNAME"

注意:请将上述命令中your_server_username@your_ip_address内容替换为您的实际值。

随后一起来仔细看看上述这些命令的用途。

前两条命令go getgo build是标准的Go命令,分别用于拉取依存项和构建项目。请注意go build命令指定的可执行文件的名称应该是MathApp。这个名称应该与Dockerfile中所用名称一致。

第三条命令,

docker login -u $DH_USERNAME -p $DH_PASSWORD -e $DH_EMAIL

使用(上文操作中设置的)环境变量与Docker Hub进行身份验证,这样我们才可以推送最新映像。第四条命令,

docker build -t ma-prod .

根据最新代码基构建一个名为ma-prod的Docker映像。第五条命令,

docker tag ma-prod:latest $DH_USERNAME/ma-prod:latest

为最新创建的映像添加your_docker_hub_username/ma-prod:latest标签。这样我们就可以将该映像推送至Docker Hub上相应的代码库中。第六条命令,

docker push $DH_USERNAME/ma-prod:latest

可将该映像推送至Docker Hub。最后一条命令,

ssh -oStrictHostKeyChecking=no your_server_username@your_ip_address "~/update.sh $DH_USERNAME"

使用ssh命令登录到您的服务器,执行我们在上文步骤中创建的update.sh脚本。这个脚本可以从Docker Hub获取最新映像,并据此启动一个新的容器。

部署应用程序

截至目前我们并未真正将应用程序部署到服务器,因此先来进行手工部署。请注意这一操作并非是必须的。当您下一次将改动的代码推送至代码库后,只要构建和测试都能成功完成,Semaphore将自动部署您的应用程序。我们这里进行手工部署只是为了测试一切都能正常工作。

您可以阅读 Semaphore文档了解如何在构建页面手工部署应用程序。

应用程序部署完毕后,可通过下列地址访问:

http://your_ip_address:8080/sum/4/5

访问结果应该类似下图所示:

结果与开发过程中看到的完全相同。唯一的差异在于此处访问时使用的URL并不是localhost,而是服务器的IP地址。

对配置进行测试

至此已配置了自动构建和部署过程,我们的工作流也得以大幅简化。让我们对代码进行一些细小的改动,然后看看服务器上的应用程序是如何自动更新以反映这些改动的。

试试看将文字的颜色由黑色改为红色。为此请在views/result.html文件中,将第#8行由

    <body>

改为

    <body style="color: red">

随后保存文件,并在您的应用程序目录中使用下列命令提交改动:

git add views/result.html
git commit -m 'Change the color of text from black (default) to red'

使用下列命令将改动推送至代码库:

git push origin master

git push命令成功执行后,Semaphore会检测到代码库中的改动,并自动开始构建过程。构建过程(包括测试过程)成功完成后,Semaphore将启动部署过程。Semaphore仪表板会实时显示构建和部署过程的状态。

一旦Semaphore仪表板显示构建和部署过程均已完成,请刷新下列页面:

http://your_ip_address:8080/sum/4/5

随后您将可以看到类似下图的结果:

结论

在这篇文章中,我们了解到如何为Go应用程序创建Docker容器,并使用Semaphore将Docker容器部署至服务器。

现在您已经可以使用Docker简化下一个Go应用程序的开发工作。如果您有任何问题,欢迎发布到下方的评论区。

作者

Kulshekhar Kabra是一位独立开发者,他的工作可以方便地接触到各种新技术,并将其运用到新项目中。只要工作之余有闲时间,他还很喜欢撰写技术文章并制作视频教程。

阅读英文原文How To Deploy a Go Web Application with Docker


感谢陈兴璐对本文的策划和审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT