标签 Package 下的文章

写Go代码时遇到的那些问题[第1期]

程序员步入“大龄”,写代码的节奏也会受到影响。以前是长时间持续地写,现在写代码的节奏变成了“波浪形”:即写一段时间,歇一段时间。当然这里的“歇”并不是真的歇,而是做其他事情了,比如:回顾、整理与总结。

平时写Go代码,时不时就遇到一些问题,或是写出一些让自己还算满意的代码,这里全部列为“问题”行列。这些“问题”(以及其解决方法)往往比较“小”、比较“碎片”,不适合以自己“擅长”的“长篇”风格写出来分享,也不知道以什么样的“题目”去分享更好,但这样的“问题”在日常又总是会遇到。考量来考量去,赶脚还是用一系列的文章去分享比较合适,即每隔一段时间,积累了一些问题后,就写一篇文章分享一下。

这是第一篇,后续不确定时间地(注意:这不是weekly的哦)发布新篇,直到没啥可写了或不写Go代码了^0^。

一、Go包管理

首当其冲的是Go包管理

1. vendor的“传染性”带来的问题

Go从1.5版本开始引入vendor机制以辅助Go的包管理。随着vendor机制的应用日益广泛,我们会发现:有些时候你要是不用vendor(在不借助第三方包管理工具的前提下),很多编译问题是解决不了的,或者说vendor机制有一定的传染性。比如下面这个例子:

img{512x368}

如上图所示:app_c包直接调用lib_a包中函数,并使用了lib_b包(v0.2版本)中的类型,lib_a包vendor了lib_b包(v0.1版本)。在这样的情况下,当我们编译app_c包时,是否会出现什么问题呢?我们一起来看一下这个例子:

在$GOPATH/src路径下面我们查看当前示例的目录结构:

$tree
├── app_c
    ├── c.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go

各个源文件的示例代码如下:

//lib_a/a.go
package lib_a

import "lib_b"

func Foo(b lib_b.B) {
    b.Do()
}

//lib_a/vendor/lib_b/b.go

package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.1")
}

// lib_b/b.go
package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.2")
}

// app_c/c.go
package app_c

import (
    "lib_a"
    "lib_b"
)

func main() {
    var b lib_b.B
    lib_a.Foo(b)
}

进入app_c目录,执行编译命令:

$go build c.go
# command-line-arguments
./c.go:10:11: cannot use b (type "lib_b".B) as type "lib_a/vendor/lib_b".B in argument to lib_a.Foo

我们看到go compiler认为:app_c包main函数中定义的变量b的类型(lib_b.B)与lib_a.Foo的参数b的类型(lib_a/vendor/lib_b.B)是不同的类型,不能相互赋值

2. 通过手工vendor解决上述问题

这个例子非常有代表性,那么怎么解决这个问题呢?我们需要在app_c中也使用vendor机制,即将app_c所需的lib_a和lib_b都vendor到app_c中。

按照上述思路解决后的示例的目录结构:

$tree
├── app_c
    ├── c.go
    └── vendor
        ├── lib_a
        │   └── a.go
        └── lib_b
            └── b.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go

不过要注意的是:app_c/vendor下面的库中的vendor目录要被删除掉的,我们只保留顶层vendor。现在我们再来编译c.go就可以顺利编译通过了。

3. 使用dep

对于demo或规模不大、依赖不多的小项目,手工进行vendor还是蛮有效的。一个可行的手工vendor步骤:

  • 在项目顶层创建vendor;
  • 通过go list -json ./…查看项目依赖 “deps”;
  • 逐一下载各个依赖,并确定要使用的版本(tag or branch),将特定版本cp到顶层的vendor目录下,至少要做到vendor所有直接依赖包;
  • 可以在顶层vendor下创建dependencies.list文件,手工记录vendor的依赖包列表以及版本信息。

但是对于稍大一点的项目,手工vendor就会费时费力,有时仅能顾及到“直接依赖包”的vendor,“数不清”的间接依赖/传递依赖会让你头疼不已。这个时候我们会想到使用第三方的包管理工具。在现在这个时间点,如果你再和我提godepglide等,那你就out了,dep是首选。

《初窥dep》一文中,我们对当时的dep进行了较为详细的工作机制分析,如今dep已经演化到0.3.2版本了,并且commandline交互接口已经稳定了。dep init默认采用network mode,即到各个依赖包的upstream上查找版本信息并下载;dep init也支持-gopath模式,即在本地$GOPATH下获取依赖包的元信息并分析。

不过,对于在国内的gopher,dep init的过程依然是一道很难逾越的“坎”。问题多出在:第三方包特别喜欢依赖的golang.org/x下的那些包,常见的包有:net、text、crypto等。golang.org/x/{package_name}仅仅是canonical import path,真正的代码存储在go.googlesource.com上,而在国内get这些包,我们会得到如下错误:

$go get -u golang.org/x/net
package golang.org/x/net: unrecognized import path "golang.org/x/net" (https fetch: Get https://golang.org/x/net?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

这将导致dep init命令长期阻塞,给国内gopher带来极为糟糕的体验。更为糟糕的是,即便是采用了一些fan qiang方式,有些时候go.googlesource.com依旧无法连接。因此,我一般的作法是在国外的主机上进行dep init,然后将vendor checkin到代码仓库中。这样其他人在得到你的代码后,也不需dep ensure(也要下载依赖包)即可实现reproducable build。

有些朋友可能会将从github.com/golang上下载的net包来代替golang.org/x/net,并使用dep init -v -gopath=true的模式。但这种替换会被dep分析出来,因为dep会尝试去读取代码库的元信息,结果依然会是失败。

二. 非容器化应用的本地日志管理

微服务、容器化大行其道的今天,单个应用的日志处理变得简单化了,应用只需要将要输出的信息输出到stdout、stderr上即可。logging基础设施会收集容器日志,并做后续归档、分析、过滤、查找、展示等处理。但是在非容器环境、在没有统一的logging基础设施的前提下,日志的管理就又交还给应用自身了。浅显的日志管理至少要包含日志的rotate(轮转)、压缩归档以及历史归档文件的处理吧。这里我们就来探讨一下这个问题的几种解决方法。

1. 托管给logrotate

在主流的Linux发行版上都有一个logrotate工具程序,应用程序可以借助该工具对应用输出的日志进行rotate、压缩、归档和删除历史归档日志,这样可大幅简化应用的日志输出逻辑,应用仅需要将日志输出到一个具名文件中即可,其余都交给logrotate处理。

我们建立一个输出log的demo app:

//testlogrotate.go

package main

import (
    "log"
    "os"
    "time"
)

func main() {
    file, err := os.OpenFile("./app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln("Failed to open log file:", err)
    }
    defer file.Close()

    logger := log.New(file,
        "APP_LOG_PREFIX: ",
        log.Ldate|log.Ltime|log.Lshortfile)

    for {
        logger.Println("test log")
        time.Sleep(time.Second * 1)
    }
}

该程序每隔1s向app.log文件写入一行日志。

# tail -f app.log
APP_LOG_PREFIX: 2018/01/12 19:14:43 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:44 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:45 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:46 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:47 testlogrotate.go:22: test log
... ..

接下来,我们就要用logrotate对该app.log文件进行定期的rotate、压缩归档以及历史归档清理了,我们需要为app.log定制一个配置。logrotate读取配置的目录是/etc/logrotate.d,我们在/etc/logrotate.d下面建立applog文件(当然你也可以在任意其他目录下建立配置文件,不过其他目录下的配置文件无法被logrotate的cron任务感知到,不过这样的配置文件可以手工与logrotate程序结合使用),文件内容如下:

# cat /etc/logrotate.d/applog

/data/tonybai/test/go/app.log {
  rotate 7
  daily
  size=10M
  compress
  dateext
  missingok
  copytruncate
}

这个配置的大致含义是:
* 每天rotate一次
* 日志保留7天(rotate=7, daily rotate)
* 归档日志采用压缩形式
* 归档日志带有时间戳
* 当当前日志size > 10M时,会进行一次rotate
* 最重要的是copytruncate这个配置,这个配置的含义是将app.log当前日志copy到一个归档文件后,对app.log进行truncate操作,这样app.log的open file fd并不改变,不会影响到原app继续写日志。当然这个copy的过程中可能会有少量日志lost。

如果你觉得logrotate在时间粒度和精确度上依旧无法满足你的要求,你可以结合crontab自己定时执行logrotate(crontab -e编辑crontab的配置):

# logrotate -f /etc/logrotate.d/applog

下面是rotate时,tail -f中看到的情况:

APP_LOG_PREFIX: 2018/01/12 20:25:59 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:00 testlogrotate.go:21: test log
tail: app.log: file truncated
APP_LOG_PREFIX: 2018/01/12 20:26:01 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:02 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:03 testlogrotate.go:21: test log

可以看到tail可以检测到file truncate事件。

2. 使用自带rotate功能log包

在go技术栈中众多的logging包中,logrus是使用较为广泛的一个包,支持与std库 log API兼容的结构化日志、支持logging level设置、支持安全地并发写日志以及hook等。但logrus自身并不具备auto rotate功能,需要结合其他工具才能实现。这里用nate finchlumberjack,我们来看一个简单的例子:

// testlogrusAndlumberjack.go

package main

import (
    "time"

    "github.com/natefinch/lumberjack"
    log "github.com/sirupsen/logrus"
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &log.JSONFormatter{}

    logger.Out = &lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    for {
        logger.Debug("this is an app log")
        time.Sleep(2 * time.Millisecond)
    }
}

从代码里,我们看到:通过设置logger.Out为一个lumberjack.Logger的实例,将真正的Write交给了lumberjack.Logger,而后者实现了log的rotate功能,与logrotate的配置有些类似,这里也包括日志最大size设定、保留几个归档日志、是否压缩、最多保留几天的日志。不过当前lumberjack实现的rotate判断条件仅有一个:MaxSize,而没有定时rotate的功能。

我们执行一下该程序,等待一会,并停止程序。可以看到目录下的日志文件发生了变化:

$ls -lh
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:03 app-2018-01-12T21-03-42.844.log.gz
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:04 app-2018-01-12T21-04-15.017.log.gz
-rw-r--r--  1 tony  staff   457K Jan 12 21:04 app.log

lumberjack每发现app.log大于MaxSize就会rotate一次,这里已经有了两个归档压缩文件,并被lumberjack赋予了时间戳和序号,便于检索和查看。

3. 关于对日志level的支持以及loglevel的热更新

对日志level的支持是logging包选项的一个重要参考要素。logrus支持设置六个log level:

    PanicLevel
    FatalLevel
    ErrorLevel
    WarnLevel
    InfoLevel
    DebugLevel

并且对不同的leve的日志,logrus支持设定hook分别处理,比如:放到不同的日志文件中。通过logrus.Logger.SetLevel方法可以运行时更新logger实例的loglevel,这个特性可以让我们在生产环境上通过临时打开debuglevel日志对程序进行更细致的观察,以定位问题,快速定位bug,非常实用。

结合系统Signal机制,我们可以通过USR1和USR2两个signal来运行时调整程序的日志级别,我们来看一个示例:

img{512x368}

从上面图片可以看到,日志级别从高到低分别为:Panic, Fatal, Error, Warn,Info和Debug。如果要调高log level,我们向程序发送USR1来调高日志级别,相反,发送USR2来调低日志级别:

我们在testlogrusAndlumberjack.go上面做些修改:增加对signal: USR1和USR2的监听处理,同时循环打印各种级别日志,以后续验证日志级别的动态调整:

// testloglevelupdate.go

import (
    log "github.com/sirupsen/logrus"
    ... ...
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &log.JSONFormatter{}

    logger.Out = &lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2)
    go watchAndUpdateLoglevel(c, logger)

    for {
        logger.Debug("it is debug level log")
        logger.Info("it is info level log")
        logger.Warn("it is warning level log")
        logger.Error("it is warning level log")
        time.Sleep(5 * time.Second)
    }
}

watchAndUpdateLoglevel函数用于监听程序收到的系统信号,并根据信号类型调整日志级别:

// testloglevelupdate.go
func watchAndUpdateLoglevel(c chan os.Signal, logger *log.Logger) {
    for {
        select {
        case sig := <-c:
            if sig == syscall.SIGUSR1 {
                level := logger.Level
                if level == log.PanicLevel {
                    fmt.Println("Raise log level: It has been already the most top log level: panic level")
                } else {
                    logger.SetLevel(level - 1)
                    fmt.Println("Raise log level: the current level is", logger.Level)
                }

            } else if sig == syscall.SIGUSR2 {
                level := logger.Level
                if level == log.DebugLevel {
                    fmt.Println("Reduce log level: It has been already the lowest log level: debug level")
                } else {
                    logger.SetLevel(level + 1)
                    fmt.Println("Reduce log level: the current level is", logger.Level)
                }

            } else {
                fmt.Println("receive unknown signal:", sig)
            }
        }
    }
}

运行该程序后,你可以通过如下命令向程序发送信号:

$ kill -s USR1|USR2 程序的进程号

通过日志的输出,可以判断出日志级别调整是否生效,这里就不细说了。

不过这里还要提一点的是logrus目前对于输出的日志中双引号内的一些字符(比如双引号自身)会做转义处理,即在前面加上“反斜杠”,比如:

{"level":"debug","msg":"receive a msg: {\"id\":\"000002\",\"ip\":\"201.108.111.117\"}","time":"2018-01-11T20:42:31+08:00"}

这个问题让日志可读性大幅下降,但这个问题似乎尚处于无解状态

三. json marshal json string时的转义问题

之前写过这样一个function,用于统一marshal内部组件通信的应答消息:

func marshalResponse(code int, msg string, result interface{}) (string, error) {
    m := map[string]interface{}{
        "code":   0,
        "msg":    "ok",
        "result": result,
    }

    b, err := json.Marshal(&m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

不过当result类型为json string时,这个函数的输出带有转义反斜线:

//testmarshaljsonstring.go
... ...
func main() {
    s, err := marshalResponse(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response error:", err)
        return
    }
    fmt.Println(s)
}

运行这个程序输出:

{"code":0,"msg":"ok","result":"{\"name\": \"tony\", \"city\": \"shenyang\"}"}

怎么解决掉这个问题呢?json提供了一种RawMessage类型,本质上就是[]byte,我们将json string转换成RawMessage后再传给json.Marshal就可以解决掉这个问题了:

//testmarshaljsonstring.go
func marshalResponse1(code int, msg string, result interface{}) (string, error) {
    s, ok := result.(string)
    var m = map[string]interface{}{
        "code": 0,
        "msg":  "ok",
    }

    if ok {
        rawData := json.RawMessage(s)
        m["result"] = rawData
    } else {
        m["result"] = result
    }

    b, err := json.Marshal(&m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

func main() {
    s, err = marshalResponse1(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response1 error:", err)
        return
    }
    fmt.Println(s)
}

再运行这个程序的输出结果就变成了我们想要的结果了:

{"code":0,"msg":"ok","result":{"name":"tony","city":"shenyang"}}

四. 如何在main包之外使用flag.Parse后的命令行flag变量

我们在使用Go开发交互界面不是很复杂的command-line应用时,一般都会使用std中的flag包进行命令行flag解析,并在main包中校验和使用flag.Parse后的flag变量。常见的套路是这样的:

//testflag1.go
package main

import (
    "flag"
    "fmt"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

func usage() {
    fmt.Println("flagdemo-app is a daemon application which provides xxx service.\n")
    fmt.Println("Usage of flagdemo-app:\n")
    fmt.Println("\t flagdemo-app [options]\n")
    fmt.Println("The options are:\n")

    flag.PrintDefaults()
}

func main() {
    flag.Usage = usage
    flag.Parse()

   // ... ...
   // 这里我们可以使用endpoints、user、password等flag变量了
}

在这样的一个套路中,我们可以在main包中直接使用flag.Parse后的flag变量了。但有些时候,我们需要在main包之外使用这些flag vars(比如这里的:endpoints、user、password),怎么做呢,有几种方法,我们逐一来看看。

1. 全局变量法

我想大部分gopher第一个想法就是使用全局变量,即建立一个config包,包中定义全局变量,并在main中将这些全局变量绑定到flag的Parse中:

$tree globalvars
globalvars
├── config
│   └── config.go
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/globalvars/config/config.go

package config

var (
    Endpoints string
    User      string
    Password  string
)

// flag-demo/globalvars/etcd/etcd.go
package etcd

import (
    "fmt"

    "../config"
)

func EtcdProxy() {
    fmt.Println(config.Endpoints, config.User, config.Password)
    //... ....
}

// flag-demo/globalvars/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
    "./etcd"
)

func init() {
    flag.StringVar(&config.Endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&config.User, "user", "", "etcdv3 client user")
    flag.StringVar(&config.Password, "password", "", "etcdv3 client password")
}

.... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

可以看到,我们在绑定cmdline flag时使用的是config包中定义的全局变量。并且在另外一个etcd包中,使用了这些变量。

我们运行这个程序:

./main -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123

不过这种方法要注意这些全局变量值在Go包初始化过程的顺序,比如:如果在etcd包的init函数中使用这些全局变量,那么你得到的各个变量值将为空值,因为etcd包的init函数在main.init和main.main之前执行,这个时候绑定和Parse都还未执行。

2. 传参法

第二种比较直接的想法就是将Parse后的flag变量以参数的形式、以某种init的方式传给其他要使用这些变量的包。

$tree parampass
parampass
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/parampass/etcd/etcd.go
package etcd
... ...

func EtcdProxy(endpoints, user, password string) {
    fmt.Println(endpoints, user, password)
}

// flag-demo/parampass/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy(endpoints, user, password)

    time.Sleep(5 * time.Second)
}

这种方法非常直观,这里就不解释了。但注意:一旦使用这种方式,一定需要在main包与另外的包之间建立某种依赖关系,至少main包会import那些使用flag变量的包。

3. 配置中心法

全局变量法直观,而且一定程度上解除了其他包与main包的耦合。但是有一个问题,那就是一旦flag变量发生增减,config包就得相应添加或删除变量定义。是否有一种方案可以在flag变量发生变化时,config包不受影响呢?我们可以用配置中心法。所谓的配置中心法,就是实现一个与flag变量类型和值无关的通过配置存储结构,我们在main包中向该结构注入parse后的flag变量,在其他需要flag变量的包中,我们使用该结构得到flag变量的值。

$tree configcenter
configcenter
├── config
│   └── config.go
└── main.go

//flag-demo/configcenter/config/config.go
package config

import (
    "log"
    "sync"
)

var (
    m  map[string]interface{}
    mu sync.RWMutex
)

func init() {
    m = make(map[string]interface{}, 10)
}

func SetString(k, v string) {
    mu.Lock()
    m[k] = v
    mu.Unlock()
}

func SetInt(k string, i int) {
    mu.Lock()
    m[k] = i
    mu.Unlock()
}

func GetString(key string) string {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return ""
    }
    return v.(string)
}

func GetInt(key string) int {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return 0
    }
    return v.(int)
}

func Dump() {
    log.Println(m)
}

// flag-demo/configcenter/main.go

package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}
... ...
func main() {
    flag.Usage = usage
    flag.Parse()

    // inject flag vars to config center
    config.SetString("endpoints", endpoints)
    config.SetString("user", user)
    config.SetString("password", password)

    time.Sleep(5 * time.Second)
}

我们在main中使用config的SetString将flag vars注入配置中心。之后,我们在其他包中就可以使用:GetString、GetInt获取这些变量值了,这里就不举例了。

4、“黑魔法”: flag.Lookup

flag包中提供了一种类似上述的”配置中心”的机制,但这种机制不需要我们显示注入“flag vars”了,我们只需按照flag提供的方法在其他package中读取对应flag变量的值即可。

$tree flaglookup
flaglookup
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/flaglookup/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

......

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

// flag-demo/flaglookup/etcd/etcd.go
package etcd

import (
    "flag"
    "fmt"
)

func EtcdProxy() {
    endpoints := flag.Lookup("endpoints").Value.(flag.Getter).Get().(string)
    user := flag.Lookup("user").Value.(flag.Getter).Get().(string)
    password := flag.Lookup("password").Value.(flag.Getter).Get().(string)

    fmt.Println(endpoints, user, password)
}

运行该程序:

$go run main.go -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123

输出与我们的预期是一致的。

5、对比

我们用一幅图来对上述几种方法进行对比:

img{512x368}

很显然,经过简单包装后,“黑魔法”flaglookup应该是比较优异的方案。main包、other packages只需import flag即可。

注意:在main包中定义exported的全局flag变量并被其他package import的方法是错误的,很容易造成import cycle问题。并且任何其他package import main包都是不合理的

五. 小结

以上是这段时间遇到的、收集的一些Go问题以及solution。注意:这些solution不一定是最优方案哦!如果您有更好方案,欢迎批评指正和互动交流

本文章中涉及到的所有源码和配置文件在这里可以下载到。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

微信赞赏:
img{512x368}

Hello, ROS

ROS,全称是Robot Operating System,字面译为“机器人操作系统”。不过ROS并非是一个真正意义上的操作系统,而仅仅是一套用于机器人操作和控制软件开发的开发框架(framework),包括各种库和工具。

ROS在2007年诞生于斯坦福大学人工智能实验室Stanford Artificial Intelligence Laboratory,简称SAIL;2008年至2013年,ROS的开发和推广由Willow Garage公司(该公司2014年已关门大吉)主导。2013年8月,ROS的管理权转移给了Open Source Robotics Foundation。截至目前,ROS已经成为全世界使用和支持最为广泛的机器人开发框架之一。

一、ROS简介

ROS推出的初衷旨在降低机器人类软件开发的门槛,提高复用率,避免机器人软件开发人员的重复劳动,快速搭建机器人原型。因此,它采用了当时流行的面向服务SOA的软件架构,最大程度上降低内部耦合,并且易于被第三方扩展。采用C++作为主要开发语言,提高ROS的可移植性,让ROS可以很方便地移植到其他各种CPU体系和OS上。

ROS最初的是针对单机家用移动智能机器人而设计的,因此ROS1版本在以下几方面尚存不足:

  • 鲁棒性

ROS1版本运行时仅有一个master node,一旦master node发生crash,整个robot将无法正常工作。

  • 安全性

ROS1内部完全不设防,Node间通信完全是信赖的。任何Node都可以轻易得到其他node的各种topic数据、参数以及访问相关关键service。

  • 实时性

在ROS1的设计约束下,ROS内部各个节点间产生的实时数据通过master建立的内部网络在各个node间传递。一旦数据量很大,数据可能因内部网络通信性能问题而导致延迟,致使机器人工作异常。这也是ROS在工业机器人领域并未受到“热烈欢迎”的重要原因之一。

为了解决上述这些问题,ROS启动了ROS2的设计和实现。ROS2的第一个alpha版本发布于2015年,最新一个版本是今年七月份发布的beta2版本,ROS2的1.0版本计划将于今年年末正式发布。不过对于ROS2,笔者了解也不多,感兴趣的童鞋可以移步其wiki观看详情。

二、ROS1安装

在深入ROS1之前,我们先来安装一个ROS1。我们首先需要选择一个ROS1的发布版。ROS的发布模式与Ubuntu极其相似:每逢偶数年份发布一个长期支持版(LTS),support 5年;每逢奇数年份发布一个支持2年的版本。

并且ROS的发布版与Ubuntu发布版有着“神同步”:

2014:     ROS Indigo Igloo  对应  Ubuntu 14.04 LTS
2016:     ROS Kinetic Kame 对应 Ubuntu 16.04 LTS

ROS主要基于Ubuntu这款OS进行开发和测试,所以官方建议ROS尽量与Ubuntu一并使用,当然其他linux distribution也可以安装ROS,但正确性和稳定性ROS不能给予明确的保证。目前ROS1发布版的最新版本为:ROS Lunar Loggerhead,但官方推荐使用Ubuntu 16.04 + ROS Kinetic Kame组合;不过由于KK版本发布也就一年出头,市面上更多组织采用的可能还是Ubuntu 14.04 + ROS Indigo Igloo组合。

这里以Ubuntu 16.04+ ROS Kinetic Kame简单说明一下ROS1的安装过程:

1、获取source list并update源

# sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
# apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116
Executing: /tmp/tmp.gJDpQgL6qG/gpg.1.sh --keyserver
hkp://ha.pool.sks-keyservers.net:80
--recv-key
421C365BD9FF1F717815A3895523BAEEB01FA116
gpg: requesting key B01FA116 from hkp server ha.pool.sks-keyservers.net
gpg: key B01FA116: public key "ROS Builder <rosbuild@ros.org>" imported
gpg: Total number processed: 1
gpg:               imported: 1

如果需要代理,可以用:
apt-key adv --keyserver-options http-proxy=<myProxy>  --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116

# apt-get update

2、安装kk版本

ROS有几个release配置供你选择安装:ROS-Base、Desktop Install和Desktop-Full Install,Desktop-Full Install是官方推荐的选项,也是安装最全的选项,它包含了ROS, rqt, rviz, robot-generic libraries, 2D/3D simulators, navigation and 2D/3D perception等package:

# apt-get install ros-kinetic-desktop-full

这个过程需要好长一段时间(依你的网络情况而定),因为ROS超级庞大,有大约2G的安装文件要下载安装。

3、初始化ROS依赖

在使用ROS之前,我们还得先初始化ROS的一些依赖,ROS为你提供了“一键式”的初始化命令:

# rosdep init
Wrote /etc/ros/rosdep/sources.list.d/20-default.list
Recommended: please run

    rosdep update

# rosdep update
reading in sources list data from /etc/ros/rosdep/sources.list.d
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/osx-homebrew.yaml
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml
... ...
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml
Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/ruby.yaml
Hit https://raw.githubusercontent.com/ros/rosdistro/master/releases/fuerte.yaml
Query rosdistro index https://raw.githubusercontent.com/ros/rosdistro/master/index.yaml
Add distro "groovy"
Add distro "hydro"
Add distro "indigo"
Add distro "jade"
Add distro "kinetic"
Add distro "lunar"
updated cache in /home/tonybai/.ros/rosdep/sources.cache
... ...

到这里,我们可以看到ROS被安装到/opt/ros/kinetic下面了:

# tree -L 1  /opt/ros/kinetic
/opt/ros/kinetic
├── bin
├── env.sh
├── etc
├── include
├── lib
├── setup.bash
├── setup.sh
├── _setup_util.py
├── setup.zsh
└── share

5 directories, 5 files

4、设置环境变量

ROS提供了设置环境变量的脚本:/opt/ros/kinetic/setup.bash,我们将其加入到.bashrc中,这样每次用户登录后就可以使用下面这些ROS专属环境变量了:

# echo "source /opt/ros/kinetic/setup.bash" >> ~/.bashrc
# source ~/.bashrc

# env|grep ROS
ROS_ROOT=/opt/ros/kinetic/share/ros
ROS_PACKAGE_PATH=/opt/ros/kinetic/share
ROS_MASTER_URI=http://localhost:11311
ROSLISP_PACKAGE_DIRECTORIES=
ROS_DISTRO=kinetic
ROS_ETC_DIR=/opt/ros/kinetic/etc/ros

5、安装一些用于ROS package构建的工具依赖

ROS的用户会创建自己的ROS package,为了方便构建这些user package,我们需要安装以下一些工具:

# apt-get install python-rosinstall python-rosinstall-generator python-wstool build-essential

6、验证安装结果

完成以上操作后,ROS kk版本就安装OK了,我们来验证一下安装结果是否正确。我们来尝试启动一下ROS的master node:

# roscore
... logging to /root/.ros/log/fc6a002e-75cf-11e7-b053-00163e1001d7/roslaunch-myhost-7609.log
Checking log directory for disk usage. This may take awhile.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://myhost:43606/
ros_comm version 1.12.7

SUMMARY
========

PARAMETERS
 * /rosdistro: kinetic
 * /rosversion: 1.12.7

NODES

auto-starting new master
process[master]: started with pid [7620]
ROS_MASTER_URI=http://myhost:11311/

setting /run_id to fc6a002e-75cf-11e7-b053-00163e1001d7
process[rosout-1]: started with pid [7633]
started core service [/rosout]

如果你看到上面这些roscore的输出,那么基本就证明你的ROS1安装成功了!

三、ROS架构

ROS安装完毕后,我们来对ROS做进一步的探索!先来看看ROS1的架构。

ROS文档中将ROS架构分为三个级别:Filesystem level、Computation Graph level和Community level。对于一个framework来说,从字面意义上理解这三个level还是有些晦涩的。Community level先不说,我们可以通过对照来理解Filesystem level和Computation Graph level,实质上它们一个对应的是ROS的静态结构,一个对应的则是ROS的运行时结构。

1、ROS Filesystem level

我们这里借用《Effective Robotics Programming with ROS 3rd》中的图来整体看一下ROS Filesystem的概念:

img{512x368}

ROS实质上是由一系列的packages组成的,在packages的基础上,ROS通过metapackage来聚合一组packages以形成一个逻辑package。基于metapackage和package概念,ROS为开发者提供了在package之间跳转、文件拷贝、包查找、执行等功能的”类FileSystem”命令集合,比如:roscd、rosls、roscp、rosrun、roscat、rospack等。下面是一些命令使用的例子:

// 切换到ros安装目录
root@myhost:~# roscd
root@myhost:/opt/ros/kinetic#

// 切换到turtlesim包目录
root@myhost:~# roscd turtlesim
root@myhost:/opt/ros/kinetic/share/turtlesim#

// list turtlesim包内的文件
root@myhost:~# rosls turtlesim
cmake  images  msg  package.xml  srv

// 查找turtlesim包的路径
root@myhost:~# rospack find turtlesim
/opt/ros/kinetic/share/turtlesim

// 执行包turtlesim下的turtlesim_node
root@myhost:~# rosrun turtlesim turtlesim_node

// 查看包turtlesim的package.xml内容
root@myhost:~# roscat turtlesim package.xml

<?xml version="1.0"?>
<package>
  <name>turtlesim</name>
  <version>0.7.1</version>
  <description>
    turtlesim is a tool made for teaching ROS and ROS packages.
  </description>
... ...
</package>

ROS安装后,其所有package均存储在$ROS_PACKAGE_PATH下面,初始情况下即为/opt/ros/kinetic/share:

root@myhost:/opt/ros/kinetic# ls share
actionlib                         eigen_stl_containers          laser_pipeline         rosbag_migration_rule  roswtf                 shape_msgs
actionlib_msgs                    executive_smach               librviz_tutorial       rosbag_storage         rqt_action             simulators
actionlib_tutorials               filters                       map_msgs               ros_base               rqt_bag                smach
... ...

每个package下的结构都类似,以turtlesim包为例:

root@myhost:/opt/ros/kinetic/share# ls -F turtlesim
cmake/  images/  msg/  package.xml  srv/

至此,上面图片中package中的结构似乎与上面看到的turtlesim package中的结构对应上了。每个package下面都至少有一个package.xml作为package的manifests,msg、srv是功能性配置,分别定义了package用到的message和提供的service的结构。这里并没有代码,只是一些配置信息。

而对应的包的可执行文件则在/opt/ros/kinetic/lib下,还是以turtlesim package为例,当我们执行下面命令时:

# rosrun turtlesim turtlesim_node

rosrun首先会到$ROS_PACKAGE_PATH下找是否有package.xml中name为”turtlesim”的package(与目录的名字无关)。如果有,rosrun会到/opt/ros/kinetic/lib/turtlesim下查找是否有turtlesim_node这个二进制可执行文件。存在,则启动之;否则报错。

root@myhost:/opt/ros/kinetic/lib/turtlesim# ls
draw_square  mimic  turtlesim_node  turtle_teleop_key

root@myhost:/opt/ros/kinetic/lib/turtlesim# rosrun turtlesim turtlesim_node
[ INFO] [1501549501.410816841]: Starting turtlesim with node name /turtlesim
[ INFO] [1501549501.428589492]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000]

还有一种package:metapackage。metapackage在目录结构上与普通package无异,但package.xml尾部多了metapackage标签,我们以ros_core/package.xml为例:

<package>
  <name>ros_core</name>
  <version>1.3.1</version>

  <buildtool_depend>catkin</buildtool_depend>

  <run_depend>catkin</run_depend>
  <run_depend>cmake_modules</run_depend>
  <run_depend>common_msgs</run_depend>
  <run_depend>gencpp</run_depend>
  <run_depend>geneus</run_depend>
  <run_depend>genlisp</run_depend>
  <run_depend>genmsg</run_depend>
  <run_depend>gennodejs</run_depend>
  <run_depend>genpy</run_depend>
  <run_depend>message_generation</run_depend>
  <run_depend>message_runtime</run_depend>
  <run_depend>ros</run_depend>
  <run_depend>ros_comm</run_depend>
  <run_depend>rosbag_migration_rule</run_depend>
  <run_depend>rosconsole_bridge</run_depend>
  <run_depend>roscpp_core</run_depend>
  <run_depend>rosgraph_msgs</run_depend>
  <run_depend>roslisp</run_depend>
  <run_depend>rospack</run_depend>
  <run_depend>std_msgs</run_depend>
  <run_depend>std_srvs</run_depend>

  <export>
    <metapackage/>
  </export>
</package>

这种包称为metapackage,它的实质是一组package的集合。

2、ROS Computation Graph level

说完了ROS的静态结构,我们再来看看ROS整体的运行时结构,即ROS Computation Graph level:

img{512x368}

ROS在运行时层面是由一个master和一组node组成的,master的作用就是名字注册和查找,建立node与topic间联系以及服务发现之用。node间的通信方式可以是:

  • 服务srv调用
  • topic的发布和订阅

我们通过rosnode命令可以操作node,比如查看当前ROS中node信息:

# rosnode list
/rosout
/turtlesim

/rosout node是一个由roscore命令启动的特殊node,它相当于整个ROS运行环境的stdout/stderr,将所有node发往/rosout topic的消息汇聚在一起。

每个ROS运行时环境有且仅有一个ros master,ros master通过执行roscore命令启动,这也是一个ROS运行环境启动最先应该执行的命令:

# roscore
... logging to /home/tonybai/.ros/log/ee13b88e-7666-11e7-af90-4ccc6a7061a6/roslaunch-tonybai-myhost-26158.log
Checking log directory for disk usage. This may take awhile.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://tonybai-myhost:36180/
ros_comm version 1.12.7

SUMMARY
========

PARAMETERS
 * /rosdistro: kinetic
 * /rosversion: 1.12.7

NODES

auto-starting new master
process[master]: started with pid [26169]
ROS_MASTER_URI=http://tonybai-myhost:11311/

setting /run_id to ee13b88e-7666-11e7-af90-4ccc6a7061a6
process[rosout-1]: started with pid [26182]
started core service [/rosout]

roscore位于/opt/ros/kinetic/bin下,它实际上是一个python脚本,它调用位于/opt/ros/kinetic/lib/python2.7/dist-packages/roslaunch下的roslaunch lib,并依据launch配置文件/opt/ros/kinetic/etc/ros/roscore.xml启动对应的核心node:

// /opt/ros/kinetic/etc/ros/roscore.xml
<!--
  ROS Core Stack definition

  Before making any modifications to this file, please read:

http://ros.org/wiki/roscore

  -->
<launch>
  <group ns="/">
    <param name="rosversion" command="rosversion roslaunch" />
    <param name="rosdistro" command="rosversion -d" />
    <node pkg="rosout" type="rosout" name="rosout" respawn="true"/>
  </group>
</launch>

roscore会自动启动master,master对应的是一个metapackage: ros。ros package的package.xml如下:

<package>
  <name>ros</name>
  <version>1.13.5</version>
  <description>ROS packaging system</description>
  <maintainer email="dthomas@osrfoundation.org">Dirk Thomas</maintainer>
  ... ...

  <buildtool_depend>catkin</buildtool_depend>

  <run_depend>catkin</run_depend> <!-- only for backward compatibility with rosbuild -->
  <run_depend>mk</run_depend>
  <run_depend>rosbuild</run_depend>
  <run_depend>roslang</run_depend>
  <run_depend>roslib</run_depend>
  <run_depend>rosbash</run_depend>
  <run_depend>rosboost_cfg</run_depend>
  <run_depend>rosclean</run_depend>
  <run_depend>roscreate</run_depend>
  <run_depend>rosmake</run_depend>
  <run_depend>rosunit</run_depend>

  <export>
    <metapackage/>
  </export>
</package>

ROS的运行时当前目录为~/.ros,在这个目录下你会看到ros的一些运行时输出信息:

$ tree  -L 1 ~/.ros
/home/tonybai/.ros
├── log/
├── roscore-11311.pid
├── rosdep/
├── rospack_cache_00988404638878154258
├── rospack_cache_04359245844500407984
├── rospack_cache_05251971726343818934
├── rospack_cache_11134725904490598093
├── rosstack_cache_00988404638878154258
├── rosstack_cache_04359245844500407984
├── rosstack_cache_05251971726343818934
└── rosstack_cache_11134725904490598093

2 directories, 9 files

roscore还会启动一个Parameter Server,用于各个节点保存或读取parameters,通过rosparam可以查看相关param信息,比如当前param的list:

$ rosparam list
/background_b
/background_g
/background_r
/rosdistro
/roslaunch/uris/host_tonybai_myhost__36180
/rosversion
/run_id

我们可以通过ros提供的rqt_graph命令查看node之间以及node与topic之间的订阅和发布关系,如下图:

img{512x368}

3、ROS的“分布式”源码结构

安装过程中ROS的“庞大”,与ROS在github上源码库的“渺小”形成鲜明对比。其实我们安装的ROS与这份源码库并非一一对应的:ROS的源码结构也是“分布式”的,ROS源码实质上是一系列package源码的组合。当前版本的ROS发布版采用bloom工具进行release的。以kk版本为例,bloom读取一份rosdistro库的kk版本distribution.yaml文件(这份文件比较庞大),即ROS发布文件,并根据文件中的描述信息,下载对应的package源码并编译构建的:

// kinetic/distribution.yaml)
%YAML 1.1
# ROS distribution file
# see REP 143: http://ros.org/reps/rep-0143.html
---
release_platforms:
  debian:
  - jessie
  fedora:
  - '23'
  - '24'
  ubuntu:
  - xenial
repositories:
  abb:
    doc:
      type: git
      url: https://github.com/ros-industrial/abb.git
      version: kinetic-devel
    release:
      packages:
      - abb
      - abb_driver
      - abb_resources
      ... ...
      tags:
        release: release/kinetic/{package}/{version}
      url: https://github.com/ros-industrial-release/abb-release.git
      version: 1.3.0-1
    source:
      type: git
      url: https://github.com/ros-industrial/abb.git
      version: kinetic
    status: developed
  abb_experimental:
    doc:
      type: git
      url: https://github.com/ros-industrial/abb_experimental.git
      version: kinetic-devel
    status: developed
... ...
type: distribution
version: 2

鉴于ROS这种分布式的相对松散的源码组织结构,对ROS的裁剪则相对简单,只需挑选你自己需要的第三方包即可。

四、启动你的第一个ROS“机器人”

ROS虽然号称机器人开发框架,但拥有一个实体版机器人并不是进行ROS开发的必要条件。ROS的一大优势就是可以利用各种仿真工具进行机器人操作和控制逻辑的仿真和调试。常见的仿真器主要有三个:TurtlesimRviz+arbotixGazebo。Turtlesim是一个QT开发的2D轨迹显示界面,只能显示运动轨迹;arbotix是含有一个差速驱动机器人的控制器,结合rviz使用,用于机器人运动及topic数据的3D显示,但不包含物理学引擎;Gazebo是全功能的3D物理模拟器,要用这个模拟器,需要掂量掂量你的主机的内存和显卡是否够用。

本文是入门文章,我们就从turtlesim开始。假设此时roscore已经启动了。

我们来启动一下turtlesim_node:

# rosrun turtlesim turtlesim_node
[ INFO] [1501549501.410816841]: Starting turtlesim with node name /turtlesim
[ INFO] [1501549501.428589492]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000]

这时你的desktop会出现一个新的窗口,如下图:

img{512x368}

不过此时小海龟一动不动!如果要让它移动,我们需要告诉他如何移动!

我们启动另外一个node – turtle_teleop_key:

# rosrun turtlesim turtle_teleop_key
Reading from keyboard
---------------------------
Use arrow keys to move the turtle.

通过turtle_teleop_key,我们可以使用方向键控制小海龟的移动了:

img{512x368}

其原理在于:turtle_teleop_key将方向键产生的数据转换为位置信息后,发布到topic: /turtle1/cmd_vel上;turtlesim_node由于subscribe了该topic,因此将接收到新的位置数据,这样小海龟就会移动到新的位置上去:

img{512x368}

turtlesim node启动后还启动了一个service: spawn,调用该服务我们可以在窗口上创建出一个新的小海龟:

# rosservice call /spawn 2 2 0.2 ""
name: turtle2

img{512x368}

可以看到,通过service调用或向topic发布数据,我们可以自由控制小海龟。下面的是一个稍微复杂的控制指令,其结果就是让小海龟1进行持续的转圈动作:

rostopic pub /turtle1/cmd_vel geometry_msgs/Twist -r 1 -- '[2.0, 0.0, 0.0]' '[0.0, 0.0, -1.8]'

img{512x368}

五、创建你的第一个ROS package

现在我们来创建第一个ROS package!

1、初始化ros workspace

我们要添加自己的ROS package,一般不会直接在ROS的安装目录下创建,因此我们需要创建自己的workspace,并在后续将其加入到ROS_PACKAGE_PATH中,以使得ros的文件系统命令也能适用于我们自己的workspace路径。

# mkdir -p ~/myros_ws/src
# cd ~/myros_ws/src
# catkin_init_workspace
Creating symlink "/home/tonybai/myros_ws/src/CMakeLists.txt" pointing to "/opt/ros/kinetic/share/catkin/cmake/toplevel.cmake"

$ tree ~/myros_ws/
/home/tonybai/myros_ws/
└── src
    └── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake

1 directory, 1 file

2、创建Package

我们来创建一个我们自己的package – chattingsim:

# cd ~/myros_ws/src
# catkin_create_pkg chattingsim std_msgs rospy roscpp
Created file chattingsim/package.xml
Created file chattingsim/CMakeLists.txt
Created folder chattingsim/include/chattingsim
Created folder chattingsim/src
Successfully created files in /home/tonybai/myros_ws/src/chattingsim. Please adjust the values in package.xml.

# tree chattingsim/
chattingsim/
├── CMakeLists.txt
├── include
│   └── chattingsim
├── package.xml
└── src

3 directories, 2 files

虽然目前我们的chattingsim package并没有任何有意义的代码,但不妨碍我们先来编译一下myros_ws这个workspace:

# cd ~/myros_ws/
# catkin_make

# catkin_make
Base path: /home/tonybai/myros_ws
Source space: /home/tonybai/myros_ws/src
Build space: /home/tonybai/myros_ws/build
Devel space: /home/tonybai/myros_ws/devel
Install space: /home/tonybai/myros_ws/install
####
#### Running command: "cmake /home/tonybai/myros_ws/src -DCATKIN_DEVEL_PREFIX=/home/tonybai/myros_ws/devel -DCMAKE_INSTALL_PREFIX=/home/tonybai/myros_ws/install -G Unix Makefiles" in "/home/tonybai/myros_ws/build"
####
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
... ...
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Found gtest sources under '/usr/src/gtest': gtests will be built
-- Using Python nosetests: /usr/bin/nosetests-2.7
-- catkin 0.7.6
-- BUILD_SHARED_LIBS is on
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- ~~  traversing 1 packages in topological order:
-- ~~  - chattingsim
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- +++ processing catkin package: 'chattingsim'
-- ==> add_subdirectory(chattingsim)
-- Configuring done
-- Generating done
-- Build files have been written to: /home/tonybai/myros_ws/build
####
#### Running command: "make -j4 -l4" in "/home/tonybai/myros_ws/build"
####

catkin_make后,myros_ws下面又增加了不少目录和文件:

~/myros_ws$ tree -L 2
.
├── build
│   ├── catkin
│   ├── catkin_generated
│   ├── CATKIN_IGNORE
│   ├── catkin_make.cache
│   ├── chattingsim
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   ├── cmake_install.cmake
│   ├── CTestTestfile.cmake
│   ├── gtest
│   ├── Makefile
│   └── test_results
├── devel
│   ├── env.sh
│   ├── lib
│   ├── setup.bash
│   ├── setup.sh
│   ├── _setup_util.py
│   ├── setup.zsh
│   └── share
└── src
    ├── chattingsim
    └── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake

12 directories, 12 files

我们看到~/myros_ws/devel目录下的结构与/opt/ros/kinetic下的非常相似,我们将其加入到ROS_PACKAGE_PATH:

# cd ~/myros_ws/devel
# source ./setup.bash
# echo $ROS_PACKAGE_PATH
/home/tonybai/myros_ws/src:/opt/ros/kinetic/share

3、添加talker和listener

chattingsim package的架子已经搭好,接下来我们开始“填肉”。这里我们直接使用ros tutorials中写好的两个源文件talker.cpplistener.cpp,我们把这两个文件放在~/myros_ws/src/chattingsim/src下面。

在build之前,我们需要修改一下chattingsim的CMakeLists.txt:

cmake_minimum_required(VERSION 2.8.3)
project(chattingsim)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  genmsg
)

generate_messages(DEPENDENCIES std_msgs)

include_directories(
  include ${catkin_INCLUDE_DIRS}
)

add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_dependencies(talker chattingsim_generate_messages_cpp)

add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(listener chattingsim_generate_messages_cpp)

构建chattingsim package:

~/myros_ws# catkin_make
Base path: /home/tonybai/myros_ws
Source space: /home/tonybai/myros_ws/src
Build space: /home/tonybai/myros_ws/build
Devel space: /home/tonybai/myros_ws/devel
Install space: /home/tonybai/myros_ws/install
####
#### Running command: "make cmake_check_build_system" in "/home/tonybai/myros_ws/build"
####
####
#### Running command: "make -j4 -l4" in "/home/tonybai/myros_ws/build"
####
[  0%] Built target std_msgs_generate_messages_eus
[  0%] Built target std_msgs_generate_messages_cpp
[  0%] Built target std_msgs_generate_messages_lisp
[  0%] Built target std_msgs_generate_messages_py
[  0%] Built target std_msgs_generate_messages_nodejs
[  0%] Built target chattingsim_generate_messages_cpp
[  0%] Built target chattingsim_generate_messages_lisp
[ 14%] Generating EusLisp manifest code for chattingsim
[ 28%] Building CXX object chattingsim/CMakeFiles/talker.dir/src/talker.cpp.o
[ 28%] Built target chattingsim_generate_messages_nodejs
[ 42%] Generating Python msg __init__.py for chattingsim
[ 57%] Generating Python srv __init__.py for chattingsim
[ 71%] Building CXX object chattingsim/CMakeFiles/listener.dir/src/listener.cpp.o
[ 71%] Built target chattingsim_generate_messages_py
[ 71%] Built target chattingsim_generate_messages_eus
[ 71%] Built target chattingsim_generate_messages
[ 85%] Linking CXX executable /home/tonybai/myros_ws/devel/lib/chattingsim/talker
[ 85%] Built target talker
[100%] Linking CXX executable /home/tonybai/myros_ws/devel/lib/chattingsim/listener
[100%] Built target listener

4、启动chattingsim的talker node和listener node

在两个terminal窗口分别启动listener node和talker node:

# rosrun chattingsim listener
[ INFO] [1501577165.148477238]: I heard: [hello world 3]
[ INFO] [1501577165.248349227]: I heard: [hello world 4]
[ INFO] [1501577165.348301478]: I heard: [hello world 5]
[ INFO] [1501577165.448340592]: I heard: [hello world 6]
[ INFO] [1501577165.548433696]: I heard: [hello world 7]
[ INFO] [1501577165.648466054]: I heard: [hello world 8]
[ INFO] [1501577165.748424131]: I heard: [hello world 9]
[ INFO] [1501577165.848457076]: I heard: [hello world 10]
[ INFO] [1501577165.948449431]: I heard: [hello world 11]
[ INFO] [1501577166.048470110]: I heard: [hello world 12]
[ INFO] [1501577166.148340964]: I heard: [hello world 13]

# rosrun chattingsim talker
[ INFO] [1501577164.847745179]: hello world 0
[ INFO] [1501577164.947898377]: hello world 1
[ INFO] [1501577165.047889213]: hello world 2
[ INFO] [1501577165.147882701]: hello world 3
[ INFO] [1501577165.247923700]: hello world 4
[ INFO] [1501577165.347918242]: hello world 5
[ INFO] [1501577165.447917169]: hello world 6
[ INFO] [1501577165.547916593]: hello world 7
[ INFO] [1501577165.647920474]: hello world 8
[ INFO] [1501577165.747930882]: hello world 9
[ INFO] [1501577165.847917356]: hello world 10
[ INFO] [1501577165.947918365]: hello world 11
[ INFO] [1501577166.047918187]: hello world 12
[ INFO] [1501577166.147919712]: hello world 13
^C[ INFO] [1501577166.247984284]: hello world 14

至此,基于我们第一个package: chattingsim而创建的node工作正常!

六、小结

如果说人工智能的算法是大脑,实体的机械部件构成四肢,那么ROS则提供了大脑与各肢体间提供了神经传递的机制。之前ROS在国内发展的不瘟不火,随着Baidu Apollo项目将ros作为Apollo-platform支持的一部分,更多人会去了解ROS,ROS在国内的发展势也许会走得更顺畅一些。ROSCon 2017也即将于下月在加拿大温哥华召开,ROS2对ROS1的安全性和实时性的加强也势必会让ROS有更多用武之地,值得期待。

注:ros wiki 资料非常详尽,ros tutorial是学习ros的起点,几乎不用任何其他书籍。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats