CLI ツール開発を支える技術 2019春 / Techniques that support building CLI tools, 2019 Spring

CLI ツール開発を支える技術 2019春 / Techniques that support building CLI tools, 2019 Spring

9eed44f137609e6ce3b6f1e14f80b9e1?s=128

Masayuki Izumi

May 20, 2019
Tweet

Transcript

  1. ©2019 Wantedly, Inc. $-*πʔϧ։ൃΛࢧ͑Δٕज़य़ Techniques that support building CLI tools,

    2019 Spring golang.tokyo #24 May 20, 2019 - Masayuki izumi
  2. ϖʔδλΠτϧ ϖʔδαϒλΠτϧ ©2019 Wantedly, Inc. (P$PO4QSJOH͓͔ͭΕ͞·Ͱͨ͠ ӡӦͷΈͳ͞ΜɺొஃऀͷΈͳ͞ΜɺࢀՃ͞ΕͨΈͳ͞Μ#

  3. ©2019 Wantedly, Inc.

  4. ©2019 Wantedly, Inc.

  5. ϖʔδλΠτϧ ϖʔδαϒλΠτϧ ©2019 Wantedly, Inc. ͓Θ͔Γ͍͚ͨͩͨͩΖ͏͔

  6. ©2019 Wantedly, Inc.

  7. ϖʔδλΠτϧ ϖʔδαϒλΠτϧ ©2019 Wantedly, Inc. ͔ͿΒͳ͍Α͏ͳ࿩Λ͠·͢

  8. ©2019 Wantedly, Inc. $-*πʔϧ։ൃΛࢧ͑Δٕज़य़ Techniques that support building CLI tools,

    2019 Spring golang.tokyo #24 May 20, 2019 - Masayuki izumi
  9. ©2019 Wantedly, Inc. $ whoami @izumin5210 Wantedly, Inc. ‣ Wantedly

    People - Application Engineer (Go, Ruby, Web Frontend) ‣ Interested in… - Microservices - Developer Productivity - Developer Experience
  10. ©2019 Wantedly, Inc. Talk abst and desc

  11. ©2019 Wantedly, Inc. Building CLI tools 101 Section subhead

  12. ©2019 Wantedly, Inc. (PͰ$-*ͱ͍͑͹ʜ Α͘ฉ͘ศརύοέʔδ܈ ‣ ͪΐͬͱศརͳϑϥάύʔα  AqBHA TUBOEBSEMJC

    JNQPSUFECZQLHT   ATQGQqBHA TUBST JNQPSUFECZQLHT   AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT   ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT #VJMEJOH$-*UPPMT ਺ࣈ͸͢΂ͯ࣌఺ͷ΋ͷ TUBS਺͸HJUIVCDPNɺඃJNQPSU਺͸HPEPDPSHΛࢀর
  13. ©2019 Wantedly, Inc. (PͰ$-*ͱ͍͑͹ʜ Α͘ฉ͘ศརύοέʔδ܈ ‣ ͪΐͬͱศརͳϑϥάύʔα  AqBHA TUBOEBSEMJC

    JNQPSUFECZQLHT   ATQGQqBHA TUBST JNQPSUFECZQLHT   AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT   ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT #VJMEJOH$-*UPPMT ਺ࣈ͸͢΂ͯ࣌఺ͷ΋ͷ TUBS਺͸HJUIVCDPNɺඃJNQPSU਺͸HPEPDPSHΛࢀর AqBHA͕-POHPQUJPOະରԠͳͷ͕ಋೖͷେ͖ͳϞνϕʔγϣϯ  AQqBHAAqBHAͷ*'Λ౿ऻͭͭ͠104*9(/6TUZMFରԠ  AHPqBHTATUSVDUUBHΛΈͯ͏·͘΍Δ  ALJOHQJOAqVFOUTUZMFͳ"1*Ͱ͔͍͍ͬ͜
  14. ©2019 Wantedly, Inc. (PͰ$-*ͱ͍͑͹ʜ Α͘ฉ͘ศརύοέʔδ܈ ‣ ͪΐͬͱศརͳϑϥάύʔα  AqBHA TUBOEBSEMJC

    JNQPSUFECZQLHT   ATQGQqBHA TUBST JNQPSUFECZQLHT   AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT   ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT #VJMEJOH$-*UPPMT ਺ࣈ͸͢΂ͯ࣌఺ͷ΋ͷ TUBS਺͸HJUIVCDPNɺඃJNQPSU਺͸HPEPDPSHΛࢀর
  15. ©2019 Wantedly, Inc. (PͰ$-*ͱ͍͑͹ʜ Α͘ฉ͘ศརύοέʔδ܈ ‣ ͪΐͬͱศརͳϑϥάύʔα  AqBHA TUBOEBSEMJC

    JNQPSUFECZQLHT   ATQGQqBHA TUBST JNQPSUFECZQLHT   AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT   ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT ‣ αϒίϚϯυ ϔϧϓੜ੒ ίϚϯυ࣮ߦલޙͷϑοΫͳͲͷػೳΛ࣋ͭଟػೳϑϨʔϜϫʔΫ  ATQGDPCSBA TUBST JNQPSUFECZQLHT   AVSGBWFDMJA TUBST JNQPSUFECZQLHT   ANJUDIFMMIDMJA TUBST JNQPSUFECZQLHT #VJMEJOH$-*UPPMT ਺ࣈ͸͢΂ͯ࣌఺ͷ΋ͷ TUBS਺͸HJUIVCDPNɺඃJNQPSU਺͸HPEPDPSHΛࢀর
  16. ©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ن໛ײɾػೳཁ͔݅ΒΞϓϩʔνΛܾΊΔ ‣ ͭ͘Γ͍ͨπʔϧ͸ͲΕ͘Β͍ͷن໛ʹͳΔ͔  ௒୯ػೳɾ͍͔ͭ͘ͷϑϥάͱҾ਺͕͋Δ͚ͩ 

    FHADEA AMTA AJ[VNJOHFYA  ͍͔ͭ͘αϒίϚϯυʢαϒαϒίϚϯυ ʜʣΛ΋ͭ  FHAEPDLFSA ALVCFDUMA AJ[VNJOHSBQJA AJ[VNJOTVCFFA #VJMEJOH$-*UPPMT
  17. ©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ن໛ײɾػೳཁ͔݅ΒΞϓϩʔνΛܾΊΔ ‣ ͭ͘Γ͍ͨπʔϧ͸ͲΕ͘Β͍ͷن໛ʹͳΔ͔  ௒୯ػೳɾ͍͔ͭ͘ͷϑϥάͱҾ਺͕͋Δ͚ͩ 

    FHADEA AMTA AJ[VNJOHFYA  ͍͔ͭ͘αϒίϚϯυʢαϒαϒίϚϯυ ʜʣΛ΋ͭ  FHAEPDLFSA ALVCFDUMA AJ[VNJOHSBQJA AJ[VNJOTVCFFA ‣ ͲΕ͘Β͍ͷػೳੑ͕ඞཁ͔  ϑϥά ɾ؀ڥม਺ɾઃఆϑΝΠϧʜͲ͜·Ͱඞཁʁ #VJMEJOH$-*UPPMT
  18. ©2019 Wantedly, Inc. switch res := flag.Arg(0); res { case

    "topic": if flag.NArg() < 2 { return errors.New("must specify subcommand: [create]") } switch cmd := flag.Arg(1); cmd { case "create": name := flag.Arg(2) if name == "" { return errors.New("must specify a topic name") } _, err := client.CreateTopic(ctx, name) if err != nil { return err } case "publish": body := flag.Arg(2) if body == "" { return errors.New("must specify a message body") } if err := requireString("topic", topicID); err != nil { return err } topic, err := getTopic(ctx, *topicID, client) if err != nil { return err } _ = topic.Publish(ctx, &pubsub.Message{Data: []byte(body)}) topic.Stop() default: return fmt.Errorf("unknown subcommand: %q for %s", cmd, res) } case "subscription": if flag.NArg() < 2 { ӈͷίʔυʢJ[VNJOQVCTVCDMJʣ͸ѹ౗తʹࠔͬͯ ͍Δ͜ͱ͕͋ΓɺͦΕΛര଎Ͱղܾͨͯ͘͠ॻ͍ͨͱ͖ ͷ΋ͷ ن໛ײɾεϐʔυײʹΑͬͯ͸ ͜͏͍͏ίʔυ͔Β͸͡ΊΔͷ΋ѱ͘ͳ͍ͷ͔΋ʜʁ
  19. ©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ·Α͍ͬͯΔͱ͖ͷબఆϑϩʔ ‣ ͸΍͘ղܾ͍ͨ͠໰୊͕͋ΔʂͳΜͰ΋͍͍͔Βૣ͘࡞Γ͍ͨʂ  Ұ୴AqBHA͔AQqBHAͰ͓஡Λ୙ͭͭ͠ര଎Ͱͭͬͯ͘ɺ͋ͱ͔Βߟ͑·͠ΐ͏ 

    αϒίϚϯυ͕ඞཁͳ৔߹͸ADPCSBAΛ࢖͍·͠ΐ͏ #VJMEJOH$-*UPPMT
  20. ©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ·Α͍ͬͯΔͱ͖ͷબఆϑϩʔ ‣ ͸΍͘ղܾ͍ͨ͠໰୊͕͋ΔʂͳΜͰ΋͍͍͔Βૣ͘࡞Γ͍ͨʂ  Ұ୴AqBHA͔AQqBHAͰ͓஡Λ୙ͭͭ͠ര଎Ͱͭͬͯ͘ɺ͋ͱ͔Βߟ͑·͠ΐ͏ 

    αϒίϚϯυ͕ඞཁͳ৔߹͸ADPCSBAΛ࢖͍·͠ΐ͏ ‣ αϒίϚϯυ͸͍·ͷͱ͜Ζ࢖͏༧ఆ͸ͳ͍  AQqBHA͕͓͢͢ΊͰ͢ #VJMEJOH$-*UPPMT
  21. ©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ·Α͍ͬͯΔͱ͖ͷબఆϑϩʔ ‣ ͸΍͘ղܾ͍ͨ͠໰୊͕͋ΔʂͳΜͰ΋͍͍͔Βૣ͘࡞Γ͍ͨʂ  Ұ୴AqBHA͔AQqBHAͰ͓஡Λ୙ͭͭ͠ര଎Ͱͭͬͯ͘ɺ͋ͱ͔Βߟ͑·͠ΐ͏ 

    αϒίϚϯυ͕ඞཁͳ৔߹͸ADPCSBAΛ࢖͍·͠ΐ͏ ‣ αϒίϚϯυ͸͍·ͷͱ͜Ζ࢖͏༧ఆ͸ͳ͍  AQqBHA͕͓͢͢ΊͰ͢ ‣ ؀ڥม਺ɾઃఆϑΝΠϧΛ࢖͏༧ఆ͕͋Δ  ADPCSBA͕͓͢͢ΊͰ͢ #VJMEJOH$-*UPPMT
  22. ©2019 Wantedly, Inc. Why `pflag` and `cobra`? ·Α͍ͬͯΔͱ͖ͷ͓͢͢Ίબఆ ‣ ADPCSBAͷϑϥάύʔα͸AQqBHAͰ࣮૷͞Ε͍ͯΔ

     ͋ͱ͔ΒʮαϒίϚϯυ͕΄͍͠ʂʯͱ͍͏ͱ͖ʹADPCSBAಋೖ͕؆୯ ࠷ॳͷٕज़બఆ HJUIVCDPNTQGDPCSB
  23. ©2019 Wantedly, Inc. Why `pflag` and `cobra`? ·Α͍ͬͯΔͱ͖ͷ͓͢͢Ίબఆ ‣ ADPCSBAͷϑϥάύʔα͸AQqBHAͰ࣮૷͞Ε͍ͯΔ

     ͋ͱ͔ΒʮαϒίϚϯυ͕΄͍͠ʂʯͱ͍͏ͱ͖ʹADPCSBAಋೖ͕؆୯ ‣ ઃఆϑΝΠϧɾ؀ڥม਺ಡΈग़͠ʹศརͳύοέʔδͰ͋ΔAWJQFSAͱAQqBHAͷ૬ੑ͕ྑ͍  ͋ͱ͔ΒʮઃఆϑΝΠϧ΄͍͠ʂʯͳΜͯ͜ͱ͸Α͋͘Δ  AWJQFSAʹ͍ͭͯ͸ޙड़͠·͢ ࠷ॳͷٕज़બఆ HJUIVCDPNTQGDPCSB
  24. ©2019 Wantedly, Inc. Directory(package) structure ·Α͍ͬͯΔͱ͖ͷ͓͢͢Ίύοέʔδߏ੒ ‣ ADNEBQQOBNFAʹNBJOΛஔ͍͍ͯΔέʔεΛΑ͘ݟΔ  FHNPCZNPCZ

    LVCFSOFUFT QSPNFUIFVT (PPHMF$MPVE1MBUGPSNLBOJLP ‣ ෳ਺όΠφϦΛు͖͍ͨͱ͖ʹରԠͰ͖Δ ‣ ϝΠϯͷΞϓϦέʔγϣϯίʔυ͸ʮQLHҎԼʹஔ͘ʯʮϧʔτʹஔ͘ʯ ͷύλʔϯΛΑ͘ݟΔ #VJMEJOH$-*UPPMT ├── cmd │ └── <app_name> │ └── main.go ├── go.mod ├── go.sum └── pkg └── <app_name> └── **/*.go ├── cmd │ └── <app_name> │ └── main.go ├── go.mod ├── go.sum └── */**.go
  25. ©2019 Wantedly, Inc. Configuration Section subhead

  26. ©2019 Wantedly, Inc. ‣ ϢʔβʹແବͳೖྗΛͤͨ͘͞ͳ͍ ‣ ҰํͰɺσϑΥϧτͰશͯͷχʔζʹ౴͑Δͷ͸ϜϦ $POGJHVSBUJPO

  27. ©2019 Wantedly, Inc. Ͳ͔͜ΒɾͲ͏͍͏༏ઌॱҐͰઃఆΛಡΈࠐΉʁ
 ઃఆϑΝΠϧʢ)0.&ʣ ઃఆϑΝΠϧʢ18%ʣ ؀ڥม਺ ࣮ߦ࣌Φϓγϣϯ $POGJHVSBUJPO

  28. ©2019 Wantedly, Inc. github.com/spf13/viper ͳΜͰ΋Ͱ͖ΔઃఆಡΈࠐΈύοέʔδ ‣ ͋ΒΏΔͱ͜Ζ͔ΒઃఆΛूΊͯ͘ΕΔ  ίϚϯυϥΠϯϑϥάʢAQqBHA AqBHAʣ

     ؀ڥม਺  ઃఆϑΝΠϧʢ+40/ 50.- :".- )$- +BWBQSPQFSUJFTʣ  ϦϞʔτͷઃఆʢFHFUDE $POTVMʣ  σϑΥϧτ஋ ‣ ಡΈऔͬͨઃఆ஋͸TUSVDUʹNBQQJOHͰ͖Δ $POGJHVSBUJPO
  29. ©2019 Wantedly, Inc. cobra & viper $POGJHVSBUJPO type Config struct

    { Port int Host string Scheme string User, Pass string } func main() { var cfg Config cmd := &cobra.Command{ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { // env viper.AutomaticEnv() // config file viper.AddConfigPath(".") viper.AddConfigPath("$HOME") viper.SetConfigName(".awesomeapp") err := viper.ReadInConfig() if err != nil { fmt.Fprintln(os.Stderr, err) } // flags err = viper.BindPFlags(cmd.Flags()) if err != nil { fmt.Fprintln(os.Stderr, err) }
  30. ©2019 Wantedly, Inc. $POGJHVSBUJPO type Config struct { Port int

    Host string Scheme string User, Pass string } func main() { var cfg Config cmd := &cobra.Command{ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { // … }, RunE: func(_ *cobra.Command, args []string) error { fmt.Printf("%#v\n", cfg) return nil }, } cmd.PersistentFlags().IntP("port", "p", 5000, "") cmd.PersistentFlags().StringP("host", "o", "127.0.0.1", "") cmd.PersistentFlags().String("scheme", "http", "") cmd.PersistentFlags().String("user", "root", "") cmd.PersistentFlags().String("pass", "", "") err := cmd.Execute() if err != nil { fmt.Fprintln(os.Stderr, err) cobra & viper ‣ ઃఆ஋ΛಡΈࠐΉߏ଄ମͷఆٛ ‣ ίϚϯυϥΠϯϑϥάͷఆٛ
  31. ©2019 Wantedly, Inc. cobra & viper $POGJHVSBUJPO Scheme string User,

    Pass string } func main() { var cfg Config cmd := &cobra.Command{ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { // env viper.AutomaticEnv() // config file viper.AddConfigPath(".") viper.AddConfigPath("$HOME") viper.SetConfigName(".awesomeapp") err := viper.ReadInConfig() if err != nil { fmt.Fprintln(os.Stderr, err) } // flags err = viper.BindPFlags(cmd.Flags()) if err != nil { fmt.Fprintln(os.Stderr, err) } return viper.Unmarshal(&cfg) }, RunE: func(_ *cobra.Command, args []string) error { fmt.Printf("%#v\n", cfg) return nil }, ‣ ؀ڥม਺ͷಡΈࠐΈ ‣ ઃఆϑΝΠϧͷಡΈࠐΈ  ݕࡧର৅ύεͷࢦఆ ‣ ࣮૷ಡΉݶΓA)0.&A͸͏·͘ղऍ͢Δ  ϑΝΠϧ໊ʢ֦ுࢠൈ͖ʣͷࢦఆ ‣ ίϚϯυϥΠϯϑϥάͷόΠϯυ
  32. ©2019 Wantedly, Inc. cobra & viper $POGJHVSBUJPO Scheme string User,

    Pass string } func main() { var cfg Config cmd := &cobra.Command{ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { // env viper.AutomaticEnv() // config file viper.AddConfigPath(".") viper.AddConfigPath("$HOME") viper.SetConfigName(".awesomeapp") err := viper.ReadInConfig() if err != nil { fmt.Fprintln(os.Stderr, err) } // flags err = viper.BindPFlags(cmd.Flags()) if err != nil { fmt.Fprintln(os.Stderr, err) } return viper.Unmarshal(&cfg) }, RunE: func(_ *cobra.Command, args []string) error { fmt.Printf("%#v\n", cfg) return nil }, ‣ ؀ڥม਺ͷಡΈࠐΈ ‣ ઃఆϑΝΠϧͷಡΈࠐΈ  ݕࡧର৅ύεͷࢦఆ ‣ ࣮૷ಡΉݶΓA)0.&A͸͏·͘ղऍ͢Δ  ϑΝΠϧ໊ʢ֦ுࢠൈ͖ʣͷࢦఆ ‣ ίϚϯυϥΠϯϑϥάͷόΠϯυ :) % cat .awesomeapp.toml host = "localhost" port = 8000 :) % USER=admin PASS=pass go run . --port 8080 main.Config{Port:8080, Host:"localhost", Scheme:"http", User:"admin", Pass:"pass"}
  33. ©2019 Wantedly, Inc. Debuggability Section subhead

  34. ©2019 Wantedly, Inc. ‣ ϏϧυࡁΈόΠφϦͰόά౿ΜͩΒͲ͏͢Δʁ ‣ %FCVHHFS΋1SJOU%FCVH΋Ͱ͖ͳ͍ͱ੾Γ෼͚΋େม %FCVHHBCJMJUZ

  35. ©2019 Wantedly, Inc. ‣ ϏϧυࡁΈόΠφϦͰόά౿ΜͩΒͲ͏͢Δʁ ‣ %FCVHHFS΋1SJOU%FCVH΋Ͱ͖ͳ͍ͱ੾Γ෼͚΋େม %FCVHHBCJMJUZ

  36. ©2019 Wantedly, Inc. -v (--verbose) / --debug
 σόοάΦϓγϣϯΛͭͬͯ͘ɺ։ൃதʹ͔ͭͬͨ1SJOU%FCVH͸ͦͷ··࢒͢ $POGJHVSBUJPO

  37. ©2019 Wantedly, Inc. ‣ W WFSCPTF   */'0MFWFM͘Β͍·ͰͷϩάΛग़͢ ‣

    EFCVH  %&#6(MFWFM΋;͘Ίɺ͢΂ͯͷϩάΛग़͢ %FCVHHBCJMJUZ -PHMFWFM
  38. ©2019 Wantedly, Inc. %FCVHHBCJMJUZ e.g. cobra & zap RunE: func(_

    *cobra.Command, args []string) error { zap.L().Info("loaded config", zap.Any("config", cfg)) return nil }, } // ... var debug, verbose bool cobra.OnInitialize(func() { var zapCfg zap.Config switch { case debug: zapCfg = zap.NewProductionConfig() zapCfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) case verbose: zapCfg = zap.NewDevelopmentConfig() zapCfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) } zapLogger, err := zapCfg.Build() if err != nil { fmt.Fprintln(os.Stderr, err) return } zap.ReplaceGlobals(zapLogger) }) cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose lev cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Debug level log")
  39. ©2019 Wantedly, Inc. %FCVHHBCJMJUZ e.g. cobra & zap RunE: func(_

    *cobra.Command, args []string) error { zap.L().Info("loaded config", zap.Any("config", cfg)) return nil }, } // ... var debug, verbose bool cobra.OnInitialize(func() { var zapCfg zap.Config switch { case debug: zapCfg = zap.NewProductionConfig() zapCfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) case verbose: zapCfg = zap.NewDevelopmentConfig() zapCfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) } zapLogger, err := zapCfg.Build() if err != nil { fmt.Fprintln(os.Stderr, err) return } zap.ReplaceGlobals(zapLogger) }) cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose lev cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Debug level log") ‣ ίϚϯυϥΠϯϑϥάͷఆٛ
  40. ©2019 Wantedly, Inc. %FCVHHBCJMJUZ e.g. cobra & zap RunE: func(_

    *cobra.Command, args []string) error { zap.L().Info("loaded config", zap.Any("config", cfg)) return nil }, } // ... var debug, verbose bool cobra.OnInitialize(func() { var zapCfg zap.Config switch { case debug: zapCfg = zap.NewProductionConfig() zapCfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) case verbose: zapCfg = zap.NewDevelopmentConfig() zapCfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) } zapLogger, err := zapCfg.Build() if err != nil { fmt.Fprintln(os.Stderr, err) return } zap.ReplaceGlobals(zapLogger) }) cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose lev cmd.PersistentFlags().BoolVar(&debug, "debug", false, "Debug level log") ‣ ϑϥάΛ΋ͱʹ[BQॳظԽ
  41. ©2019 Wantedly, Inc. %FCVHHBCJMJUZ e.g. cobra & zap cmd :=

    &cobra.Command{ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { // env zap.L().Debug("load env vars") viper.AutomaticEnv() // config file zap.L().Debug("load config files") // ... // flags zap.L().Debug("bind flags") // ... zap.L().Debug("unmarshal config") return viper.Unmarshal(&cfg) }, RunE: func(_ *cobra.Command, args []string) error { zap.L().Info("loaded config", zap.Any("config", cfg)) return nil }, } // ... var debug, verbose bool cobra.OnInitialize(func() { var zapCfg zap.Config switch { ‣ ίʔυதʹద౓ʹϩάΛ࢒͢ ‣ ϑϥάΛ༩͑ͳ͍ʢ[BQΛॳظԽ͠ͳ͍ʣͱ
 OPPQMPHHFSʹॻ͖ࠐ·ΕΔ  Α͏͢ΔʹԿ΋͠ͳ͍  ςετʹ΋Өڹͳ͍ͷͰɺΨϯΨϯ࢒͢΂͠
  42. ©2019 Wantedly, Inc. %FCVHHBCJMJUZ e.g. cobra & zap cmd :=

    &cobra.Command{ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { // env zap.L().Debug("load env vars") viper.AutomaticEnv() // config file zap.L().Debug("load config files") // ... // flags zap.L().Debug("bind flags") // ... zap.L().Debug("unmarshal config") return viper.Unmarshal(&cfg) }, RunE: func(_ *cobra.Command, args []string) error { zap.L().Info("loaded config", zap.Any("config", cfg)) return nil }, } // ... var debug, verbose bool cobra.OnInitialize(func() { var zapCfg zap.Config switch { ‣ ίʔυதʹద౓ʹϩάΛ࢒͢ ‣ ϑϥάΛ༩͑ͳ͍ʢ[BQΛॳظԽ͠ͳ͍ʣͱ
 OPPQMPHHFSʹॻ͖ࠐ·ΕΔ  Α͏͢ΔʹԿ΋͠ͳ͍  ςετʹ΋Өڹͳ͍ͷͰɺΨϯΨϯ࢒͢΂͠ :) % USER=admin PASS=pass go run . --port 8080 -v 2019-05-20T06:24:57.227+0900 INFO awesomeapp/main.go:50 loaded config {"config": {"Port": 8080,"Host":"localhost","Scheme":"http","User":"admin","Pass":"pass"}} :) % USER=admin PASS=pass go run . --port 8080 --debug {"level":"debug","ts":1558301103.629214,"caller":"awesomeapp/main.go:26","msg":"load env vars"} {"level":"debug","ts":1558301103.629287,"caller":"awesomeapp/main.go:30","msg":"load config files"} {"level":"debug","ts":1558301103.63246,"caller":"awesomeapp/main.go:40","msg":"bind flags"} {"level":"debug","ts":1558301103.632509,"caller":"awesomeapp/main.go:46","msg":"unmarshal config"} {"level":"info","ts":1558301103.632673,"caller":"awesomeapp/main.go:50","msg":"loaded config","config":{"Port": 8080,"Host":"localhost","Scheme":"http","User":"admin","Pass":"pass"}}
  43. ©2019 Wantedly, Inc. Testability Section subhead

  44. ©2019 Wantedly, Inc. 5FTUBCJMJUZ Design & Test I/O ӜGNU1SJOUMO זו׾׉ךתתⵃ欽ׅ׷ךכ

    5FTUBCMFהכ鎉ְꨇְ ӜJP8SJUFSזוח⣛㶷ׇׁ׷ Ӝ刿ח䗳銲ח䘔ׄג*0ח㼎׃גJOUFSGBDF׾㐣תׇ׷ Design I/O *0·ΘΓ͸(P$PO4QSJOH8FXBOU"8&40.&$-*UPPMEFWFMPQNFOUʹͯૉ੖Β͍͠঺հ͕ IUUQTTQFBLFSEFDLDPNNJDOODJNXFXBOUBXFTPNFDMJUPPMBOEEFWFMPQNFOU
  45. ©2019 Wantedly, Inc. $-*ͷςετͷπϥϛςετͮ͠Β͍ೖग़ྗ͕ଟ͍ ‣ FHผͷ$-*ͷXSBQQFS ‣ Ҿ਺͕ଟ͍ɾ௕͍ͱ୯७ʹॻ͘ͷ͕ΊΜͲ͍͘͞ ‣ FHίʔυδΣωϨʔλ

    ‣ JNQPSUͷॱ൪΍ϑΥʔϚοτͰফ໣͢Δ ‣ ੜ੒෺͕ଟ͍ͱ*0ΛGBLFʹ͢Δςετ΋େม 5FTUBCJMJUZ
  46. ©2019 Wantedly, Inc. $-*ͷςετͷπϥϛςετͮ͠Β͍ೖग़ྗ͕ଟ͍ ‣ FHผͷ$-*ͷXSBQQFS ‣ Ҿ਺͕ଟ͍ɾ௕͍ͱ୯७ʹॻ͘ͷ͕ΊΜͲ͍͘͞ ‣ FHίʔυδΣωϨʔλ

    ‣ JNQPSUͷॱ൪΍ϑΥʔϚοτͰফ໣͢Δ ‣ ੜ੒෺͕ଟ͍ͱ*0ΛGBLFʹ͢Δςετ΋େม 5FTUBCJMJUZ 'JMFTZTUFN͝ͱGBLFʹͰ͖Ε͹ָͳͷͰ͸ʁ
  47. ©2019 Wantedly, Inc. github.com/spf13/afero "'JMF4ZTUFN"CTUSBDUJPO4ZTUFNGPS(P 5FTUBCJMJUZ type Fs interface {

    Create(name string) (File, error) Mkdir(name string, perm os.FileMode) error MkdirAll(path string, perm os.FileMode) error Open(name string) (File, error) OpenFile(name string, flag int, perm os.FileMode) (File, error) Remove(name string) error RemoveAll(path string) error Rename(oldname, newname string) error Stat(name string) (os.FileInfo, error) Name() string Chmod(name string, mode os.FileMode) error Chtimes(name string, atime time.Time, mtime time.Time) error } func DirExists(fs Fs, path string) (bool, error) func Exists(fs Fs, path string) (bool, error) func Glob(fs Fs, pattern string) (matches []string, err error) func ReadDir(fs Fs, dirname string) ([]os.FileInfo, error) func ReadFile(fs Fs, filename string) ([]byte, error) func TempDir(fs Fs, dir, prefix string) (name string, err error) func Walk(fs Fs, root string, walkFn filepath.WalkFunc) error func WriteFile(fs Fs, filename string, data []byte, perm os.FileMode) error `os` ΍ `io/ioutil ` ͷ ϑΝΠϧೖग़ྗܥʹ͋Γͦ͏ͳϝιουɾؔ਺܈
  48. ©2019 Wantedly, Inc. github.com/spf13/afero "'JMF4ZTUFN"CTUSBDUJPO4ZTUFNGPS(P 5FTUBCJMJUZ type Fs interface {

    Create(name string) (File, error) Mkdir(name string, perm os.FileMode) error MkdirAll(path string, perm os.FileMode) error Open(name string) (File, error) OpenFile(name string, flag int, perm os.FileMode) (File, error) Remove(name string) error RemoveAll(path string) error Rename(oldname, newname string) error Stat(name string) (os.FileInfo, error) Name() string Chmod(name string, mode os.FileMode) error Chtimes(name string, atime time.Time, mtime time.Time) error } func DirExists(fs Fs, path string) (bool, error) func Exists(fs Fs, path string) (bool, error) func Glob(fs Fs, pattern string) (matches []string, err error) func ReadDir(fs Fs, dirname string) ([]os.FileInfo, error) func ReadFile(fs Fs, filename string) ([]byte, error) func TempDir(fs Fs, dir, prefix string) (name string, err error) func Walk(fs Fs, root string, walkFn filepath.WalkFunc) error func WriteFile(fs Fs, filename string, data []byte, perm os.FileMode) error `os` ΍ `io/ioutil ` ͷ ϑΝΠϧೖग़ྗܥʹ͋Γͦ͏ͳϝιουɾؔ਺܈
  49. ©2019 Wantedly, Inc. github.com/spf13/afero "'JMF4ZTUFN"CTUSBDUJPO4ZTUFNGPS(P 5FTUBCJMJUZ func NewBasePathFs(source Fs, path

    string) Fs func NewCacheOnReadFs(base Fs, layer Fs, cacheTime time.Duration) Fs func NewCopyOnWriteFs(base Fs, layer Fs) Fs func NewMemMapFs() Fs func NewOsFs() Fs func NewReadOnlyFs(source Fs) Fs func NewRegexpFs(source Fs, re *regexp.Regexp) Fs ͍Ζ͍Ζͳ `afero.Fs` ͷίϯετϥΫλ
  50. ©2019 Wantedly, Inc. github.com/spf13/afero "'JMF4ZTUFN"CTUSBDUJPO4ZTUFNGPS(P 5FTUBCJMJUZ func NewBasePathFs(source Fs, path

    string) Fs func NewCacheOnReadFs(base Fs, layer Fs, cacheTime time.Duration) Fs func NewCopyOnWriteFs(base Fs, layer Fs) Fs func NewMemMapFs() Fs func NewOsFs() Fs func NewReadOnlyFs(source Fs) Fs func NewRegexpFs(source Fs, re *regexp.Regexp) Fs ͍Ζ͍Ζͳ `afero.Fs` ͷίϯετϥΫλ
  51. ©2019 Wantedly, Inc. github.com/spf13/afero "'JMF4ZTUFN"CTUSBDUJPO4ZTUFNGPS(P 5FTUBCJMJUZ func NewBasePathFs(source Fs, path

    string) Fs func NewCacheOnReadFs(base Fs, layer Fs, cacheTime time.Duration) Fs func NewCopyOnWriteFs(base Fs, layer Fs) Fs func NewMemMapFs() Fs func NewOsFs() Fs func NewReadOnlyFs(source Fs) Fs func NewRegexpFs(source Fs, re *regexp.Regexp) Fs ͍Ζ͍Ζͳ `afero.Fs` ͷίϯετϥΫλ ‣ A/FX0T'T A  ;ͭ͏ʹ04ͷpMFTZTUFNʹॻ͖ࠐΉ࣮૷  ΞϓϦέʔγϣϯίʔυͰ͸͜ΕΛ࢖͏ ‣ A/FX.FN.BQ'T A  ϑΝΠϧʹॻ͖ࠐΉϑϦΛͯ͠ΠϯϝϞϦͰอ࣋͢Δ  *0͕ൃੜ͠ͳ͍ͨΊߴ଎ͰɺฒྻςετͰ΋ίϯϑϦΫτ͠ͳ͍  ςετίʔυͰ࢖͏
  52. ©2019 Wantedly, Inc. $-*ͷςετͷπϥϛςετͮ͠Β͍ೖग़ྗ͕ଟ͍ ‣ FHผͷ$-*ͷXSBQQFS ‣ Ҿ਺͕ଟ͍ɾ௕͍ͱ୯७ʹॻ͘ͷ͕ΊΜͲ͍͘͞ ‣ FHίʔυδΣωϨʔλ

    ‣ JNQPSUͷॱ൪΍ϑΥʔϚοτͰফ໣͢Δ ‣ ੜ੒෺͕ଟ͍ͱ*0ΛGBLFʹ͢Δςετ΋େม 5FTUBCJMJUZ ग़ྗ͕ڊେͩͬͨΓෳࡶͩͬͨΓ͢Δܥ (PMEFOpMFΛखॻ͖͢Δͷ΋େมʜ
  53. ©2019 Wantedly, Inc. Snapshot testing HJUIVCDPNCSBEMFZKLFNQDVQBMPZ 5FTUBCJMJUZ t.Run("path/to/generated_file.go", func(t *testing.T)

    { data, err := ioutil.ReadFile("path/to/generated_file.go") if err != nil { t.Fatalf("failed to read file: %v", err) } cupaloy.SnapshotT(t, string(data)) })
  54. ©2019 Wantedly, Inc. Snapshot testing HJUIVCDPNCSBEMFZKLFNQDVQBMPZ 5FTUBCJMJUZ t.Run("path/to/generated_file.go", func(t *testing.T)

    { data, err := ioutil.ReadFile("path/to/generated_file.go") if err != nil { t.Fatalf("failed to read file: %v", err) } cupaloy.SnapshotT(t, string(data)) })
  55. ©2019 Wantedly, Inc. Snapshot testing HJUIVCDPNCSBEMFZKLFNQDVQBMPZ 5FTUBCJMJUZ t.Run("path/to/generated_file.go", func(t *testing.T)

    { data, err := ioutil.ReadFile("path/to/generated_file.go") if err != nil { t.Fatalf("failed to read file: %v", err) } cupaloy.SnapshotT(t, string(data)) }) ੜ੒͞ΕͨAHFOFSBUFE@pMFHPAΛಡΈग़͠ɺεφοϓγϣοτΛͱΔ  εφοϓγϣοτϑΝΠϧ͕ଘࡏ͠ͳ͍৔߹  ATOBQTIPUTUFTUOBNFAʹ৽͍͠εφοϓγϣοτΛอଘ  εφοϓγϣοτϑΝΠϧ͕ଘࡏ͢Δ৔߹  طଘͷεφοϓγϣοτͱൺֱ͠ɺ
 ҟͳ͍ͬͯΕ͹EJGGΛදࣔ͠ςετΛ'"*-ͤ͞Δ  A61%"5&@4/"14)054A͕ఆٛ͞Ε͍ͯΕ͹ɺ
 εφοϓγϣοτΛߋ৽͢Δ
  56. ©2019 Wantedly, Inc. Snapshot testing HJUIVCDPNCSBEMFZKLFNQDVQBMPZ 5FTUBCJMJUZ t.Run("path/to/generated_file.go", func(t *testing.T)

    { data, err := ioutil.ReadFile("path/to/generated_file.go") if err != nil { t.Fatalf("failed to read file: %v", err) } cupaloy.SnapshotT(t, string(data)) }) ੜ੒͞ΕͨAHFOFSBUFE@pMFHPAΛಡΈग़͠ɺεφοϓγϣοτΛͱΔ  εφοϓγϣοτϑΝΠϧ͕ଘࡏ͠ͳ͍৔߹  ATOBQTIPUTUFTUOBNFAʹ৽͍͠εφοϓγϣοτΛอଘ  εφοϓγϣοτϑΝΠϧ͕ଘࡏ͢Δ৔߹  طଘͷεφοϓγϣοτͱൺֱ͠ɺ
 ҟͳ͍ͬͯΕ͹EJGGΛදࣔ͠ςετΛ'"*-ͤ͞Δ  A61%"5&@4/"14)054A͕ఆٛ͞Ε͍ͯΕ͹ɺ
 εφοϓγϣοτΛߋ৽͢Δ ςετͷॳճ࣮ߦ࣌ͷ݁Ռ͔ΒHPMEFOpMFΛੜ੒ͯ͘͠ΕΔ ճ໨Ҡߦ͸TOBQTIPUͷEJGGΛݟͭͭɺదٓߋ৽͍ͯ͘͠
  57. ©2019 Wantedly, Inc. 5FTUBCJMJUZ fs := afero.NewMemMapFs() // テストでは OS

    のファイルシステムではなくメモリに書き込む err := NewGenerator(fs).Generate("path/to/generated_file.go") if err != nil { t.Fatalf("returned %v, want nil", err) } // ioutil ではなく afero から読み出す data, err := afero.ReadFile(fs, "path/to/generated_file.go") if err != nil { t.Fatalf("failed to read file: %v", err) } cupaloy.SnapshotT(t, string(data)) afero × snapshot testing (cupaloy)
  58. ©2019 Wantedly, Inc. Viper × afero 5FTUBCJMJUZ func SetFs(fs afero.Fs)

    func (v *Viper) SetFs(fs afero.Fs) Viper ͕ࢀর͢ΔϑΝΠϧγεςϜ΋ `afero.Fs` ͳͷͰɺ ͜ΕΛࠩ͠ସ͑ͯઃఆϑΝΠϧಡΈࠐΈͷςετΛ͢Δ͜ͱ΋ग़དྷ·͢
  59. ©2019 Wantedly, Inc. ‣ ໨తɾঢ়گʹ͋ͬͨύοέʔδɾϑϨʔϜϫʔΫબ୒Λ৺͕͚Α͏  ·ΑͬͨΒATQGDPCSBAͰؒҧ͍͸গͳ͍͸ͣ ‣ ઃఆϑΝΠϧɾ؀ڥม਺ͷಡΈࠐΈʹ͸ATQGWJQFSAΛ૊Έ߹ΘͤΔͱศར 

    ઃఆͷ༏ઌॱҐ෇͚΍TUSVDU΁ͷϚοϐϯά·Ͱ΋΍ͬͯ͘ΕΔ ‣ ϏϧυޙͰ΋໰୊ͷ੾Γ෼͚͕Ͱ͖ΔΑ͏ʹɺσόοάΦϓγϣϯΛ͚ͭͯ1SJOU%FCVH͠·͘Ζ͏  Ϣʔβʹͱͬͯ΋໰୊ղܾͷॿ͚ʹͳΔ  ใࠂ΋ͯ͠΋Β͍΍͍͢ ‣ ϑΝΠϧγεςϜ͕བྷΉͱ͖͸ɺATQGBGFSPAΛ࢖ͬͯந৅Խ͢Δ͜ͱΛݕ౼͠Α͏  A PT'JMFA͸΋ͪΖΜͷ͜ͱɺAJP3FBEFSAͱAJP8SJUFSAͷGBLFΛͭ͘ΔΑΓ΋΋͏ҰஈམͰ͖Δ  ίʔυੜ੒౳Λ࣮૷͢Δ༧ఆ͕͋ΔͳΒTOBQTIPUUFTUJOH΋Φεεϝ