1. What is Micro?

Micro is a cloud-native development framework that addresses the key points of need for building cloud-native services. The MicroServices architectural pattern Micro addresses the complexity of distributed systems and provides a simpler, programmable abstraction as the foundation. It also provides a set of services that are the building blocks of the platform.

Technology is constantly evolving and the infrastructure stack is constantly changing. Micro is a platform with a pluggable foundation and a rigorously defined API that can be built on top of it to solve these problems. It can be used in any stack or cloud environment.

2. Main composition

The key components of Micro are:

  • Server: Distributed System Runtime consists of building module services that abstract the infrastructure and provide a programmable abstraction layer.
  • Clients: Multiple entry points through which your services can be accessed. Write the service once and access it through all the methods you already know. HTTP API,gRPC agent or command line interface
  • The Library:Go Library makes it easy to write services without having to piece together lines of code. Automatic configuration and initialization by default, just import to get started quickly.
  • Plugins:Micro is runtime – and infrastructure-independent. Each base building block service uses the Go Micro standard library to provide a pluggable base. We simplify usage by pre-initializing both local usage and cloud computing.
  • Service:
    • Auth: Authentication and authorization are core requirements for any production-ready platform. Micro has a built-in authentication service for managing service-to-service and user-to-service authentication.
    • Broker: A message broker that allows asynchronous messaging. Microservices are event-driven architectures that should provide messaging as category 1 citizens. Notify other services of events without worrying about responses.
    • Config: Manages dynamic configuration in a centralized location for your services to access. The ability to load configurations from multiple sources enables you to update configurations without restarting services.
    • Debug: provides built-in aggregation of statistics, logs, and trace information for debugging. The debug service extracts information about all the services to help understand the overall scope of the system from one location.
    • Network: The decline of service-to-service networking solutions. Distribute the load of service discovery, load balancing, and fault tolerance to the network. Micronetworks dynamically build a delay-based routing table from the local registry. It includes support for multi-cloud networks.
    • Registry: The registry provides service discovery to locate other services and stores feature-rich metadata and endpoint information. It’s a service browser that lets you store this information centrally and dynamically at run time.
    • Runtime: Service runtime, which manages the life cycle of the service, starting with the service. The runtime service can run locally or on Kubernetes, providing a seamless abstraction across both.
    • Store: State is a fundamental requirement of any system. We provide a key-value store to provide a simple store of state that can be shared between services or unloaded for long periods to keep microservices stateless and horizontally scalable.

To write applications that run on Micro, you can use the Go Micro framework.

  • Go-micro: Use the powerful GO microframework to easily and quickly develop microservices. Go Micro removes the complexity of distributed systems and provides simpler abstractions to build highly extensible microservices.

3. Start fast

The installation

Using the go: Go install github.com/micro/micro/v3 or download binaries # MacOS curl - fsSL https://raw.githubusercontent.com/micro/micro/master/scripts/install.sh | /bin/bash # Linux wget -q https://raw.githubusercontent.com/micro/micro/master/scripts/install.sh -O - | /bin/bash # Windows powershell -Command "iwr -useb https://raw.githubusercontent.com/micro/micro/master/scripts/install.ps1 | iex"Copy the code

Of course, you can use micro in Docker

docker pull micro/micro
Copy the code

Can use the latest version of Micro

Running a service

# 1. The first step we need to start our server Micro Server # 1. In docker environment we can run the following commandCopy the code
Docker run micro/micro server # 2020-09-23 08:49:21 file=user/user.go:54 level=info Setting up config docker run micro/micro server # 2020-09-23 08:49:21 file=user/user key to /root/.micro/config_secret_key 2020-09-23 08:49:21 file=user/user.go:45 level=info Loading config key from /root/.micro/config_secret_key 2020-09-23 08:49:21 file=user/user.go:80 level=info Loading keys /root/.micro/id_rsa and /root/.micro/id_rsa.pub 2020-09-23 08:49:21 file=user/user.go:99 level=info Setting up keys for JWT at /root/.micro/id_rsa and /root/.micro/id_rsa.pub 2020-09-23 08:49:24 file=server/server.go:110 level=info Starting server  2020-09-23 08:49:24 file=server/server.go:121 level=info Registering network 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering runtime 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering registry 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering config 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering store 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering broker 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering events 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering auth 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering proxy 2020-09-23 08:49:24 file=server/server.go:121 level=info Registering api 2020-09-23 08:49:24 file=server/server.go:186 level=info Starting server runtime 2020-09-23 08:49:25 file=service/service.go:195 level=info Starting [service] server 2020-09-23 08:49:25 file=grpc/grpc.go:902 level=info Server [grpc] Listening on [::]:10001 2020-09-23 08:49:25 file=grpc/grpc.go:732 level=info Registry [mdns] Registering node: server-b4a865a4-cab0-4d1d-93f5-83ea83ea10dcCopy the code

At the other command line terminal

Username: admin Password: micro micro login # 2 Docker exec-it 7184a69AC6cc /micro login # Now can run our Hellworld service micro run github.com/micro/services/helloworld # 3. We can run the following command in a docker environment docker exec 7184 a69ac6cc/micro - it run github.com/micro/services/helloworldCopy the code

You can run the following command to view the output logs of the service running

Docker exec -it 7184a69ac6cc /micro logs HelloWorld 0-20200526211855 - cb27e3aa2013 go: finding github.com/micro/micro/v3 v3.0.0 - beta. 4.0.20200921154750-68282 c70c194 go: Finding github.com/micro/go-micro/v3 v3.0.0 - beta. 2.0.20200921154545-9 dbd75f2cc13 go: Finding github.com/google/uuid v1.1.2 go: finding google.golang.org/protobuf v1.25.0 go: Finding github.com/oxtoacart/bpool v0.0.0-20190530202638-03653 db5a59c go: finding github.com/miekg/dns v1.1.27 go: Finding golang.org/x/crypto v0.0.0-20200709230013-948 cd5f35899 go: Finding golang.org/x/net v0.0.0-20200707034311 - ab3426394381 go: Finding golang.org/x/sys v0.0.0-20200625212154- dDB9806d33AE go: Finding golang.org/x/text v0.3.3 GO: Finding github.com/stretchr/testify v1.6.1 go: finding github.com/davecgh/go-spew v1.1.1 go: Finding github.com/pmezard/go-difflib v1.0.0 go: Finding Gopkg. in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c go: Finding github.com/blang/semver v3.5.1+ Incompatible Go: Finding github.com/hashicorp/go-version v1.2.1 Go: Finding github.com/bitly/go-simplejson v0.5.0 go: Finding github.com/juju/fslock v0.0.0-20160525022230-4d5C94c67b4b go: Finding github.com/urfave/cli/v2 v2.2.0 go: Finding github.com/cpuguy83/go-md2man/v2 v2.0.0 go: Finding github.com/russross/blackfriday/v2 v2.0.1 go: finding github.com/shurcooL/sanitized_anchor_name v1.0.0 go: Finding github.com/dgrijalva/jwt-go v3.2.0+ Incompatible Go: Finding google.golang.org/grpc v1.26.0 go: Finding google.golang.org/genproto v0.0.0-20200526211855 - cb27e3aa2013 go: Finding github.com/patrickmn/go-cache v2.1.0+ Incompatible Go: Finding github.com/pkg/errors v0.9.1 go: Finding github.com/hpcloud/tail v1.0.0 go: Finding Gopkg.in /fsnotify.v1 v1.4.7 go: Finding gopkg. / in the tomb. V1 v1.0.0-20141024135613 - dd632973f1e7 go: Finding github.com/teris-io/shortid v0.0.0-20171029131806-771 a37caa5cf go: Finding github.com/xanzy/go-gitlab v0.35.1 go: Finding github.com/google/go-querystring v1.0.0 go: Finding github.com/hashicorp/go-cleanhttp v0.5.1 go: Finding github.com/hashicorp/go-retryablehttp v0.6.4 go: Finding golang.org/x/oauth2 v0.0.0-20190604053449-0 f29369cfe45 go: Finding golang.org/x/time v0.0.0-20191024005414-555d28b269f0 go: Finding Go.etcd. IO /bbolt v1.3.5 go: Finding github.com/micro/go-micro v1.18.0 go: Finding github.com/rhysd/go-github-selfupdate v1.2.2 go: Finding github.com/google/go-github/v30 v30.1.0 go: Finding github.com/inconshreveable/go-update v0.0.0-20160112193335-8152 e7eb6ccf go: Finding github.com/tcnksm/go-gitconfig v0.1.2 go: Finding github.com/ulikunitz/xz v0.5.5 2020-09-23 10:12:53 File =service/service.go:195 level=info Starting [service] helloworld 2020-09-23 10:12:53 file=grpc/grpc.go:902 level=info Server [grpc] Listening on [::]:37227 2020-09-23 10:12:53 file=grpc/grpc.go:732 level=info Registry [service] Registering node: helloworld-e7fc841c-53fe-41e5-a7e8-10efe56bc935 2020-09-23 10:13:17 file=handler/helloworld.go:14 level=info Received Helloworld.Call request 2020-09-23 10:13:25 file=handler/helloworld.go:14 level=info Received Helloworld.Call requestCopy the code

You can run the following command to view the service status and metadata

Docker exec -it 7184a69AC6cc /micro status NAME VERSION SOURCE status BUILD UPDATED  METADATA helloworld latest github.com/micro/services/helloworld running n/a 21m48s ago owner=admin, group=microCopy the code

You can run the following command to invoke related services

Call '{"name":"Jane"}' # docker exec it 7184a69ac6cc /micro call  helloworld Helloworld.Call '{"name":"Jane"}' { "msg": "Hello Jane" }Copy the code

You can run the following command to query the list of services

Micro Services API Auth Broker Config events docker execit 7184a69ac6cc/Micro Services API Auth Broker Config Events helloworld network proxy registry runtime server storeCopy the code

You can run the following command to implement a new service

New Foobar Creating Service Go. Micro. Service. Foobar in Foobar. ├─ main Handler │ └ ─ ─ foobar. Go ├ ─ ─ the subscriber │ └ ─ ─ foobar. Go ├ ─ ─ proto/foobar │ └ ─ ─ foobar. Proto ├ ─ ─ Dockerfile ├ ─ ─ a Makefile ├ ─ ─ the README. Md ├ ─ ─ the gitignore └ ─ ─. Mod download protobuf for micro: brew install protobuf go get -u github.com/golang/protobuf/proto go get -u github.com/golang/protobuf/protoc-gen-go go get github.com/micro/micro/v2/cmd/protoc-gen-micro@master compile the proto file foobar.proto: cd foobar protoc --proto_path=.:$GOPATH/src --go_out=. --micro_out=. proto/foobar/foobar.protoCopy the code

As you can see, the following tools must be installed before the first service can be built

  • protoc
  • protobuf/proto
  • protoc-gen-micro

They are needed to convert proto files into Go code. The proto file exists to provide a language-neutral way to describe service endpoints, their input and output types, and to provide an efficient serialization format.

Therefore, once all the tools are installed, within the service root, we can issue the following command to generate the Go code from the source file

protoc --proto_path=.:$GOPATH/src --go_out=. --micro_out=. proto/foobar/foobar.proto
Copy the code

The generated code must be submitted to source control so that other services can import prototypes when making service calls

At this point, we know how to write a service, run it, and invoke other services. We had it all at our fingertips, but there were still some pieces of writing an application that were missing. Storage interfaces are one of them, helping to persist data even without a database.

In addition to many other useful built-in services, Micro includes persistent storage services for storing data.

To maintain a value, we can use the following command

micro store write key1 value1

Copy the code

Then use the following command to read the value

micro store read key
Copy the code

Now, because the sample service is running, we cannot use Micro Run at this time, but we can redeploy it using Micro Update.

We can simply issue the update command (remember to switch back to the root directory of the sample service first)

micro update .


docker exec -it 7184a69ac6cc /micro update helloworld
Copy the code

At this point, you can check that the service has been updated using Micro Status

If things get out of hand for some reason, we can try the time-tested “turn it off and turn it back on” solution:

micro kill .
micro run .
Copy the code

4.FAQ

What’s the difference between Micro and Go-kit?

Go-kit describes itself as a standard library for Microservices. Like Go, Go-Kit gives you separate packages that you can use to build applications. Go-kit is perfect for places where you want complete control over how services are defined.

Go Micro is also the standard library for MicroServices, so check it out if you want to choose the best abstraction for your work. In addition, Micro is a framework for Microservices that encapsulates all the requirements of back-end and API development. Think of it as a platform.

What about performance?

Micro leverages gRPC, so its performance is similar. Micro can also balance load across multiple service instances and distribute the load to the distributed system infrastructure when scaling is required.

What are local and platform?

Micro supports the environment as a concept. Env is a tiny server hosted locally or elsewhere. It is defined as the name of the host:port mapped to the micro agent (gRPC agent). We introduced two environments, “Local” and “Platform.”

Docker execit 7184a69AC6cc /micro env * local 127.0.0.1:8081 platform proxy.m3o.comCopy the code

Local is a Local server started by Micro Server and the agent is located on port :8081. Platform is the environment that we host as a paid product in the cloud, ideally where you can run code if you need to host it.

5. Appendix 1. Cli tool library

In micro code, there is a lot of use of the command line tool library, which can be said to be important on the road to understanding micro. Let’s look at the details of this tool.

5.1. The cli

Cli is an easy, fast, and fun software package for building command line programs. The goal is to let developers write fast, distributable command-line applications in an expressive manner.

5.1.1 installation

$ GO111MODULE=on go get github.com/urfave/cli/v2

Copy the code

5.1.2 Simple use

One of the philosophies behind cli is that apis should be fun and full of discovery. As a result, you can have as little as one line of code in a CLI application

package main

import (
  "os"

  "github.com/urfave/cli/v2"
)

func main() {
  (&cli.App{}).Run(os.Args)
}
Copy the code

The application will run and display help text, but it’s not very useful.

NAME: main - A new cli application USAGE: main [global options] command [command options] [arguments...]  COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)Copy the code

Let’s perform an action and provide some help documentation:

package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Name: "boom", Usage: "make an explosive entrance", Action: func(c *cli.Context) error { fmt.Println("boom! I say!" ) return nil }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code

When running the program, app.Run takes the Action specified by Action.

boom! I say!
Copy the code

You can find the parameters by calling the Args function on cli.context, for example:

package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Action: func(c *cli.Context) error { fmt.Printf("Hello %q", c.Args().Get(0)) return nil }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code
go run ~/workspace/src/just.for.test/clilearn/main.go friends
Hello "friends"%

Copy the code

Setting and querying flags is simple.

package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag { &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", }, }, Action: func(c *cli.Context) error { name := "Nefertiti" if c.NArg() > 0 { name = c.Args().Get(0) } if c.String("lang") == "spanish" { fmt.Println("Hola", name) } else { fmt.Println("Hello", name) } return nil }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code
go run ~/workspace/src/just.for.test/clilearn/main.go -lang spanish
Hola someone
Copy the code

Sometimes it is useful to specify the value of flag in the use method string. Such placeholders are represented by backquotes.

package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "config", Aliases: []string{"c"}, Usage: "Load configuration from `FILE`", }, }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code
NAME: main - A new cli application USAGE: main [global options] command [command options] [arguments...]  COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --config FILE, -c FILE Load configuration from FILE --help, -h show help (default: false)Copy the code

Note that only the first placeholder is used. Subsequent backquotes will remain intact.

You can set alternate (or abbreviated) names for flag by providing a comma-separated list of names

package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag { &cli.StringFlag{ Name: "lang", Aliases: []string{"l"}, Value: "english", Usage: "language for the greeting", }, }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code

In this way, flag can be set to –lang Spanish or -l Spanish. Note that it is an error to give two different forms of the same flag in the same command call.

Flags for applications and commands are displayed in defined order. However, you can sort them from outside the library by using FlagsByName or CommandByName in conjunction with sort

package main import ( "log" "os" "sort" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang, l", Value: "english", Usage: "Language for the greeting", }, &cli.StringFlag{ Name: "config, c", Usage: "Load configuration from `FILE`", }, }, Commands: []*cli.Command{ { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) error { return nil }, }, { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) error { return nil }, }, }, } sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code

The output order will be output in the sorted order

--config FILE, -c FILE  Load configuration from FILE
--lang value, -l value  Language for the greeting (default: "english")
Copy the code

You create the Required flag by setting the Required field to true. If the user does not provide the required flags, an error message is displayed.

package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := cli.NewApp() app.Flags = []cli.Flag {  &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Required: true, }, } app.Action = func(c *cli.Context) error { var output string if c.String("lang") == "spanish" { output = "Hola" } else { output = "Hello" } fmt.Println(output) return nil } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code

Sometimes it is useful to specify a default value for flag in a flag declaration. This is useful if the default value of the tag is a computed value. Default values can be set through the DefaultText structure field.

package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Usage: "Use a randomized port", Value: 0, DefaultText: "random", }, }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code

The output text will contain the default value information

--port value  Use a randomized port (default: random)
Copy the code

The priority of the different sources of flag values is as follows (from high to low) :

  • User-specified command line flag value
  • Environment variable (if specified)
  • Configuration file (if specified)
  • Default values defined

Subcommands: You can define subcommands for more git-like command-line applications.

package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) error { fmt.Println("added task: ", c.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) error { fmt.Println("completed task: ", c.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Subcommands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(c *cli.Context) error { fmt.Println("new task template: ", c.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(c *cli.Context) error { fmt.Println("removed task template: ", c.Args().First()) return nil }, }, }, }, }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code
go run main.go template add job
new task template:  job
Copy the code

For other organizations in applications with many subcommands, you can associate a category with each command to group them together in help output

package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "noop", }, { Name: "add", Category: "template", }, { Name: "remove", Category: "template", }, }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code
go run main.go NAME: main - A new cli application USAGE: main [global options] command [command options] [arguments...]  COMMANDS: noop help, h Shows a list of commands or help for one command template: add remove GLOBAL OPTIONS: --help, -h show help (default: false)Copy the code
go run main.go noop NAME: main noop - USAGE: main noop [command options] [arguments...]  OPTIONS: --help, -h show help (default: false)Copy the code
go run main.go noop add NAME: main add - USAGE: main add [arguments...]  CATEGORY: templateCopy the code

Calling app.run does not automatically call os.exit, which means that by default, the Exit code will be “offline” to 0. An explicit exit code can be set by returning a non-nil error that satisfies cli.exitcoder, or cli.multierror containing an error that satisfies cli.exitcoder.

package main import ( "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.BoolFlag{ Name: "ginger-crouton", Usage: "is it in the soup?", }, }, Action: func(ctx *cli.Context) error { if ! ctx.Bool("ginger-crouton") { return cli.Exit("Ginger croutons are not in the soup", 86) } return nil }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code
Ginger croutons are not in the soup
exit status 86
Copy the code

Combining short options: The traditional use of short names for options looks like this:

cmd -s -o -m "Some message"
Copy the code

Suppose you want users to be able to combine options with their short names. You can do this using UseShortOptionHandling bool in the application configuration, or by attaching a single command to the command configuration.

package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{} app.UseShortOptionHandling = true app.Commands = []*cli.Command{ { Name: "short", Usage: "complete a task on the list", Flags: []cli.Flag{ &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}}, &cli.BoolFlag{Name: "option", Aliases: []string{"o"}}, &cli.StringFlag{Name: "message", Aliases: []string{"m"}}, }, Action: func(c *cli.Context) error { fmt.Println("serve:", c.Bool("serve")) fmt.Println("option:", c.Bool("option")) fmt.Println("message:", c.String("message")) return nil }, }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code
go run main.go  short -s -o -m "s"
serve: true
option: true
message: s
Copy the code
go run main.go  short -som "s"
serve: true
option: true
message: s
Copy the code

You can enable command completion by setting the EnableBashCompletion flag

package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := cli.NewApp() app.EnableBashCompletion = true app.Commands = []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) error { fmt.Println("added task: ", c.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) error { fmt.Println("completed task: ", c.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Subcommands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(c *cli.Context) error { fmt.Println("new task template: ", c.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(c *cli.Context) error { fmt.Println("removed task template: ", c.Args().First()) return nil }, }, }, }, } err := app.Run(os.Args) if err ! = nil { log.Fatal(err) } }Copy the code