Slide 1

Slide 1 text

©2019 Wantedly, Inc. $-*πʔϧ։ൃΛࢧ͑Δٕज़य़ Techniques that support building CLI tools, 2019 Spring golang.tokyo #24 May 20, 2019 - Masayuki izumi

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

©2019 Wantedly, Inc.

Slide 4

Slide 4 text

©2019 Wantedly, Inc.

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

©2019 Wantedly, Inc.

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

©2019 Wantedly, Inc. $-*πʔϧ։ൃΛࢧ͑Δٕज़य़ Techniques that support building CLI tools, 2019 Spring golang.tokyo #24 May 20, 2019 - Masayuki izumi

Slide 9

Slide 9 text

©2019 Wantedly, Inc. $ whoami @izumin5210 Wantedly, Inc. ‣ Wantedly People - Application Engineer (Go, Ruby, Web Frontend) ‣ Interested in… - Microservices - Developer Productivity - Developer Experience

Slide 10

Slide 10 text

©2019 Wantedly, Inc. Talk abst and desc

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

©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*Ͱ͔͍͍ͬ͜

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

©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Λࢀর

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

©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ʣ͸ѹ౗తʹࠔͬͯ ͍Δ͜ͱ͕͋ΓɺͦΕΛര଎Ͱղܾͨͯ͘͠ॻ͍ͨͱ͖ ͷ΋ͷ ن໛ײɾεϐʔυײʹΑͬͯ͸ ͜͏͍͏ίʔυ͔Β͸͡ΊΔͷ΋ѱ͘ͳ͍ͷ͔΋ʜʁ

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

©2019 Wantedly, Inc. Why `pflag` and `cobra`? ·Α͍ͬͯΔͱ͖ͷ͓͢͢Ίબఆ ‣ ADPCSBAͷϑϥάύʔα͸AQqBHAͰ࣮૷͞Ε͍ͯΔ ͋ͱ͔ΒʮαϒίϚϯυ͕΄͍͠ʂʯͱ͍͏ͱ͖ʹADPCSBAಋೖ͕؆୯ ‣ ઃఆϑΝΠϧɾ؀ڥม਺ಡΈग़͠ʹศརͳύοέʔδͰ͋ΔAWJQFSAͱAQqBHAͷ૬ੑ͕ྑ͍ ͋ͱ͔ΒʮઃఆϑΝΠϧ΄͍͠ʂʯͳΜͯ͜ͱ͸Α͋͘Δ AWJQFSAʹ͍ͭͯ͸ޙड़͠·͢ ࠷ॳͷٕज़બఆ HJUIVCDPNTQGDPCSB

Slide 24

Slide 24 text

©2019 Wantedly, Inc. Directory(package) structure ·Α͍ͬͯΔͱ͖ͷ͓͢͢Ίύοέʔδߏ੒ ‣ ADNEBQQOBNFAʹNBJOΛஔ͍͍ͯΔέʔεΛΑ͘ݟΔ FHNPCZNPCZ LVCFSOFUFT QSPNFUIFVT (PPHMF$MPVE1MBUGPSNLBOJLP ‣ ෳ਺όΠφϦΛు͖͍ͨͱ͖ʹରԠͰ͖Δ ‣ ϝΠϯͷΞϓϦέʔγϣϯίʔυ͸ʮQLHҎԼʹஔ͘ʯʮϧʔτʹஔ͘ʯ ͷύλʔϯΛΑ͘ݟΔ #VJMEJOH$-*UPPMT ├── cmd │ └── │ └── main.go ├── go.mod ├── go.sum └── pkg └── └── **/*.go ├── cmd │ └── │ └── main.go ├── go.mod ├── go.sum └── */**.go

Slide 25

Slide 25 text

©2019 Wantedly, Inc. Configuration Section subhead

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

©2019 Wantedly, Inc. github.com/spf13/viper ͳΜͰ΋Ͱ͖ΔઃఆಡΈࠐΈύοέʔδ ‣ ͋ΒΏΔͱ͜Ζ͔ΒઃఆΛूΊͯ͘ΕΔ ίϚϯυϥΠϯϑϥάʢAQqBHA AqBHAʣ ؀ڥม਺ ઃఆϑΝΠϧʢ+40/ 50.- :".- )$- +BWBQSPQFSUJFTʣ ϦϞʔτͷઃఆʢFHFUDE $POTVMʣ σϑΥϧτ஋ ‣ ಡΈऔͬͨઃఆ஋͸TUSVDUʹNBQQJOHͰ͖Δ $POGJHVSBUJPO

Slide 29

Slide 29 text

©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) }

Slide 30

Slide 30 text

©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 ‣ ઃఆ஋ΛಡΈࠐΉߏ଄ମͷఆٛ ‣ ίϚϯυϥΠϯϑϥάͷఆٛ

Slide 31

Slide 31 text

©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͸͏·͘ղऍ͢Δ ϑΝΠϧ໊ʢ֦ுࢠൈ͖ʣͷࢦఆ ‣ ίϚϯυϥΠϯϑϥάͷόΠϯυ

Slide 32

Slide 32 text

©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"}

Slide 33

Slide 33 text

©2019 Wantedly, Inc. Debuggability Section subhead

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

©2019 Wantedly, Inc. ‣ W WFSCPTF */'0MFWFM͘Β͍·ͰͷϩάΛग़͢ ‣ EFCVH %(MFWFM΋;͘Ίɺ͢΂ͯͷϩάΛग़͢ %FCVHHBCJMJUZ -PHMFWFM

Slide 38

Slide 38 text

©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")

Slide 39

Slide 39 text

©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") ‣ ίϚϯυϥΠϯϑϥάͷఆٛ

Slide 40

Slide 40 text

©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ॳظԽ

Slide 41

Slide 41 text

©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ʹॻ͖ࠐ·ΕΔ Α͏͢ΔʹԿ΋͠ͳ͍ ςετʹ΋Өڹͳ͍ͷͰɺΨϯΨϯ࢒͢΂͠

Slide 42

Slide 42 text

©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"}}

Slide 43

Slide 43 text

©2019 Wantedly, Inc. Testability Section subhead

Slide 44

Slide 44 text

©2019 Wantedly, Inc. 5FTUBCJMJUZ Design & Test I/O ӜGNU1SJOUMO זו׾׉ךתתⵃ欽ׅ׷ךכ 5FTUBCMFהכ鎉ְꨇְ ӜJP8SJUFSזוח⣛㶷ׇׁ׷ Ӝ刿ח䗳銲ח䘔ׄג*0ח㼎׃גJOUFSGBDF׾㐣תׇ׷ Design I/O *0·ΘΓ͸(P$PO4QSJOH8FXBOU"8&40.&$-*UPPMEFWFMPQNFOUʹͯૉ੖Β͍͠঺հ͕ IUUQTTQFBLFSEFDLDPNNJDOODJNXFXBOUBXFTPNFDMJUPPMBOEEFWFMPQNFOU

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

©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 ` ͷ ϑΝΠϧೖग़ྗܥʹ͋Γͦ͏ͳϝιουɾؔ਺܈

Slide 48

Slide 48 text

©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 ` ͷ ϑΝΠϧೖग़ྗܥʹ͋Γͦ͏ͳϝιουɾؔ਺܈

Slide 49

Slide 49 text

©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` ͷίϯετϥΫλ

Slide 50

Slide 50 text

©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` ͷίϯετϥΫλ

Slide 51

Slide 51 text

©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͕ൃੜ͠ͳ͍ͨΊߴ଎ͰɺฒྻςετͰ΋ίϯϑϦΫτ͠ͳ͍ ςετίʔυͰ࢖͏

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

©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)) })

Slide 54

Slide 54 text

©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)) })

Slide 55

Slide 55 text

©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͕ఆٛ͞Ε͍ͯΕ͹ɺ
 εφοϓγϣοτΛߋ৽͢Δ

Slide 56

Slide 56 text

©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Λݟͭͭɺదٓߋ৽͍ͯ͘͠

Slide 57

Slide 57 text

©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)

Slide 58

Slide 58 text

©2019 Wantedly, Inc. Viper × afero 5FTUBCJMJUZ func SetFs(fs afero.Fs) func (v *Viper) SetFs(fs afero.Fs) Viper ͕ࢀর͢ΔϑΝΠϧγεςϜ΋ `afero.Fs` ͳͷͰɺ ͜ΕΛࠩ͠ସ͑ͯઃఆϑΝΠϧಡΈࠐΈͷςετΛ͢Δ͜ͱ΋ग़དྷ·͢

Slide 59

Slide 59 text

©2019 Wantedly, Inc. ‣ ໨తɾঢ়گʹ͋ͬͨύοέʔδɾϑϨʔϜϫʔΫબ୒Λ৺͕͚Α͏ ·ΑͬͨΒATQGDPCSBAͰؒҧ͍͸গͳ͍͸ͣ ‣ ઃఆϑΝΠϧɾ؀ڥม਺ͷಡΈࠐΈʹ͸ATQGWJQFSAΛ૊Έ߹ΘͤΔͱศར ઃఆͷ༏ઌॱҐ෇͚΍TUSVDU΁ͷϚοϐϯά·Ͱ΋΍ͬͯ͘ΕΔ ‣ ϏϧυޙͰ΋໰୊ͷ੾Γ෼͚͕Ͱ͖ΔΑ͏ʹɺσόοάΦϓγϣϯΛ͚ͭͯ1SJOU%FCVH͠·͘Ζ͏ Ϣʔβʹͱͬͯ΋໰୊ղܾͷॿ͚ʹͳΔ ใࠂ΋ͯ͠΋Β͍΍͍͢ ‣ ϑΝΠϧγεςϜ͕བྷΉͱ͖͸ɺATQGBGFSPAΛ࢖ͬͯந৅Խ͢Δ͜ͱΛݕ౼͠Α͏ APT'JMFA͸΋ͪΖΜͷ͜ͱɺAJP3FBEFSAͱAJP8SJUFSAͷGBLFΛͭ͘ΔΑΓ΋΋͏ҰஈམͰ͖Δ ίʔυੜ੒౳Λ࣮૷͢Δ༧ఆ͕͋ΔͳΒTOBQTIPUUFTUJOH΋Φεεϝ