【k8s源码阅读】代码生成-gengo

一、代码生成逻辑和编译器原理

  1. 收集Go语言源码文件信息及内容
  2. 通过Lexer词法分析器进行一系列词法分析
  3. 生成抽象语法树
  4. 对抽象语法树进行类型检查
  5. 生成代码,将抽象语法树转换为机器代码

二、收集Go包信息

Go语言的条件编译有两种定义方法,分别介绍如下。

  • 构建标签:在源码里添加注释信息,比如//+build linux,该标签决定了源码文件只在Linux平台上才会被编译。
  • 文件后缀:改变Go语言代码文件的后缀,比如foo_linux.go,该后缀决定了源码文件只在Linux平台上才会被编译。

另外,go/build工具有几个重要的类型和方法,其中Context类型指定构建上下文环境,例如GOARCH、GOOS、GOROOT、GOPATH等;Package类型用于描述Go包信息;Import方法导入指定的包,返回该包的Package指针类型,用于收集有关Go包的信息。它们用于处理Go项目目录结构、源码、语法、基本操作等。

gengo收集包信息可以分为两步:为生成的代码文件设置构建标签;收集Go包信息并且读取源码内容

在聊这部分之前,我们可以先看一下支持的命令行参数有哪些

golang 复制代码
func (g *GeneratorArgs) AddFlags(fs *pflag.FlagSet) {
    fs.StringSliceVarP(&g.InputDirs, "input-dirs", "i", g.InputDirs, "Comma-separated list of import paths to get input types from.")
    fs.StringVarP(&g.OutputBase, "output-base", "o", g.OutputBase, "Output base; defaults to $GOPATH/src/ or ./ if $GOPATH is not set.")
    fs.StringVarP(&g.OutputPackagePath, "output-package", "p", g.OutputPackagePath, "Base package path.")
    fs.StringVarP(&g.OutputFileBaseName, "output-file-base", "O", g.OutputFileBaseName, "Base name (without .go suffix) for output files.")
    fs.StringVarP(&g.GoHeaderFilePath, "go-header-file", "h", g.GoHeaderFilePath, "File containing boilerplate header text. The string YEAR will be replaced with the current 4-digit year.")
    fs.BoolVar(&g.VerifyOnly, "verify-only", g.VerifyOnly, "If true, only verify existing output, do not write anything.")
    fs.StringVar(&g.GeneratedBuildTag, "build-tag", g.GeneratedBuildTag, "A Go build tag to use to identify files generated by this command. Should be unique.")
    fs.StringVar(&g.TrimPathPrefix, "trim-path-prefix", g.TrimPathPrefix, "If set, trim the specified prefix from --output-package when generating files.")
}

为生成的代码文件构建标签

github.com/kubernetes/...

golang 复制代码
func Default() *GeneratorArgs {
    return &GeneratorArgs{
       OutputBase:                 DefaultSourceTree(),
       GoHeaderFilePath:           filepath.Join(DefaultSourceTree(), "k8s.io/gengo/boilerplate/boilerplate.go.txt"),
       GeneratedBuildTag:          "ignore_autogenerated",
       GeneratedByCommentTemplate: "// Code generated by GENERATOR_NAME. DO NOT EDIT.",
       defaultCommandLineFlags:    true,
    }
}

在Default中默认指定了GeneratedBuildTag,在每次构建的时候,代码生成器会把它作为构建标签打入生成的代码文件中。

比如在deepcopy_gen的代码生成器中,生成Package信息的时候就会加上这个标签。

github.com/kubernetes/...

golang 复制代码
func Packages(context *generator.Context, arguments *args.GeneratorArgs) generator.Packages {
    boilerplate, err := arguments.LoadGoBoilerplate()
    if err != nil {
       klog.Fatalf("Failed loading boilerplate: %v", err)
    }

    inputs := sets.NewString(context.Inputs...)
    packages := generator.Packages{}
    header := append([]byte(fmt.Sprintf("//go:build !%s\n// +build !%s\n\n", arguments.GeneratedBuildTag, arguments.GeneratedBuildTag)), boilerplate...)
    
    .......
}

最后,生成的代码header如下

这在k8s中表示该文件是由代码生成器自动生成的,不需要人工干预或者编辑。

收集Go包信息并且读取源码内容

github.com/kubernetes/...

golang 复制代码
// NewBuilder makes a new parser.Builder and populates it with the input
// directories.
func (g *GeneratorArgs) NewBuilder() (*parser.Builder, error) {
    b := parser.New()

    // flag for including *_test.go
    b.IncludeTestFiles = g.IncludeTestFiles

    // Ignore all auto-generated files.
    b.AddBuildTags(g.GeneratedBuildTag)

    for _, d := range g.InputDirs {
       var err error
       if strings.HasSuffix(d, "/...") {
          err = b.AddDirRecursive(strings.TrimSuffix(d, "/..."))
       } else {
          err = b.AddDir(d)
       }
       if err != nil {
          return nil, fmt.Errorf("unable to add directory %q: %v", d, err)
       }
    }
    return b, nil
}

代码生成器会通过input-dirs传入Go包路径,并且调用build.importPackage方法

github.com/kubernetes/...

golang 复制代码
// If we have not seen this before, process it now.
ignoreError := false
if _, found := b.parsed[pkgPath]; !found {
    // Ignore errors in paths that we're importing solely because
    // they're referenced by other packages.
    ignoreError = true

    // Add it.
    if err := b.addDir(dir, userRequested); err != nil {
       if isErrPackageNotFound(err) {
          klog.V(6).Info(err)
          return nil, nil
       }

       return nil, err
    }

    // Get the canonical path now that it has been added.
    if buildPkg, _ := b.getLoadedBuildPackage(dir); buildPkg != nil {
       canonicalPackage := canonicalizeImportPath(buildPkg.ImportPath)
       klog.V(5).Infof("importPackage %s, canonical path is %s", dir, canonicalPackage)
       pkgPath = canonicalPackage
    }
}

在上面的代码中会去先判断一下path有没有加入到b.parsed中,如果没有的话就把它"加入进去"。

加入解析map的过程:

golang 复制代码
// Get package information from the go/build package. Automatically excludes
// e.g. test files and files for other platforms-- there is quite a bit of
// logic of that nature in the build package.
func (b *Builder) importBuildPackage(dir string) (*build.Package, error) {
    if buildPkg, ok := b.getLoadedBuildPackage(dir); ok {
       return buildPkg, nil
    }
    // This validates the `package foo // github.com/bar/foo` comments.
    buildPkg, err := b.importWithMode(dir, build.ImportComment)
    if err != nil {
       if _, ok := err.(*build.NoGoError); !ok {
          return nil, fmt.Errorf("unable to import %q: %v", dir, err)
       }
    }
    if buildPkg == nil {
       // Might be an empty directory. Try to just find the dir.
       buildPkg, err = b.importWithMode(dir, build.FindOnly)
       if err != nil {
          return nil, err
       }
    }

    // Remember it under the user-provided name.
    klog.V(5).Infof("saving buildPackage %s", dir)
    b.setLoadedBuildPackage(dir, buildPkg)

    return buildPkg, nil
}

其中,build.ImportComment用于解析import语句后面的注释信息,build.FindOnly用于查找包所在的目录,但是这个地方不会读取源码内容。

这个时候我们就获取到了所有的包路径信息,并且可以准备go源码文件给解析器了,等待Lexer词法解析的处理。

github.com/kubernetes/...

golang 复制代码
files := []string{}
files = append(files, buildPkg.GoFiles...)
if b.IncludeTestFiles {
    files = append(files, buildPkg.TestGoFiles...)
}

for _, file := range files {
    if !strings.HasSuffix(file, ".go") {
       continue
    }
    absPath := filepath.Join(buildPkg.Dir, file)
    //源码内容
    data, err := ioutil.ReadFile(absPath)
    if err != nil {
       return fmt.Errorf("while loading %q: %v", absPath, err)
    }
    //解析
    err = b.addFile(pkgPath, absPath, data, userRequested)
    if err != nil {
       return fmt.Errorf("while parsing %q: %v", absPath, err)
    }
}

三、代码解析

Go是一个静态语言,它的语法很简单。Go语言标准库就支持代码解析功能。Kubernetes在该基础上进行了功能的封装。整体来说可以分为三步:

  • 通过标准库go/tokens提供的Lexer词法分所器对代码文本进行词法分析,最终得到Tokens
  • 通过标准库go/parser和go/ast 将 Tokens构建为抽象语法树(AST)
  • 通过标准库go/types下的Check方法进行抽象语法树类型检查,完成代码解析过程

源文件的token保存在b.fset中,这个对象用于记录文件中偏移量、类型、原始字面量及词法分析的数据结构和方法等。拿到token之后,使用parser.ParseFile解析器对Tokens数据进行处理,Parrser解析器将传入两种标识,其中parser.DeclarationErrors表示报告声明错误,parser.ParseComments表示解析代码中的注释并将它们添加到抽象语法树中。最终得到抽象语法树结构。

golang 复制代码
func (b *Builder) addFile(pkgPath importPathString, path string, src []byte, userRequested bool) error {
    for _, p := range b.parsed[pkgPath] {
       if path == p.name {
          klog.V(5).Infof("addFile %s %s already parsed, skipping", pkgPath, path)
          return nil
       }
    }
    klog.V(6).Infof("addFile %s %s", pkgPath, path)
    p, err := parser.ParseFile(b.fset, path, src, parser.DeclarationErrors|parser.ParseComments)
    if err != nil {
       return err
    }
    
    ......
}

拿到了抽象语法树之后,会对它进行类型检查,使用的是Go标准库的。但是这里增加了缓存,性能优化。

github.com/kubernetes/...

golang 复制代码
func (b *Builder) typeCheckPackage(pkgPath importPathString, logErr bool) (*tc.Package, error) {
    if pkg, ok := b.typeCheckedPackages[pkgPath]; ok {
       ........
    }
    ......
    pkg, err := c.Check(string(pkgPath), b.fset, files, nil)
    b.typeCheckedPackages[pkgPath] = pkg // record the result whether or not there was an error
    return pkg, err
}

四、类型系统

github.com/kubernetes/...

gengo的类型系统在Go语言本身的类型系统之上归类并添加了几种类型。gengo的类型系统在Go语言标准库go/types的基础上进行了封装。

所有的类型都通过vendor/k8s.io/gengo/parser/parse.go的walkType方法进行识别,gengo类型系统中的Struct、Map、Pointer、Interface等,与Go语言是供的类型并无差别。

小工具

抽取tag,并且转化成结构化数据,可以用于IOC或者自定义tag中。

golang 复制代码
// ExtractCommentTags parses comments for lines of the form:
//
//   'marker' + "key=value".
//
// Values are optional; "" is the default.  A tag can be specified more than
// one time and all values are returned.  If the resulting map has an entry for
// a key, the value (a slice) is guaranteed to have at least 1 element.
//
// Example: if you pass "+" for 'marker', and the following lines are in
// the comments:
//   +foo=value1
//   +bar
//   +foo=value2
//   +baz="qux"
// Then this function will return:
//   map[string][]string{"foo":{"value1, "value2"}, "bar": {""}, "baz": {"qux"}}
func ExtractCommentTags(marker string, lines []string) map[string][]string {
    out := map[string][]string{}
    for _, line := range lines {
       line = strings.Trim(line, " ")
       if len(line) == 0 {
          continue
       }
       if !strings.HasPrefix(line, marker) {
          continue
       }
       // TODO: we could support multiple values per key if we split on spaces
       kv := strings.SplitN(line[len(marker):], "=", 2)
       if len(kv) == 2 {
          out[kv[0]] = append(out[kv[0]], kv[1])
       } else if len(kv) == 1 {
          out[kv[0]] = append(out[kv[0]], "")
       }
    }
    return out
}

// ExtractSingleBoolCommentTag parses comments for lines of the form:
//
//   'marker' + "key=value1"
//
// If the tag is not found, the default value is returned.  Values are asserted
// to be boolean ("true" or "false"), and any other value will cause an error
// to be returned.  If the key has multiple values, the first one will be used.
func ExtractSingleBoolCommentTag(marker string, key string, defaultVal bool, lines []string) (bool, error) {
    values := ExtractCommentTags(marker, lines)[key]
    if values == nil {
       return defaultVal, nil
    }
    if values[0] == "true" {
       return true, nil
    }
    if values[0] == "false" {
       return false, nil
    }
    return false, fmt.Errorf("tag value for %q is not boolean: %q", key, values[0])
}

五、代码生成

代码生成部分主要要去看Generator接口。

github.com/kubernetes/...

  • Name:代码生成器的名称,返回值为生成的目标代码文件名的前缀,例如deepcopy-gen代码生成器的目标代码文件名的前缀为zz_generatted.deepcopy。
  • Filter:类型过滤器,过滤掉不符合当前代码生成器所需的类型。
  • Namers:命名管理器,支持创建不同类型的名称。例如,根据类型生成名称。
  • Init:代码生成器生成代码之前的初始化操作。
  • Finalize:代码生成器生成代码之后的收尾操作。
  • PackageVars:生成全局变量代码块,例如var(...)。
  • PackageConsts:生成常量代码块,例如consts(...)。
  • GenerateType:生成代码块。根据传入的类型生成代码。
  • Imports:获得需要生成的import代码块。通过该方法生成Go语言的impport代码块,例如import(...)。
  • Filename:生成的目标代码文件的全名,例如deepcopy-gen代码生成器的目标代码文件名为zz_generated.deepcopy.go。
  • FileType:生成代码文件的类型,一般为golang,也有protoidl等文件类型
golang 复制代码
// The call order for the functions that take a Context is:
// 1. Filter()        // Subsequent calls see only types that pass this.
// 2. Namers()        // Subsequent calls see the namers provided by this.
// 3. PackageVars()
// 4. PackageConsts()
// 5. Init()
// 6. GenerateType()  // Called N times, once per type in the context's Order.
// 7. Imports()
//
// You may have multiple generators for the same file.
type Generator interface {
    // The name of this generator. Will be included in generated comments.
    Name() string

    // Filter should return true if this generator cares about this type.
    // (otherwise, GenerateType will not be called.)
    //
    // Filter is called before any of the generator's other functions;
    // subsequent calls will get a context with only the types that passed
    // this filter.
    Filter(*Context, *types.Type) bool

    // If this generator needs special namers, return them here. These will
    // override the original namers in the context if there is a collision.
    // You may return nil if you don't need special names. These names will
    // be available in the context passed to the rest of the generator's
    // functions.
    //
    // A use case for this is to return a namer that tracks imports.
    Namers(*Context) namer.NameSystems

    // Init should write an init function, and any other content that's not
    // generated per-type. (It's not intended for generator specific
    // initialization! Do that when your Package constructs the
    // Generators.)
    Init(*Context, io.Writer) error

    // Finalize should write finish up functions, and any other content that's not
    // generated per-type.
    Finalize(*Context, io.Writer) error

    // PackageVars should emit an array of variable lines. They will be
    // placed in a var ( ... ) block. There's no need to include a leading
    // \t or trailing \n.
    PackageVars(*Context) []string

    // PackageConsts should emit an array of constant lines. They will be
    // placed in a const ( ... ) block. There's no need to include a leading
    // \t or trailing \n.
    PackageConsts(*Context) []string

    // GenerateType should emit the code for a particular type.
    GenerateType(*Context, *types.Type, io.Writer) error

    // Imports should return a list of necessary imports. They will be
    // formatted correctly. You do not need to include quotation marks,
    // return only the package name; alternatively, you can also return
    // imports in the format `name "path/to/pkg"`. Imports will be called
    // after Init, PackageVars, PackageConsts, and GenerateType, to allow
    // you to keep track of what imports you actually need.
    Imports(*Context) []string

    // Preferred file name of this generator, not including a path. It is
    // allowed for multiple generators to use the same filename, but it's
    // up to you to make sure they don't have colliding import names.
    // TODO: provide per-file import tracking, removing the requirement
    // that generators coordinate..
    Filename() string

    // A registered file type in the context to generate this file with. If
    // the FileType is not found in the context, execution will stop.
    FileType() string
}

下面以deepcopy-gen代码生成器为例,看一下代码生成的原理。

首先通过build.sh脚本,手动构建deepcopy-gen代码生成器二进制文件,然后将需要生成的包k8s.io/kubernetes/pkg/apis/abac/v1betal作为deepcopy-gen的输入源,并在内部进行一系列解析,最终通过-O参数生成名为zz_generated.deepcopy.go的代码文件。

实例化generator.Packages

github.com/kubernetes/...

golang 复制代码
packages = append(packages,
    &generator.DefaultPackage{
       PackageName: strings.Split(filepath.Base(pkg.Path), ".")[0],
       PackagePath: path,
       HeaderText:  header,
       GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
          return []generator.Generator{
             NewGenDeepCopy(arguments.OutputFileBaseName, pkg.Path, boundingDirs, (ptagValue == tagValuePackage), ptagRegister),
          }
       },
       FilterFunc: func(c *generator.Context, t *types.Type) bool {
          return t.Name.Package == pkg.Path
       },
    })

在deepcopy-gen代码生成器的Packages函数中,实例化generator.Packages对象并返回该对象。根据输入源信息,实例化当前Packages对象的结构:PackageName字段为vlbetal,PackagePath字段为k8s.io/kubernetes/pkg/apis/abac/vIbetal。其中,最主要的是GeneratorFunc定义了Generator接口的实现(即NewCGenDeepCopy实现了Generator接口方法)。

执行代码生成

github.com/kubernetes/...

在gengo中,generator定义代码生成器通用接口Generator。通过ExecutePackage函数,调用不同代码生成器(如deepcopy-gen)的Generator接口方法,并生成代码。代码示例如下:

golang 复制代码
func (c *Context) ExecutePackage(outDir string, p Package) error {
    path := filepath.Join(outDir, p.Path())
    ........
    for _, g := range p.Generators(packageContext) {
       // Filter out types the *generator* doesn't care about.
       genContext := packageContext.filteredBy(g.Filter)
       // Now add any extra name systems defined by this generator
       genContext = genContext.addNameSystems(g.Namers(genContext))

       fileType := g.FileType()
       if len(fileType) == 0 {
          return fmt.Errorf("generator %q must specify a file type", g.Name())
       }
       f := files[g.Filename()]
       ..........
}

在第一步中返回的DefaultPackage就是Package的一个实例,而Generators会直接调用d.GeneratorFunc(c)

ExecutePackage的执行流程如下:

  • 生成Header代码块
  • 生成Imports代码块
  • 生成Vars全局变量代码块
  • 生成Consts常量代码块
  • 生成Body代码块。

最后通过

golang 复制代码
err = assembler.AssembleFile(f, finalPath)

将生成代码写入文件当中。

真正执行代码生成的逻辑其实是在GenerateType方法中的,在Generator接口里。deepcopy-gen实现了这个接口:

github.com/kubernetes/...

在这个代码内部,通过Go语言标准库text/template模版语言渲染出生成的Body代码块信息。

golang 复制代码
sw := generator.NewSnippetWriter(w, c, "$", "$")
args := argsFromType(t)

if deepCopyIntoMethodOrDie(t) == nil {
    sw.Do("// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.\n", args)
    if isReference(t) {
       sw.Do("func (in $.type|raw$) DeepCopyInto(out *$.type|raw$) {\n", args)
       sw.Do("{in:=&in\n", nil)
    } else {
       sw.Do("func (in *$.type|raw$) DeepCopyInto(out *$.type|raw$) {\n", args)
    }
    if deepCopyMethodOrDie(t) != nil {
       if t.Methods["DeepCopy"].Signature.Receiver.Kind == types.Pointer {
          sw.Do("clone := in.DeepCopy()\n", nil)
          sw.Do("*out = *clone\n", nil)
       } else {
          sw.Do("*out = in.DeepCopy()\n", nil)
       }
       sw.Do("return\n", nil)
    } else {
       g.generateFor(t, sw)
       sw.Do("return\n", nil)
    }
    if isReference(t) {
       sw.Do("}\n", nil)
    }
    sw.Do("}\n\n", nil)
}

generator.NewSnippetWriter内部封装了text/template模板语言,通过将模板应用于数据结构来执行模板。SnippetWriter对象在实例化时传入模板指令的标识符(即指令开始为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ,指令结束为 ,指令结束为 </math>,指令结束为,有时候也会使用{{}}作为模板指令的标识符)。

SnippetWriter通过Do函数加载模板字符串,并执行渲染模板。模板指令中的点表示引用args参数传递到模板指令中。模板指令中的|表示管道符,即把左边的值传递给右边。

六、扩展-iocli

最近在调研一些代码生成的框架,其中看到了Alibaba开源的IOC-Golang中利用controller-tools提供的能力实现了一套简单的代码生成cli。

Repo:github.com/alibaba/IOC...

代码结构

该cli主要是解析一些注释上面的信息,并且根据注释信息来生成代码,达到IOC和AOP的部分能力。暂且不说代码生成的方式来实现IOC的优劣,个人感觉面对不可读的项目代码是很难受的,况且如果出现了引入的bug,进行排查将会是一场噩梦。虽然代码生成是主流,毕竟性能高,并且非极端case可能风险也比较小。

我们先来看一下iocli-gen的项目,作为一个代码生成的学习项目还是非常好的。

项目主要分为两个部分:

  • generator:维护代码生成的上下文,生成代码的主要逻辑
  • marker:解析tag的定义,如下:
golang 复制代码
// +ioc:autowire=true
// +ioc:autowire:type=allimpls
// +ioc:autowire:proxy=false
// +ioc:autowire:implements=github.com/alibaba/ioc-golang/iocli/gen/marker.DefinitionGetter

Marker

marker是用来配置tag的解析规则的,实际上需要的是一个*markers.Definition。比如:

golang 复制代码
type enableIOCGolangAutowireMarker struct {
}

func (m *enableIOCGolangAutowireMarker) GetMarkerDefinition() *markers.Definition {
    return markers.Must(markers.MakeDefinition("ioc:autowire", markers.DescribesType, false))
}

通过下面的方法可以定义作用类型(Package、Type、Field),返回类型和名称

golang 复制代码
func MakeDefinition(name string, target TargetType, output interface{}) (*Definition, error) {
    def := &Definition{
       Name:   name,
       Target: target,
       Output: reflect.TypeOf(output),
       Strict: true,
    }

    if err := def.loadFields(); err != nil {
       return nil, err
    }

    return def, nil
}

Generator

generator实现了Generator接口,并且完成了Marker的注册,和代码生成的真正逻辑。

Marker注册

golang 复制代码
// RegisterMarkers is called in main, register all markers
func (Generator) RegisterMarkers(into *markers.Registry) error {
    allImplGettersVal, err := allimpls.GetImpl(util.GetSDIDByStructPtr(new(marker.DefinitionGetter)))
    if err != nil {
       return err
    }
    defs := make([]*markers.Definition, 0)
    for _, g := range allImplGettersVal.([]marker.DefinitionGetter) {
       defs = append(defs, g.GetMarkerDefinition())
    }
    return markers.RegisterAll(into, defs...)
}

代码生成

golang 复制代码
func (d Generator) Generate(ctx *genall.GenerationContext) error {
    .......

    for _, root := range ctx.Roots {
       // 2. generate codes under current pkg
       outContents := objGenCtx.generateForPackage(ctx, root)
       if outContents == nil {
          continue
       }
       // 3. write codes to file
       writeOut(ctx, nil, root, outContents)
    }
    return nil
}

generateForPackage当中,根据传入的路径package会先进行一个类型检查。

golang 复制代码
infos := make([]*markers.TypeInfo, 0)
if err := markers.EachType(ctx.Collector, root, func(info *markers.TypeInfo) {
    infos = append(infos, info)
}); err != nil {
    root.AddError(err)
    return nil
}

然后会从上下文的容器中,读取出来在源文件目录下所有加载出来的markers.TypeInfo信息,这个信息实际上就保存了通过marker解析出来的数据,这个会是一个map形式。真正的代码生成逻辑如下所示:

golang 复制代码
copyCtx.generateMethodsFor(genCtx, root, imports, infos)

outBytes := outContent.Bytes()

outContent = new(bytes.Buffer)
writeHeader(root, outContent, root.Name, imports, ctx.HeaderText)
writeMethods(root, outContent, outBytes)

outBytes = outContent.Bytes()
formattedBytes, err := format.Source(outBytes)

generateMethodsFor中就是根据业务需求,依据markers.TypeInfo信息进行代码生成,其中可以根据业务需求定制模板。writeHeaderwriteMethods就是来补充生成文件信息的。

最后一步,就是输出文件到指定的目录下面了。

golang 复制代码
func writeOut(ctx *genall.GenerationContext, outputFile io.WriteCloser, root *loader.Package, outBytes []byte) {
    if outputFile == nil {
       var err error
       name := "zz_generated.ioc.go"
       outputFile, err = ctx.Open(root, name)
       if err != nil {
          root.AddError(err)
          return
       }
    }
    n, err := outputFile.Write(outBytes)
    if err != nil {
       root.AddError(err)
       return
    }
    if n < len(outBytes) {
       root.AddError(io.ErrShortWrite)
    }
}

所以,如果需要自己定制 cli 的话,也是可以借鉴这个项目的,只需要重写marker的定义,以及 代码生成 部分即可。这个项目为我们提供了比较好的cli模板。

相关推荐
木鱼时刻1 天前
容器与 Kubernetes 基本概念与架构
容器·架构·kubernetes
chuanauc1 天前
Kubernets K8s 学习
java·学习·kubernetes
庸子2 天前
基于Jenkins和Kubernetes构建DevOps自动化运维管理平台
运维·kubernetes·jenkins
李白你好2 天前
高级运维!Kubernetes(K8S)常用命令的整理集合
运维·容器·kubernetes
Connie14512 天前
k8s多集群管理中的联邦和舰队如何理解?
云原生·容器·kubernetes
伤不起bb2 天前
Kubernetes 服务发布基础
云原生·容器·kubernetes
别骂我h2 天前
Kubernetes服务发布基础
云原生·容器·kubernetes
weixin_399380692 天前
k8s一键部署tongweb企业版7049m6(by why+lqw)
java·linux·运维·服务器·云原生·容器·kubernetes
斯普信专业组3 天前
K8s环境下基于Nginx WebDAV与TLS/SSL的文件上传下载部署指南
nginx·kubernetes·ssl
&如歌的行板&3 天前
如何在postman中动态请求k8s中的pod ip(基于nacos)
云原生·容器·kubernetes