【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模板。

相关推荐
照物华6 小时前
K8s概念之进程、容器与 Pod 的终极指南
云原生·容器·kubernetes
Britz_Kevin1 天前
从零开始的云计算生活——第四十六天,铁杵成针,kubernetes模块之Configmap资源与Secret资源对象
kubernetes·云计算·生活
Britz_Kevin1 天前
从零开始的云计算生活——第四十七天,细水长流,kubernetes模块之ingress资源对象
kubernetes·云计算·生活
iceland91 天前
kubeadm方式部署k8s集群
云原生·容器·kubernetes
tb_first2 天前
k8sday11服务发现(2/2)
docker·云原生·容器·kubernetes·k8s
zxcxylong2 天前
almalinux9.6系统:k8s可选组件安装(1)
云原生·容器·kubernetes·metrics·almalinux·hpa·vpa
一个天蝎座 白勺 程序猿2 天前
Apache IoTDB(4):深度解析时序数据库 IoTDB 在Kubernetes 集群中的部署与实践指南
数据库·深度学习·kubernetes·apache·时序数据库·iotdb
xiao-xiang2 天前
redis-集成prometheus监控(k8s)
数据库·redis·kubernetes·k8s·grafana·prometheus
MANONGMN3 天前
Kubernetes(K8s)常用命令全解析:从基础到进阶
云原生·容器·kubernetes
Johny_Zhao3 天前
基于 Docker 的 LLaMA-Factory 全流程部署指南
linux·网络·网络安全·信息安全·kubernetes·云计算·containerd·yum源·系统运维·llama-factory