diff options
Diffstat (limited to 'src/cmd')
| -rw-r--r-- | src/cmd/go/internal/doc/dirs.go | 59 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/doc.go | 116 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/doc_test.go | 16 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/mod.go | 12 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/pkg.go | 12 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/pkgsite.go | 8 | ||||
| -rw-r--r-- | src/cmd/go/internal/modload/init.go | 5 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/doc_http_url.txt | 31 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/mod_doc.txt | 3 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/mod_outside.txt | 10 | ||||
| -rw-r--r-- | src/cmd/internal/script/scripttest/readme.go | 1 |
11 files changed, 171 insertions, 102 deletions
diff --git a/src/cmd/go/internal/doc/dirs.go b/src/cmd/go/internal/doc/dirs.go index 5efd40b1d5..86b4b526a3 100644 --- a/src/cmd/go/internal/doc/dirs.go +++ b/src/cmd/go/internal/doc/dirs.go @@ -15,6 +15,9 @@ import ( "strings" "sync" + "cmd/go/internal/cfg" + "cmd/go/internal/modload" + "golang.org/x/mod/semver" ) @@ -41,29 +44,18 @@ var dirs Dirs // dirsInit starts the scanning of package directories in GOROOT and GOPATH. Any // extra paths passed to it are included in the channel. func dirsInit(extra ...Dir) { - if buildCtx.GOROOT == "" { - stdout, err := exec.Command("go", "env", "GOROOT").Output() - if err != nil { - if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { - log.Fatalf("failed to determine GOROOT: $GOROOT is not set and 'go env GOROOT' failed:\n%s", ee.Stderr) - } - log.Fatalf("failed to determine GOROOT: $GOROOT is not set and could not run 'go env GOROOT':\n\t%s", err) - } - buildCtx.GOROOT = string(bytes.TrimSpace(stdout)) - } - dirs.hist = make([]Dir, 0, 1000) dirs.hist = append(dirs.hist, extra...) dirs.scan = make(chan Dir) go dirs.walk(codeRoots()) } -// goCmd returns the "go" command path corresponding to buildCtx.GOROOT. +// goCmd returns the "go" command path corresponding to cfg.GOROOT. func goCmd() string { - if buildCtx.GOROOT == "" { + if cfg.GOROOT == "" { return "go" } - return filepath.Join(buildCtx.GOROOT, "bin", "go") + return filepath.Join(cfg.GOROOT, "bin", "go") } // Reset puts the scan back at the beginning. @@ -187,30 +179,31 @@ var usingModules bool func findCodeRoots() []Dir { var list []Dir if !testGOPATH { - // Check for use of modules by 'go env GOMOD', - // which reports a go.mod file path if modules are enabled. - stdout, _ := exec.Command(goCmd(), "env", "GOMOD").Output() - gomod := string(bytes.TrimSpace(stdout)) - - usingModules = len(gomod) > 0 - if usingModules && buildCtx.GOROOT != "" { - list = append(list, - Dir{dir: filepath.Join(buildCtx.GOROOT, "src"), inModule: true}, - Dir{importPath: "cmd", dir: filepath.Join(buildCtx.GOROOT, "src", "cmd"), inModule: true}) - } + // TODO: use the same state used to load the package. + // For now it's okay to use a new state because we're just + // using it to determine whether we're in module mode. But + // it would be good to avoid an extra run of modload.Init. + if state := modload.NewState(); state.WillBeEnabled() { + usingModules = state.HasModRoot() + if usingModules && cfg.GOROOT != "" { + list = append(list, + Dir{dir: filepath.Join(cfg.GOROOT, "src"), inModule: true}, + Dir{importPath: "cmd", dir: filepath.Join(cfg.GOROOT, "src", "cmd"), inModule: true}) + } - if gomod == os.DevNull { - // Modules are enabled, but the working directory is outside any module. - // We can still access std, cmd, and packages specified as source files - // on the command line, but there are no module roots. - // Avoid 'go list -m all' below, since it will not work. - return list + if !usingModules { + // Modules are enabled, but the working directory is outside any module. + // We can still access std, cmd, and packages specified as source files + // on the command line, but there are no module roots. + // Avoid 'go list -m all' below, since it will not work. + return list + } } } if !usingModules { - if buildCtx.GOROOT != "" { - list = append(list, Dir{dir: filepath.Join(buildCtx.GOROOT, "src")}) + if cfg.GOROOT != "" { + list = append(list, Dir{dir: filepath.Join(cfg.GOROOT, "src")}) } for _, root := range splitGopath() { list = append(list, Dir{dir: filepath.Join(root, "src")}) diff --git a/src/cmd/go/internal/doc/doc.go b/src/cmd/go/internal/doc/doc.go index 4acee8ed42..3ebd0f5dab 100644 --- a/src/cmd/go/internal/doc/doc.go +++ b/src/cmd/go/internal/doc/doc.go @@ -21,6 +21,10 @@ import ( "strings" "cmd/go/internal/base" + "cmd/go/internal/cfg" + "cmd/go/internal/load" + "cmd/go/internal/modload" + "cmd/go/internal/search" "cmd/internal/telemetry/counter" ) @@ -362,14 +366,29 @@ func failMessage(paths []string, symbol, method string) error { // and there may be more matches. For example, if the argument // is rand.Float64, we must scan both crypto/rand and math/rand // to find the symbol, and the first call will return crypto/rand, true. -func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) { +func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg *load.Package, path, symbol string, more bool) { wd, err := os.Getwd() if err != nil { log.Fatal(err) } + loader := modload.NewState() + if testGOPATH { + loader = modload.DisabledState() + } + if len(args) > 0 && strings.Index(args[0], "@") >= 0 { + // Version query: force no root + loader.ForceUseModules = true + loader.RootMode = modload.NoRoot + modload.Init(loader) + } else if loader.WillBeEnabled() { + loader.InitWorkfile() + modload.Init(loader) + modload.LoadModFile(loader, context.TODO()) + } + if len(args) == 0 { // Easy: current directory. - return importDir(wd), "", "", false + return mustLoadPackage(ctx, loader, wd), "", "", false } arg := args[0] @@ -388,11 +407,11 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * log.Fatal("cannot use @version with local or absolute paths") } - importPkg := func(p string) (*build.Package, error) { + importPkg := func(p string) (*load.Package, error) { if version != "" { - return loadVersioned(ctx, p, version) + return loadVersioned(ctx, loader, p, version) } - return build.Import(p, wd, build.ImportComment) + return loadPackage(ctx, loader, p) } switch len(args) { @@ -407,16 +426,16 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * return pkg, arg, args[1], false } for { - dir, importPath, ok := findNextPackage(arg) + importPath, ok := findNextPackage(arg) if !ok { break } if version != "" { - if pkg, err = loadVersioned(ctx, importPath, version); err == nil { + if pkg, err = loadVersioned(ctx, loader, importPath, version); err == nil { return pkg, arg, args[1], true } } else { - if pkg, err = build.ImportDir(dir, build.ImportComment); err == nil { + if pkg, err = loadPackage(ctx, loader, importPath); err == nil { return pkg, arg, args[1], true } } @@ -434,7 +453,7 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * // package paths as their prefix. var importErr error if filepath.IsAbs(arg) { - pkg, importErr = build.ImportDir(arg, build.ImportComment) + pkg, importErr = loadPackage(ctx, loader, arg) if importErr == nil { return pkg, arg, "", false } @@ -449,7 +468,7 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * // Kills the problem caused by case-insensitive file systems // matching an upper case name as a package name. if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) { - pkg, err := build.ImportDir(".", build.ImportComment) + pkg, err := loadPackage(ctx, loader, ".") if err == nil { return pkg, "", arg, false } @@ -475,7 +494,7 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * symbol = arg[period+1:] } // Have we identified a package already? - pkg, err := importPkg(arg[0:period]) + pkg, err := loadPackage(ctx, loader, arg[0:period]) if err == nil { return pkg, arg[0:period], symbol, false } @@ -483,18 +502,16 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * // or ivy/value for robpike.io/ivy/value. pkgName := arg[:period] for { - dir, importPath, ok := findNextPackage(pkgName) + importPath, ok := findNextPackage(pkgName) if !ok { break } if version != "" { - if pkg, err = loadVersioned(ctx, importPath, version); err == nil { - return pkg, arg[0:period], symbol, true - } - } else { - if pkg, err = build.ImportDir(dir, build.ImportComment); err == nil { + if pkg, err = loadVersioned(ctx, loader, importPath, version); err == nil { return pkg, arg[0:period], symbol, true } + } else if pkg, err = loadPackage(ctx, loader, importPath); err == nil { + return pkg, arg[0:period], symbol, true } } dirs.Reset() // Next iteration of for loop must scan all the directories again. @@ -506,7 +523,7 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * if version == "" { version = v } - pkg, err := loadVersioned(ctx, pkgPath, version) + pkg, err := loadVersioned(ctx, loader, pkgPath, version) if err == nil { return pkg, pkgPath, "", false } @@ -536,7 +553,38 @@ func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg * } } // Guess it's a symbol in the current directory. - return importDir(wd), "", arg, false + return mustLoadPackage(ctx, loader, wd), "", arg, false +} + +func loadPackage(ctx context.Context, loader *modload.State, pattern string) (*load.Package, error) { + if !search.NewMatch(pattern).IsLiteral() { + return nil, fmt.Errorf("pattern %q does not specify a single package", pattern) + } + + pkgOpts := load.PackageOpts{ + IgnoreImports: true, + SuppressBuildInfo: true, + SuppressEmbedFiles: true, + } + pkgs := load.PackagesAndErrors(loader, ctx, pkgOpts, []string{pattern}) + + if len(pkgs) != 1 { + return nil, fmt.Errorf("path %q matched multiple packages", pattern) + } + + p := pkgs[0] + if p.Error != nil { + return nil, p.Error + } + return p, nil +} + +func mustLoadPackage(ctx context.Context, loader *modload.State, dir string) *load.Package { + pkg, err := loadPackage(ctx, loader, dir) + if err != nil { + log.Fatal(err) + } + return pkg } // dotPaths lists all the dotted paths legal on Unix-like and @@ -564,15 +612,6 @@ func isDotSlash(arg string) bool { return false } -// importDir is just an error-catching wrapper for build.ImportDir. -func importDir(dir string) *build.Package { - pkg, err := build.ImportDir(dir, build.ImportComment) - if err != nil { - log.Fatal(err) - } - return pkg -} - // parseSymbol breaks str apart into a symbol and method. // Both may be missing or the method may be missing. // If present, each must be a valid Go identifier. @@ -600,36 +639,33 @@ func isExported(name string) bool { return unexported || token.IsExported(name) } -// findNextPackage returns the next full file name path and import path that -// matches the (perhaps partial) package path pkg. The boolean reports if -// any match was found. -func findNextPackage(pkg string) (string, string, bool) { +// findNextPackage returns the next import path that matches the +// (perhaps partial) package path pkg. The boolean reports if any match was found. +func findNextPackage(pkg string) (string, bool) { if filepath.IsAbs(pkg) { if dirs.offset == 0 { dirs.offset = -1 - return pkg, "", true + return pkg, true } - return "", "", false + return "", false } if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name. - return "", "", false + return "", false } pkg = path.Clean(pkg) pkgSuffix := "/" + pkg for { d, ok := dirs.Next() if !ok { - return "", "", false + return "", false } if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) { - return d.dir, d.importPath, true + return d.importPath, true } } } -var buildCtx = build.Default - // splitGopath splits $GOPATH into a list of roots. func splitGopath() []string { - return filepath.SplitList(buildCtx.GOPATH) + return filepath.SplitList(cfg.BuildContext.GOPATH) } diff --git a/src/cmd/go/internal/doc/doc_test.go b/src/cmd/go/internal/doc/doc_test.go index 3eeaffc25b..cd931e33c0 100644 --- a/src/cmd/go/internal/doc/doc_test.go +++ b/src/cmd/go/internal/doc/doc_test.go @@ -7,7 +7,6 @@ package doc import ( "bytes" "flag" - "go/build" "internal/testenv" "log" "os" @@ -16,18 +15,19 @@ import ( "runtime" "strings" "testing" + + "cmd/go/internal/cfg" ) func TestMain(m *testing.M) { // Clear GOPATH so we don't access the user's own packages in the test. - buildCtx.GOPATH = "" + cfg.BuildContext.GOPATH = "" testGOPATH = true // force GOPATH mode; module test is in cmd/go/testdata/script/mod_doc.txt // Set GOROOT in case runtime.GOROOT is wrong (for example, if the test was // built with -trimpath). dirsInit would identify it using 'go env GOROOT', // but we can't be sure that the 'go' in $PATH is the right one either. - buildCtx.GOROOT = testenv.GOROOT(nil) - build.Default.GOROOT = testenv.GOROOT(nil) + cfg.GOROOT = testenv.GOROOT(nil) // Add $GOROOT/src/cmd/go/internal/doc/testdata explicitly so we can access its contents in the test. // Normally testdata directories are ignored, but sending it to dirs.scan directly is @@ -37,9 +37,9 @@ func TestMain(m *testing.M) { panic(err) } dirsInit( - Dir{importPath: "testdata", dir: testdataDir}, - Dir{importPath: "testdata/nested", dir: filepath.Join(testdataDir, "nested")}, - Dir{importPath: "testdata/nested/nested", dir: filepath.Join(testdataDir, "nested", "nested")}) + Dir{importPath: "cmd/go/internal/doc/testdata", dir: testdataDir}, + Dir{importPath: "cmd/go/internal/doc/testdata/nested", dir: filepath.Join(testdataDir, "nested")}, + Dir{importPath: "cmd/go/internal/doc/testdata/nested/nested", dir: filepath.Join(testdataDir, "nested", "nested")}) os.Exit(m.Run()) } @@ -1204,7 +1204,7 @@ func TestDotSlashLookup(t *testing.T) { t.Skip("scanning file system takes too long") } maybeSkip(t) - t.Chdir(filepath.Join(buildCtx.GOROOT, "src", "text")) + t.Chdir(filepath.Join(cfg.GOROOT, "src", "text")) var b strings.Builder var flagSet flag.FlagSet diff --git a/src/cmd/go/internal/doc/mod.go b/src/cmd/go/internal/doc/mod.go index a15ff29526..e139cc1208 100644 --- a/src/cmd/go/internal/doc/mod.go +++ b/src/cmd/go/internal/doc/mod.go @@ -8,7 +8,6 @@ import ( "context" "debug/buildinfo" "fmt" - "go/build" "os/exec" "cmd/go/internal/load" @@ -16,24 +15,19 @@ import ( ) // loadVersioned loads a package at a specific version. -func loadVersioned(ctx context.Context, pkgPath, version string) (*build.Package, error) { - loaderState := modload.NewState() - loaderState.ForceUseModules = true - loaderState.RootMode = modload.NoRoot - modload.Init(loaderState) - +func loadVersioned(ctx context.Context, loader *modload.State, pkgPath, version string) (*load.Package, error) { var opts load.PackageOpts args := []string{ fmt.Sprintf("%s@%s", pkgPath, version), } - pkgs, err := load.PackagesAndErrorsOutsideModule(loaderState, ctx, opts, args) + pkgs, err := load.PackagesAndErrorsOutsideModule(loader, ctx, opts, args) if err != nil { return nil, err } if len(pkgs) != 1 { return nil, fmt.Errorf("incorrect number of packages: want 1, got %d", len(pkgs)) } - return pkgs[0].Internal.Build, nil + return pkgs[0], nil } // inferVersion checks if the argument matches a command on $PATH and returns its module path and version. diff --git a/src/cmd/go/internal/doc/pkg.go b/src/cmd/go/internal/doc/pkg.go index c531595f86..bd4a2f6e4f 100644 --- a/src/cmd/go/internal/doc/pkg.go +++ b/src/cmd/go/internal/doc/pkg.go @@ -9,7 +9,6 @@ import ( "bytes" "fmt" "go/ast" - "go/build" "go/doc" "go/format" "go/parser" @@ -22,6 +21,9 @@ import ( "strings" "unicode" "unicode/utf8" + + "cmd/go/internal/cfg" + "cmd/go/internal/load" ) const ( @@ -36,7 +38,7 @@ type Package struct { pkg *ast.Package // Parsed package. file *ast.File // Merged from all files in the package doc *doc.Package - build *build.Package + build *load.Package typedValue map[*doc.Value]bool // Consts and vars related to types. constructor map[*doc.Func]bool // Constructors. fs *token.FileSet // Needed for printing. @@ -96,8 +98,8 @@ func (pkg *Package) prettyPath() string { // Also convert everything to slash-separated paths for uniform handling. path = filepath.Clean(filepath.ToSlash(pkg.build.Dir)) // Can we find a decent prefix? - if buildCtx.GOROOT != "" { - goroot := filepath.Join(buildCtx.GOROOT, "src") + if cfg.GOROOT != "" { + goroot := filepath.Join(cfg.GOROOT, "src") if p, ok := trim(path, filepath.ToSlash(goroot)); ok { return p } @@ -137,7 +139,7 @@ func (pkg *Package) Fatalf(format string, args ...any) { // parsePackage turns the build package we found into a parsed package // we can then use to generate documentation. -func parsePackage(writer io.Writer, pkg *build.Package, userPath string) *Package { +func parsePackage(writer io.Writer, pkg *load.Package, userPath string) *Package { // include tells parser.ParseDir which files to include. // That means the file must be in the build package's GoFiles, CgoFiles, // TestGoFiles or XTestGoFiles list only (no tag-ignored files, swig or diff --git a/src/cmd/go/internal/doc/pkgsite.go b/src/cmd/go/internal/doc/pkgsite.go index dc344cbbca..2c135cdc34 100644 --- a/src/cmd/go/internal/doc/pkgsite.go +++ b/src/cmd/go/internal/doc/pkgsite.go @@ -16,6 +16,8 @@ import ( "os/signal" "path/filepath" "strings" + + "cmd/go/internal/cfg" ) // pickUnusedPort finds an unused port by trying to listen on port 0 @@ -48,6 +50,10 @@ func doPkgsite(urlPath, fragment string) error { path += "#" + fragment } + if file := os.Getenv("TEST_GODOC_URL_FILE"); file != "" { + return os.WriteFile(file, []byte(path+"\n"), 0666) + } + // Turn off the default signal handler for SIGINT (and SIGQUIT on Unix) // and instead wait for the child process to handle the signal and // exit before exiting ourselves. @@ -73,7 +79,7 @@ func doPkgsite(urlPath, fragment string) error { const version = "v0.0.0-20251223195805-1a3bd3c788fe" cmd := exec.Command(goCmd(), "run", "golang.org/x/pkgsite/cmd/internal/doc@"+version, - "-gorepo", buildCtx.GOROOT, + "-gorepo", cfg.GOROOT, "-http", addr, "-open", path) cmd.Env = env diff --git a/src/cmd/go/internal/modload/init.go b/src/cmd/go/internal/modload/init.go index 8501d3f5ee..e71e467d9f 100644 --- a/src/cmd/go/internal/modload/init.go +++ b/src/cmd/go/internal/modload/init.go @@ -467,6 +467,11 @@ func NewState() *State { return s } +func DisabledState() *State { + fips140.Init() + return &State{initialized: true, modulesEnabled: false} +} + func (s *State) Fetcher() *modfetch.Fetcher { return s.fetcher } diff --git a/src/cmd/go/testdata/script/doc_http_url.txt b/src/cmd/go/testdata/script/doc_http_url.txt new file mode 100644 index 0000000000..19947dd770 --- /dev/null +++ b/src/cmd/go/testdata/script/doc_http_url.txt @@ -0,0 +1,31 @@ +env TEST_GODOC_URL_FILE=$WORK/url.txt + +# Outside of a module. +go doc -http +grep '/std' $TEST_GODOC_URL_FILE + +# Inside a module with a major version suffix. +# We should use the major version suffix rather than +# the location in the gopath (#75976). +cd example.com/m +go doc -http +grep '/example.com/m/v5' $TEST_GODOC_URL_FILE +go doc -http . +grep '/example.com/m/v5' $TEST_GODOC_URL_FILE + +# In GOPATH mode, we should use the location in the GOPATH. +env GO111MODULE=off +go doc -http +grep '/std' $TEST_GODOC_URL_FILE +# TODO(matloob): This should probably this be the same as 'go doc -http .' +! grep '/example.com/m' $TEST_GODOC_URL_FILE +go doc -http . +grep '/example.com/m' $TEST_GODOC_URL_FILE +! grep '/example.com/m/v5' $TEST_GODOC_URL_FILE + +-- example.com/m/go.mod -- +module example.com/m/v5 + +go 1.27 +-- example.com/m/m.go -- +package m
\ No newline at end of file diff --git a/src/cmd/go/testdata/script/mod_doc.txt b/src/cmd/go/testdata/script/mod_doc.txt index bf0a19d770..cbfd9336dd 100644 --- a/src/cmd/go/testdata/script/mod_doc.txt +++ b/src/cmd/go/testdata/script/mod_doc.txt @@ -38,9 +38,8 @@ go doc rsc.io/quote stdout 'Package quote collects pithy sayings.' # Check that a sensible error message is printed when a package is not found. -env GOPROXY=off ! go doc example.com/hello -stderr '^doc: cannot find module providing package example.com/hello: module lookup disabled by GOPROXY=off$' +stderr 'doc: no required module provides package example.com/hello; to add it:\n\s+go get example.com/hello' # When in a module with a vendor directory, doc should use the vendored copies # of the packages. 'std' and 'cmd' are convenient examples of such modules. diff --git a/src/cmd/go/testdata/script/mod_outside.txt b/src/cmd/go/testdata/script/mod_outside.txt index 7a0dc9f22f..18cc3d5a27 100644 --- a/src/cmd/go/testdata/script/mod_outside.txt +++ b/src/cmd/go/testdata/script/mod_outside.txt @@ -173,13 +173,15 @@ go build -n fmt go build ./newgo/newgo.go # 'go doc' without arguments implicitly operates on the current directory, and should fail. -# TODO(golang.org/issue/32027): currently, it succeeds. cd needmod -go doc +! go doc +stderr '^doc: go: go.mod file not found in current directory or any parent directory; see ''go help modules''$' cd .. -# 'go doc' of a non-module directory should also succeed. -go doc ./needmod +# 'go doc' of a non-module directory should also fail. +! go doc ./needmod +stderr '^doc: go: go.mod file not found in current directory or any parent directory; see ''go help modules''$' + # 'go doc' should succeed for standard-library packages. go doc fmt diff --git a/src/cmd/internal/script/scripttest/readme.go b/src/cmd/internal/script/scripttest/readme.go index af7397223f..8f333b06c2 100644 --- a/src/cmd/internal/script/scripttest/readme.go +++ b/src/cmd/internal/script/scripttest/readme.go @@ -37,6 +37,7 @@ func checkScriptReadme(t *testing.T, engine *script.Engine, env []string, script doc := new(strings.Builder) cmd := testenv.Command(t, gotool, "doc", "cmd/internal/script") + cmd.Dir = t.TempDir() // make sure the test is not running inside the std or cmd module of another GOROOT cmd.Env = env cmd.Stdout = doc if err := cmd.Run(); err != nil { |
