From 9005ad21089f91d1a0651c2468d7d12981c11c19 Mon Sep 17 00:00:00 2001 From: Peter Edge Date: Sat, 23 Feb 2019 12:06:01 -0500 Subject: [PATCH] Get ghz compliant with errcheck --- Makefile | 2 +- cmd/ghz-web/main.go | 19 ++++- cmd/ghz/main.go | 28 ++++--- go.mod | 2 + go.sum | 4 + internal/common.go | 10 ++- printer/printer.go | 85 ++++++++++---------- runner/requester.go | 164 +++++++++++++++++++++++++-------------- web/database/database.go | 13 ++-- web/router/router.go | 20 +++-- 10 files changed, 213 insertions(+), 134 deletions(-) diff --git a/Makefile b/Makefile index b522a64b..d9a0ed36 100644 --- a/Makefile +++ b/Makefile @@ -100,7 +100,7 @@ staticcheck: # Lint runs all linters. This is the main lint target to run. # TODO: add errcheck and staticcheck when the code is updated to pass them .PHONY: lint -lint: golint +lint: golint errcheck # Test runs go test on GO_PKGS. This does not produce code coverage. .PHONY: test diff --git a/cmd/ghz-web/main.go b/cmd/ghz-web/main.go index 33de0979..2889f96b 100644 --- a/cmd/ghz-web/main.go +++ b/cmd/ghz-web/main.go @@ -52,14 +52,16 @@ func main() { conf, err := config.Read(cfgPath) if err != nil { - panic(err) + handleError(err) } db, err := database.New(conf.Database.Type, conf.Database.Connection, conf.Log.Level == "debug") if err != nil { - panic(err) + handleError(err) } - defer db.Close() + defer func() { + handleError(db.Close()) + }() info := &api.ApplicationInfo{ Version: version, @@ -70,7 +72,7 @@ func main() { server, err := router.New(db, info, conf) if err != nil { - panic(err) + handleError(err) } router.PrintRoutes(server) @@ -78,3 +80,12 @@ func main() { hostPort := net.JoinHostPort("", strconv.FormatUint(uint64(conf.Server.Port), 10)) server.Logger.Fatal(server.Start(hostPort)) } + +func handleError(err error) { + if err != nil { + if errString := err.Error(); errString != "" { + fmt.Fprintln(os.Stderr, errString) + } + os.Exit(1) + } +} diff --git a/cmd/ghz/main.go b/cmd/ghz/main.go index 0bbdcf4e..87b2e93d 100644 --- a/cmd/ghz/main.go +++ b/cmd/ghz/main.go @@ -147,7 +147,7 @@ func main() { var conf config err := configor.Load(&conf, cfgPath) if err != nil { - errAndExit(err.Error()) + handleError(err) } cfg = &conf @@ -159,7 +159,7 @@ func main() { var err error cfg, err = createConfigFromArgs() if err != nil { - errAndExit(err.Error()) + handleError(err) } } @@ -220,7 +220,7 @@ func main() { report, err := runner.Run(cfg.Call, cfg.Host, options...) if err != nil { - errAndExit(err.Error()) + handleError(err) } output := os.Stdout @@ -228,23 +228,29 @@ func main() { if outputPath != "" { f, err := os.Create(outputPath) if err != nil { - errAndExit(err.Error()) + handleError(err) } - defer f.Close() + defer func() { + handleError(f.Close()) + }() output = f } p := printer.ReportPrinter{ Report: report, - Out: output} + Out: output, + } - p.Print(cfg.Format) + handleError(p.Print(cfg.Format)) } -func errAndExit(msg string) { - fmt.Fprintf(os.Stderr, msg) - fmt.Fprintf(os.Stderr, "\n") - os.Exit(1) +func handleError(err error) { + if err != nil { + if errString := err.Error(); errString != "" { + fmt.Fprintln(os.Stderr, errString) + } + os.Exit(1) + } } func usageAndExit(msg string) { diff --git a/go.mod b/go.mod index d91edbcd..7a0ed14b 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,8 @@ require ( github.com/stretchr/testify v1.3.0 github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect + go.uber.org/atomic v1.3.2 // indirect + go.uber.org/multierr v1.1.0 golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc // indirect golang.org/x/net v0.0.0-20190110200230-915654e7eabc google.golang.org/grpc v1.17.0 diff --git a/go.sum b/go.sum index 500aff49..a0f1e5af 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,10 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= diff --git a/internal/common.go b/internal/common.go index 57bdd8f1..b884723a 100644 --- a/internal/common.go +++ b/internal/common.go @@ -11,13 +11,15 @@ import ( "github.com/bojand/ghz/internal/helloworld" ) -// TestPort is the port +// TestPort is the port. var TestPort string -// TestLocalhost is the localhost +// TestLocalhost is the localhost. var TestLocalhost string -// StartServer starts server +// StartServer starts the server. +// +// For testing only. func StartServer(secure bool) (*helloworld.Greeter, *grpc.Server, error) { lis, err := net.Listen("tcp", ":0") if err != nil { @@ -44,7 +46,7 @@ func StartServer(secure bool) (*helloworld.Greeter, *grpc.Server, error) { TestLocalhost = "localhost:" + TestPort go func() { - s.Serve(lis) + _ = s.Serve(lis) }() return gs, s, err diff --git a/printer/printer.go b/printer/printer.go index 806cfbba..e35228ea 100644 --- a/printer/printer.go +++ b/printer/printer.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "log" "strings" "text/tabwriter" "time" @@ -27,7 +26,7 @@ type ReportPrinter struct { // Print the report using the given format // If format is "csv" detailed listing is printer in csv format. // Otherwise the summary of results is printed. -func (rp *ReportPrinter) Print(format string) { +func (rp *ReportPrinter) Print(format string) error { switch format { case "", "csv": outputTmpl := defaultTmpl @@ -37,44 +36,38 @@ func (rp *ReportPrinter) Print(format string) { buf := &bytes.Buffer{} templ := template.Must(template.New("tmpl").Funcs(tmplFuncMap).Parse(outputTmpl)) if err := templ.Execute(buf, *rp.Report); err != nil { - log.Println("error:", err.Error()) - return + return err } - - rp.printf(buf.String()) - - rp.printf("\n") + buf.WriteString("\n") + return rp.printf(buf.String()) case "json", "pretty": rep, err := json.Marshal(*rp.Report) if err != nil { - log.Println("error:", err.Error()) - return + return err } if format == "pretty" { var out bytes.Buffer err = json.Indent(&out, rep, "", " ") if err != nil { - log.Println("error:", err.Error()) - return + return err } rep = out.Bytes() } - - rp.printf(string(rep)) + return rp.printf(string(rep)) case "html": buf := &bytes.Buffer{} templ := template.Must(template.New("tmpl").Funcs(tmplFuncMap).Parse(htmlTmpl)) if err := templ.Execute(buf, *rp.Report); err != nil { - log.Println("error:", err.Error()) - return + return err } - - rp.printf(buf.String()) + return rp.printf(buf.String()) case "influx-summary": - rp.printf(rp.getInfluxLine()) + return rp.printf(rp.getInfluxLine()) case "influx-details": - rp.printInfluxDetails() + return rp.printInfluxDetails() + default: + return fmt.Errorf("unknown format: %s", format) } } @@ -90,7 +83,7 @@ func (rp *ReportPrinter) getInfluxLine() string { return fmt.Sprintf("%v,%v %v %v", measurement, tags, fields, timestamp) } -func (rp *ReportPrinter) printInfluxDetails() { +func (rp *ReportPrinter) printInfluxDetails() error { measurement := "ghz_detail" commonTags := rp.getInfluxTags(false) @@ -112,8 +105,11 @@ func (rp *ReportPrinter) printInfluxDetails() { fields := strings.Join(values, ",") - fmt.Fprintf(rp.Out, fmt.Sprintf("%v,%v %v %v\n", measurement, tags, fields, timestamp)) + if _, err := fmt.Fprintf(rp.Out, fmt.Sprintf("%v,%v %v %v\n", measurement, tags, fields, timestamp)); err != nil { + return err + } } + return nil } func (rp *ReportPrinter) getInfluxTags(addErrors bool) string { @@ -239,8 +235,9 @@ func (rp *ReportPrinter) getInfluxFields() string { return strings.Join(s, ",") } -func (rp *ReportPrinter) printf(s string, v ...interface{}) { - fmt.Fprintf(rp.Out, s, v...) +func (rp *ReportPrinter) printf(s string, v ...interface{}) error { + _, err := fmt.Fprintf(rp.Out, s, v...) + return err } var tmplFuncMap = template.FuncMap{ @@ -336,9 +333,11 @@ func formatStatusCode(statusCodeDist map[string]int) string { buf := &bytes.Buffer{} w := tabwriter.NewWriter(buf, 0, 0, padding, ' ', 0) for status, count := range statusCodeDist { - fmt.Fprintf(w, " [%+s]\t%+v responses\t\n", status, count) + // bytes.Buffer can be assumed to not fail on write + _, _ = fmt.Fprintf(w, " [%+s]\t%+v responses\t\n", status, count) } - w.Flush() + // bytes.Buffer can be assumed to not fail on write + _ = w.Flush() return buf.String() } @@ -347,9 +346,11 @@ func formatErrorDist(errDist map[string]int) string { buf := &bytes.Buffer{} w := tabwriter.NewWriter(buf, 0, 0, padding, ' ', 0) for status, count := range errDist { - fmt.Fprintf(w, " [%+v]\t%+s\t\n", count, status) + // bytes.Buffer can be assumed to not fail on write + _, _ = fmt.Fprintf(w, " [%+v]\t%+s\t\n", count, status) } - w.Flush() + // bytes.Buffer can be assumed to not fail on write + _ = w.Flush() return buf.String() } @@ -396,14 +397,14 @@ duration (ms),status,error{{ range $i, $v := .Details }} - + - + - +
@@ -415,7 +416,7 @@ duration (ms),status,error{{ range $i, $v := .Details }} {{ end }}

- +

- + {{ if gt (len .Tags) 0 }}
{{ range $tag, $val := .Tags }} - +
{{ $tag }} @@ -494,7 +495,7 @@ duration (ms),status,error{{ range $i, $v := .Details }}

{{ end }} - +
@@ -614,9 +615,9 @@ duration (ms),status,error{{ range $i, $v := .Details }}
- + {{ if gt (len .ErrorDist) 0 }} - +
@@ -658,14 +659,14 @@ duration (ms),status,error{{ range $i, $v := .Details }}

Data

- + JSON CSV
- +

@@ -675,7 +676,7 @@ duration (ms),status,error{{ range $i, $v := .Details }}
- +
@@ -771,11 +772,11 @@ duration (ms),status,error{{ range $i, $v := .Details }} setJSONDownloadLink(); setCSVDownloadLink(); - + - + ` ) diff --git a/runner/requester.go b/runner/requester.go index c782d492..861823fc 100644 --- a/runner/requester.go +++ b/runner/requester.go @@ -14,6 +14,7 @@ import ( "github.com/jhump/protoreflect/dynamic/grpcdynamic" "github.com/jhump/protoreflect/grpcreflect" + "go.uber.org/multierr" "google.golang.org/grpc" "google.golang.org/grpc/keepalive" "google.golang.org/grpc/metadata" @@ -48,6 +49,7 @@ type Requester struct { reqCounter int64 stopReason StopReason + lock sync.Mutex } func newRequester(c *RunConfig) (*Requester, error) { @@ -74,16 +76,21 @@ func newRequester(c *RunConfig) (*Requester, error) { } else { // use reflection to get method decriptor var cc *grpc.ClientConn - cc, err = reqr.connect(false) + // temporary connection for reflection, do not store as requester.cc + cc, err = reqr.newClientConn(false) if err != nil { return nil, err } - defer cc.Close() + defer func() { + // purposefully ignoring error as we do not care if there + // is an error on close + _ = cc.Close() + }() - ctx := context.Background() - ctx, _ = context.WithTimeout(ctx, c.dialTimeout) - // cancel ignored because we manually do Close() + // cancel is ignored here as connection.Close() is used. + // See https://godoc.org/google.golang.org/grpc#DialContext + ctx, _ := context.WithTimeout(context.Background(), c.dialTimeout) md := make(metadata.MD) if c.rmd != nil && len(*c.rmd) > 0 { @@ -108,7 +115,6 @@ func newRequester(c *RunConfig) (*Requester, error) { } // fill in the rest - // reqr.cc = cc reqr.mtd = mtd return reqr, nil @@ -117,33 +123,29 @@ func newRequester(c *RunConfig) (*Requester, error) { // Run makes all the requests and returns a report of results // It blocks until all work is done. func (b *Requester) Run() (*Report, error) { - b.start = time.Now() - - // we may have connection from newRequestor if we used reflection - if b.cc == nil { - cc, err := b.connect(true) - if err != nil { - return nil, err - } + start := time.Now() - b.cc = cc + cc, err := b.openClientConn() + if err != nil { + return nil, err } - defer b.cc.Close() - - b.stub = grpcdynamic.NewStub(b.cc) - + b.lock.Lock() + b.start = start + b.stub = grpcdynamic.NewStub(cc) b.reporter = newReporter(b.results, b.config) + b.lock.Unlock() go func() { b.reporter.Run() }() - b.runWorkers() + err = b.runWorkers() report := b.Finish() + b.closeClientConn() - return report, nil + return report, err } // Stop stops the test @@ -153,9 +155,11 @@ func (b *Requester) Stop(reason StopReason) { b.stopCh <- true } + b.lock.Lock() b.stopReason = reason + b.lock.Unlock() - b.cc.Close() + b.closeClientConn() } // Finish finishes the test run @@ -169,7 +173,31 @@ func (b *Requester) Finish() *Report { return b.reporter.Finalize(b.stopReason, total) } -func (b *Requester) connect(stats bool) (*grpc.ClientConn, error) { +func (b *Requester) openClientConn() (*grpc.ClientConn, error) { + b.lock.Lock() + defer b.lock.Unlock() + if b.cc != nil { + return b.cc, nil + } + cc, err := b.newClientConn(true) + if err != nil { + return nil, err + } + b.cc = cc + return b.cc, nil +} + +func (b *Requester) closeClientConn() { + b.lock.Lock() + defer b.lock.Unlock() + if b.cc == nil { + return + } + _ = b.cc.Close() + b.cc = nil +} + +func (b *Requester) newClientConn(withStatsHandler bool) (*grpc.ClientConn, error) { var opts []grpc.DialOption if b.config.insecure { @@ -194,7 +222,7 @@ func (b *Requester) connect(stats bool) (*grpc.ClientConn, error) { })) } - if stats { + if withStatsHandler { opts = append(opts, grpc.WithStatsHandler(&statsHandler{b.results})) } @@ -202,45 +230,48 @@ func (b *Requester) connect(stats bool) (*grpc.ClientConn, error) { return grpc.DialContext(ctx, b.config.host, opts...) } -func (b *Requester) runWorkers() { - var wg sync.WaitGroup - wg.Add(b.config.c) - +func (b *Requester) runWorkers() error { nReqPerWorker := b.config.n / b.config.c + if b.config.c == 0 { + return nil + } + + errC := make(chan error, b.config.c) // Ignore the case where b.N % b.C != 0. for i := 0; i < b.config.c; i++ { go func() { - defer wg.Done() - - b.runWorker(nReqPerWorker) + errC <- b.runWorker(nReqPerWorker) }() } - wg.Wait() + + var err error + for i := 0; i < b.config.c; i++ { + err = multierr.Append(err, <-errC) + } + return err } -func (b *Requester) runWorker(n int) { +func (b *Requester) runWorker(n int) error { var throttle <-chan time.Time if b.config.qps > 0 { throttle = time.Tick(b.qpsTick) } + var err error for i := 0; i < n; i++ { // Check if application is stopped. Do not send into a closed channel. select { case <-b.stopCh: - return + return nil default: if b.config.qps > 0 { <-throttle } - - err := b.makeRequest() - if err != nil { - fmt.Println(err.Error()) - } + err = multierr.Append(err, b.makeRequest()) } } + return err } func (b *Requester) makeRequest() error { @@ -281,43 +312,50 @@ func (b *Requester) makeRequest() error { } ctx := context.Background() + var cancel context.CancelFunc - ctx, cancel := context.WithCancel(ctx) + if b.config.timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, b.config.timeout) + } else { + ctx, cancel = context.WithCancel(ctx) + } defer cancel() - ctx, _ = context.WithTimeout(ctx, b.config.timeout) - // include the metadata if reqMD != nil { ctx = metadata.NewOutgoingContext(ctx, *reqMD) } if b.mtd.IsClientStreaming() && b.mtd.IsServerStreaming() { - b.makeBidiRequest(&ctx, streamInput) - } else if b.mtd.IsClientStreaming() { - b.makeClientStreamingRequest(&ctx, streamInput) - } else if b.mtd.IsServerStreaming() { - b.makeServerStreamingRequest(&ctx, input) - } else { - b.stub.InvokeRpc(ctx, b.mtd, input) + return b.makeBidiRequest(&ctx, streamInput) } - - return nil + if b.mtd.IsClientStreaming() { + return b.makeClientStreamingRequest(&ctx, streamInput) + } + if b.mtd.IsServerStreaming() { + return b.makeServerStreamingRequest(&ctx, input) + } + // TODO: handle response? + _, err = b.stub.InvokeRpc(ctx, b.mtd, input) + return err } -func (b *Requester) makeClientStreamingRequest(ctx *context.Context, input *[]*dynamic.Message) { +func (b *Requester) makeClientStreamingRequest(ctx *context.Context, input *[]*dynamic.Message) error { str, err := b.stub.InvokeRpcClientStream(*ctx, b.mtd) counter := 0 + // TODO: need to handle and propagate errors for err == nil { streamInput := *input inputLen := len(streamInput) if input == nil || inputLen == 0 { - str.CloseAndReceive() + // TODO: need to handle error + _, _ = str.CloseAndReceive() break } if counter == inputLen { - str.CloseAndReceive() + // TODO: need to handle error + _, _ = str.CloseAndReceive() break } @@ -333,15 +371,18 @@ func (b *Requester) makeClientStreamingRequest(ctx *context.Context, input *[]*d if err == io.EOF { // We get EOF on send if the server says "go away" // We have to use CloseAndReceive to get the actual code - str.CloseAndReceive() + // TODO: need to handle error + _, _ = str.CloseAndReceive() break } counter++ } + return nil } -func (b *Requester) makeServerStreamingRequest(ctx *context.Context, input *dynamic.Message) { +func (b *Requester) makeServerStreamingRequest(ctx *context.Context, input *dynamic.Message) error { str, err := b.stub.InvokeRpcServerStream(*ctx, b.mtd, input) + // TODO: need to handle and propagate errors for err == nil { _, err := str.RecvMsg() if err != nil { @@ -351,21 +392,25 @@ func (b *Requester) makeServerStreamingRequest(ctx *context.Context, input *dyna break } } + return nil } -func (b *Requester) makeBidiRequest(ctx *context.Context, input *[]*dynamic.Message) { +func (b *Requester) makeBidiRequest(ctx *context.Context, input *[]*dynamic.Message) error { str, err := b.stub.InvokeRpcBidiStream(*ctx, b.mtd) counter := 0 + // TODO: need to handle and propagate errors for err == nil { streamInput := *input inputLen := len(streamInput) if input == nil || inputLen == 0 { - str.CloseSend() + // TODO: need to handle error + _ = str.CloseSend() break } if counter == inputLen { - str.CloseSend() + // TODO: need to handle error + _ = str.CloseSend() break } @@ -389,6 +434,7 @@ func (b *Requester) makeBidiRequest(ctx *context.Context, input *[]*dynamic.Mess break } } + return nil } func min(a, b int) int { diff --git a/web/database/database.go b/web/database/database.go index fa3d2b39..0aaecbe3 100644 --- a/web/database/database.go +++ b/web/database/database.go @@ -16,7 +16,9 @@ const dbName = "../test/test.db" // New creates a new wrapper for the gorm database framework. func New(dialect, connection string, log bool) (*Database, error) { - createDirectoryIfSqlite(dialect, connection) + if err := createDirectoryIfSqlite(dialect, connection); err != nil { + return nil, err + } db, err := gorm.Open(dialect, connection) if err != nil { @@ -47,14 +49,15 @@ func New(dialect, connection string, log bool) (*Database, error) { return &Database{DB: db}, nil } -func createDirectoryIfSqlite(dialect string, connection string) { +func createDirectoryIfSqlite(dialect string, connection string) error { if dialect == "sqlite3" { if _, err := os.Stat(filepath.Dir(connection)); os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(connection), 0777); err != nil { - panic(err) + return err } } } + return nil } // Database is a wrapper for the gorm framework. @@ -63,6 +66,6 @@ type Database struct { } // Close closes the gorm database connection. -func (d *Database) Close() { - d.DB.Close() +func (d *Database) Close() error { + return d.DB.Close() } diff --git a/web/router/router.go b/web/router/router.go index af26c640..17b801e1 100644 --- a/web/router/router.go +++ b/web/router/router.go @@ -27,7 +27,11 @@ func New(db *database.Database, appInfo *api.ApplicationInfo, conf *config.Confi s := echo.New() s.Logger.SetLevel(getLogLevel(conf)) - s.Logger.SetOutput(getLogOutput(conf)) + output, err := getLogOutput(conf) + if err != nil { + return nil, err + } + s.Logger.SetOutput(output) s.Validator = &CustomValidator{validator: validator.New()} @@ -103,13 +107,13 @@ func New(db *database.Database, appInfo *api.ApplicationInfo, conf *config.Confi // load the precompiled statik fs statikFS, err := fs.New() if err != nil { - log.Fatal(err) + return nil, err } // get the index file indexFile, err := fs.ReadFile(statikFS, "/index.html") if err != nil { - log.Fatal(err) + return nil, err } // wrap the handler @@ -163,24 +167,24 @@ func getLogLevel(config *config.Config) log.Lvl { } } -func getLogOutput(config *config.Config) io.Writer { +func getLogOutput(config *config.Config) (io.Writer, error) { logPath := strings.TrimSpace(config.Log.Path) if logPath == "" { - return os.Stdout + return os.Stdout, nil } if _, err := os.Stat(filepath.Dir(logPath)); os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(logPath), 0777); err != nil { - panic(err) + return nil, err } } f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - panic(err) + return nil, err } - return f + return f, nil } // PrintRoutes prints routes in the server