Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up
for free
CLI ツール開発を支える技術 2019春 / Techniques that support building CLI tools, 2019 Spring
Masayuki Izumi
May 20, 2019
Programming
8
2.6k
CLI ツール開発を支える技術 2019春 / Techniques that support building CLI tools, 2019 Spring
talked on
https://golangtokyo.connpass.com/event/129067/
Masayuki Izumi
May 20, 2019
Tweet
Share
More Decks by Masayuki Izumi
See All by Masayuki Izumi
izumin5210
7
3.6k
izumin5210
1
74
izumin5210
11
5.6k
izumin5210
5
1.1k
izumin5210
7
5.9k
izumin5210
2
980
izumin5210
4
5.5k
izumin5210
4
1.2k
izumin5210
0
2.8k
Other Decks in Programming
See All in Programming
viteinfinite
0
210
zsmb
1
130
rishitdagli
0
180
mu2in
0
150
hirotokirimaru
1
440
masayaaoyama
4
550
kazaman97
0
200
dictoss
0
170
maito1201
0
220
yaamaa
0
450
akatsukinewgrad
0
210
yanagii
0
190
Featured
See All Featured
cromwellryan
101
5.9k
danielanewman
200
20k
malarkey
393
60k
smashingmag
229
18k
andyhume
62
3.5k
rasmusluckow
318
18k
aarron
258
36k
holman
461
280k
jcasabona
7
520
holman
448
130k
pauljervisheath
196
15k
samlambert
237
9.9k
Transcript
©2019 Wantedly, Inc. $-*πʔϧ։ൃΛࢧ͑Δٕज़य़ Techniques that support building CLI tools,
2019 Spring golang.tokyo #24 May 20, 2019 - Masayuki izumi
ϖʔδλΠτϧ ϖʔδαϒλΠτϧ ©2019 Wantedly, Inc. (P$PO4QSJOH͓͔ͭΕ͞·Ͱͨ͠ ӡӦͷΈͳ͞ΜɺొஃऀͷΈͳ͞ΜɺࢀՃ͞ΕͨΈͳ͞Μ#
©2019 Wantedly, Inc.
©2019 Wantedly, Inc.
ϖʔδλΠτϧ ϖʔδαϒλΠτϧ ©2019 Wantedly, Inc. ͓Θ͔Γ͍͚ͨͩͨͩΖ͏͔
©2019 Wantedly, Inc.
ϖʔδλΠτϧ ϖʔδαϒλΠτϧ ©2019 Wantedly, Inc. ͔ͿΒͳ͍Α͏ͳΛ͠·͢
©2019 Wantedly, Inc. $-*πʔϧ։ൃΛࢧ͑Δٕज़य़ Techniques that support building CLI tools,
2019 Spring golang.tokyo #24 May 20, 2019 - Masayuki izumi
©2019 Wantedly, Inc. $ whoami @izumin5210 Wantedly, Inc. ‣ Wantedly
People - Application Engineer (Go, Ruby, Web Frontend) ‣ Interested in… - Microservices - Developer Productivity - Developer Experience
©2019 Wantedly, Inc. Talk abst and desc
©2019 Wantedly, Inc. Building CLI tools 101 Section subhead
©2019 Wantedly, Inc. (PͰ$-*ͱ͍͑ʜ Α͘ฉ͘ศརύοέʔδ܈ ‣ ͪΐͬͱศརͳϑϥάύʔα AqBHA TUBOEBSEMJC
JNQPSUFECZQLHT ATQGQqBHA TUBST JNQPSUFECZQLHT AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT #VJMEJOH$-*UPPMT ࣈͯ࣌͢ͷͷ TUBSHJUIVCDPNɺඃJNQPSUHPEPDPSHΛࢀর
©2019 Wantedly, Inc. (PͰ$-*ͱ͍͑ʜ Α͘ฉ͘ศརύοέʔδ܈ ‣ ͪΐͬͱศརͳϑϥάύʔα AqBHA TUBOEBSEMJC
JNQPSUFECZQLHT ATQGQqBHA TUBST JNQPSUFECZQLHT AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT #VJMEJOH$-*UPPMT ࣈͯ࣌͢ͷͷ TUBSHJUIVCDPNɺඃJNQPSUHPEPDPSHΛࢀর AqBHA͕-POHPQUJPOະରԠͳͷ͕ಋೖͷେ͖ͳϞνϕʔγϣϯ AQqBHAAqBHAͷ*'Λ౿ऻͭͭ͠104*9(/6TUZMFରԠ AHPqBHTATUSVDUUBHΛΈͯ͏·͘Δ ALJOHQJOAqVFOUTUZMFͳ"1*Ͱ͔͍͍ͬ͜
©2019 Wantedly, Inc. (PͰ$-*ͱ͍͑ʜ Α͘ฉ͘ศརύοέʔδ܈ ‣ ͪΐͬͱศརͳϑϥάύʔα AqBHA TUBOEBSEMJC
JNQPSUFECZQLHT ATQGQqBHA TUBST JNQPSUFECZQLHT AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT #VJMEJOH$-*UPPMT ࣈͯ࣌͢ͷͷ TUBSHJUIVCDPNɺඃJNQPSUHPEPDPSHΛࢀর
©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 ࣈͯ࣌͢ͷͷ TUBSHJUIVCDPNɺඃJNQPSUHPEPDPSHΛࢀর
©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ نײɾػೳཁ͔݅ΒΞϓϩʔνΛܾΊΔ ‣ ͭ͘Γ͍ͨπʔϧͲΕ͘Β͍ͷنʹͳΔ͔ ୯ػೳɾ͍͔ͭ͘ͷϑϥάͱҾ͕͋Δ͚ͩ
FHADEA AMTA AJ[VNJOHFYA ͍͔ͭ͘αϒίϚϯυʢαϒαϒίϚϯυ ʜʣΛͭ FHAEPDLFSA ALVCFDUMA AJ[VNJOHSBQJA AJ[VNJOTVCFFA #VJMEJOH$-*UPPMT
©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ نײɾػೳཁ͔݅ΒΞϓϩʔνΛܾΊΔ ‣ ͭ͘Γ͍ͨπʔϧͲΕ͘Β͍ͷنʹͳΔ͔ ୯ػೳɾ͍͔ͭ͘ͷϑϥάͱҾ͕͋Δ͚ͩ
FHADEA AMTA AJ[VNJOHFYA ͍͔ͭ͘αϒίϚϯυʢαϒαϒίϚϯυ ʜʣΛͭ FHAEPDLFSA ALVCFDUMA AJ[VNJOHSBQJA AJ[VNJOTVCFFA ‣ ͲΕ͘Β͍ͷػೳੑ͕ඞཁ͔ ϑϥά ɾڥมɾઃఆϑΝΠϧʜͲ͜·Ͱඞཁʁ #VJMEJOH$-*UPPMT
©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ʣѹతʹࠔͬͯ ͍Δ͜ͱ͕͋ΓɺͦΕΛരͰղܾͨͯ͘͠ॻ͍ͨͱ͖ ͷͷ نײɾεϐʔυײʹΑͬͯ ͜͏͍͏ίʔυ͔Β͡ΊΔͷѱ͘ͳ͍ͷ͔ʜʁ
©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ·Α͍ͬͯΔͱ͖ͷબఆϑϩʔ ‣ ͘ղܾ͍͕ͨ͋͠ΔʂͳΜͰ͍͍͔Βૣ͘࡞Γ͍ͨʂ Ұ୴AqBHA͔AQqBHAͰ͓Λͭͭ͠രͰͭͬͯ͘ɺ͋ͱ͔Βߟ͑·͠ΐ͏
αϒίϚϯυ͕ඞཁͳ߹ADPCSBAΛ͍·͠ΐ͏ #VJMEJOH$-*UPPMT
©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ·Α͍ͬͯΔͱ͖ͷબఆϑϩʔ ‣ ͘ղܾ͍͕ͨ͋͠ΔʂͳΜͰ͍͍͔Βૣ͘࡞Γ͍ͨʂ Ұ୴AqBHA͔AQqBHAͰ͓Λͭͭ͠രͰͭͬͯ͘ɺ͋ͱ͔Βߟ͑·͠ΐ͏
αϒίϚϯυ͕ඞཁͳ߹ADPCSBAΛ͍·͠ΐ͏ ‣ αϒίϚϯυ͍·ͷͱ͜Ζ͏༧ఆͳ͍ AQqBHA͕͓͢͢ΊͰ͢ #VJMEJOH$-*UPPMT
©2019 Wantedly, Inc. ࠷ॳͷٕज़બఆ ·Α͍ͬͯΔͱ͖ͷબఆϑϩʔ ‣ ͘ղܾ͍͕ͨ͋͠ΔʂͳΜͰ͍͍͔Βૣ͘࡞Γ͍ͨʂ Ұ୴AqBHA͔AQqBHAͰ͓Λͭͭ͠രͰͭͬͯ͘ɺ͋ͱ͔Βߟ͑·͠ΐ͏
αϒίϚϯυ͕ඞཁͳ߹ADPCSBAΛ͍·͠ΐ͏ ‣ αϒίϚϯυ͍·ͷͱ͜Ζ͏༧ఆͳ͍ AQqBHA͕͓͢͢ΊͰ͢ ‣ ڥมɾઃఆϑΝΠϧΛ͏༧ఆ͕͋Δ ADPCSBA͕͓͢͢ΊͰ͢ #VJMEJOH$-*UPPMT
©2019 Wantedly, Inc. Why `pflag` and `cobra`? ·Α͍ͬͯΔͱ͖ͷ͓͢͢Ίબఆ ‣ ADPCSBAͷϑϥάύʔαAQqBHAͰ࣮͞Ε͍ͯΔ
͋ͱ͔ΒʮαϒίϚϯυ͕΄͍͠ʂʯͱ͍͏ͱ͖ʹADPCSBAಋೖ͕؆୯ ࠷ॳͷٕज़બఆ HJUIVCDPNTQGDPCSB
©2019 Wantedly, Inc. Why `pflag` and `cobra`? ·Α͍ͬͯΔͱ͖ͷ͓͢͢Ίબఆ ‣ ADPCSBAͷϑϥάύʔαAQqBHAͰ࣮͞Ε͍ͯΔ
͋ͱ͔ΒʮαϒίϚϯυ͕΄͍͠ʂʯͱ͍͏ͱ͖ʹADPCSBAಋೖ͕؆୯ ‣ ઃఆϑΝΠϧɾڥมಡΈग़͠ʹศརͳύοέʔδͰ͋ΔAWJQFSAͱAQqBHAͷ૬ੑ͕ྑ͍ ͋ͱ͔ΒʮઃఆϑΝΠϧ΄͍͠ʂʯͳΜͯ͜ͱΑ͋͘Δ AWJQFSAʹ͍ͭͯޙड़͠·͢ ࠷ॳͷٕज़બఆ HJUIVCDPNTQGDPCSB
©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
©2019 Wantedly, Inc. Configuration Section subhead
©2019 Wantedly, Inc. ‣ ϢʔβʹແବͳೖྗΛͤͨ͘͞ͳ͍ ‣ ҰํͰɺσϑΥϧτͰશͯͷχʔζʹ͑ΔͷϜϦ $POGJHVSBUJPO
©2019 Wantedly, Inc. Ͳ͔͜ΒɾͲ͏͍͏༏ઌॱҐͰઃఆΛಡΈࠐΉʁ ઃఆϑΝΠϧʢ)0.&ʣ ઃఆϑΝΠϧʢ18%ʣ ڥม ࣮ߦ࣌Φϓγϣϯ $POGJHVSBUJPO
©2019 Wantedly, Inc. github.com/spf13/viper ͳΜͰͰ͖ΔઃఆಡΈࠐΈύοέʔδ ‣ ͋ΒΏΔͱ͜Ζ͔ΒઃఆΛूΊͯ͘ΕΔ ίϚϯυϥΠϯϑϥάʢAQqBHA AqBHAʣ
ڥม ઃఆϑΝΠϧʢ+40/ 50.- :".- )$- +BWBQSPQFSUJFTʣ ϦϞʔτͷઃఆʢFHFUDE $POTVMʣ σϑΥϧτ ‣ ಡΈऔͬͨઃఆTUSVDUʹNBQQJOHͰ͖Δ $POGJHVSBUJPO
©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) }
©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 ‣ ઃఆΛಡΈࠐΉߏମͷఆٛ ‣ ίϚϯυϥΠϯϑϥάͷఆٛ
©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͏·͘ղऍ͢Δ ϑΝΠϧ໊ʢ֦ுࢠൈ͖ʣͷࢦఆ ‣ ίϚϯυϥΠϯϑϥάͷόΠϯυ
©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"}
©2019 Wantedly, Inc. Debuggability Section subhead
©2019 Wantedly, Inc. ‣ ϏϧυࡁΈόΠφϦͰόά౿ΜͩΒͲ͏͢Δʁ ‣ %FCVHHFS1SJOU%FCVHͰ͖ͳ͍ͱΓ͚େม %FCVHHBCJMJUZ
©2019 Wantedly, Inc. ‣ ϏϧυࡁΈόΠφϦͰόά౿ΜͩΒͲ͏͢Δʁ ‣ %FCVHHFS1SJOU%FCVHͰ͖ͳ͍ͱΓ͚େม %FCVHHBCJMJUZ
©2019 Wantedly, Inc. -v (--verbose) / --debug σόοάΦϓγϣϯΛͭͬͯ͘ɺ։ൃதʹ͔ͭͬͨ1SJOU%FCVHͦͷ··͢ $POGJHVSBUJPO
©2019 Wantedly, Inc. ‣ W WFSCPTF */'0MFWFM͘Β͍·ͰͷϩάΛग़͢ ‣
EFCVH %(MFWFM;͘Ίɺͯ͢ͷϩάΛग़͢ %FCVHHBCJMJUZ -PHMFWFM
©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")
©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") ‣ ίϚϯυϥΠϯϑϥάͷఆٛ
©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ॳظԽ
©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ʹॻ͖ࠐ·ΕΔ Α͏͢ΔʹԿ͠ͳ͍ ςετʹӨڹͳ͍ͷͰɺΨϯΨϯ͢͠
©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"}}
©2019 Wantedly, Inc. Testability Section subhead
©2019 Wantedly, Inc. 5FTUBCJMJUZ Design & Test I/O ӜGNU1SJOUMO זוךתתⵃ欽ׅךכ
5FTUBCMFהכ鎉ְꨇְ ӜJP8SJUFSזוח⣛㶷ׇׁ Ӝ刿ח䗳銲ח䘔ׄג*0ח㼎׃גJOUFSGBDF㐣תׇ Design I/O *0·ΘΓ(P$PO4QSJOH8FXBOU"8&40.&$-*UPPMEFWFMPQNFOUʹͯૉΒ͍͠հ͕ IUUQTTQFBLFSEFDLDPNNJDOODJNXFXBOUBXFTPNFDMJUPPMBOEEFWFMPQNFOU
©2019 Wantedly, Inc. $-*ͷςετͷπϥϛςετͮ͠Β͍ೖग़ྗ͕ଟ͍ ‣ FHผͷ$-*ͷXSBQQFS ‣ Ҿ͕ଟ͍ɾ͍ͱ୯७ʹॻ͘ͷ͕ΊΜͲ͍͘͞ ‣ FHίʔυδΣωϨʔλ
‣ JNQPSUͷॱ൪ϑΥʔϚοτͰফ͢Δ ‣ ੜ͕ଟ͍ͱ*0ΛGBLFʹ͢Δςετେม 5FTUBCJMJUZ
©2019 Wantedly, Inc. $-*ͷςετͷπϥϛςετͮ͠Β͍ೖग़ྗ͕ଟ͍ ‣ FHผͷ$-*ͷXSBQQFS ‣ Ҿ͕ଟ͍ɾ͍ͱ୯७ʹॻ͘ͷ͕ΊΜͲ͍͘͞ ‣ FHίʔυδΣωϨʔλ
‣ JNQPSUͷॱ൪ϑΥʔϚοτͰফ͢Δ ‣ ੜ͕ଟ͍ͱ*0ΛGBLFʹ͢Δςετେม 5FTUBCJMJUZ 'JMFTZTUFN͝ͱGBLFʹͰ͖ΕָͳͷͰʁ
©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 ` ͷ ϑΝΠϧೖग़ྗܥʹ͋Γͦ͏ͳϝιουɾؔ܈
©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 ` ͷ ϑΝΠϧೖग़ྗܥʹ͋Γͦ͏ͳϝιουɾؔ܈
©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` ͷίϯετϥΫλ
©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` ͷίϯετϥΫλ
©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͕ൃੜ͠ͳ͍ͨΊߴͰɺฒྻςετͰίϯϑϦΫτ͠ͳ͍ ςετίʔυͰ͏
©2019 Wantedly, Inc. $-*ͷςετͷπϥϛςετͮ͠Β͍ೖग़ྗ͕ଟ͍ ‣ FHผͷ$-*ͷXSBQQFS ‣ Ҿ͕ଟ͍ɾ͍ͱ୯७ʹॻ͘ͷ͕ΊΜͲ͍͘͞ ‣ FHίʔυδΣωϨʔλ
‣ JNQPSUͷॱ൪ϑΥʔϚοτͰফ͢Δ ‣ ੜ͕ଟ͍ͱ*0ΛGBLFʹ͢Δςετେม 5FTUBCJMJUZ ग़ྗ͕ڊେͩͬͨΓෳࡶͩͬͨΓ͢Δܥ (PMEFOpMFΛखॻ͖͢Δͷେมʜ
©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)) })
©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)) })
©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͕ఆٛ͞Ε͍ͯΕɺ εφοϓγϣοτΛߋ৽͢Δ
©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Λݟͭͭɺదٓߋ৽͍ͯ͘͠
©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)
©2019 Wantedly, Inc. Viper × afero 5FTUBCJMJUZ func SetFs(fs afero.Fs)
func (v *Viper) SetFs(fs afero.Fs) Viper ͕ࢀর͢ΔϑΝΠϧγεςϜ `afero.Fs` ͳͷͰɺ ͜ΕΛࠩ͠ସ͑ͯઃఆϑΝΠϧಡΈࠐΈͷςετΛ͢Δ͜ͱग़དྷ·͢
©2019 Wantedly, Inc. ‣ తɾঢ়گʹ͋ͬͨύοέʔδɾϑϨʔϜϫʔΫબΛ৺͕͚Α͏ ·ΑͬͨΒATQGDPCSBAͰؒҧ͍গͳ͍ͣ ‣ ઃఆϑΝΠϧɾڥมͷಡΈࠐΈʹATQGWJQFSAΛΈ߹ΘͤΔͱศར
ઃఆͷ༏ઌॱҐ͚TUSVDUͷϚοϐϯά·Ͱͬͯ͘ΕΔ ‣ ϏϧυޙͰͷΓ͚͕Ͱ͖ΔΑ͏ʹɺσόοάΦϓγϣϯΛ͚ͭͯ1SJOU%FCVH͠·͘Ζ͏ Ϣʔβʹͱͬͯղܾͷॿ͚ʹͳΔ ใࠂͯ͠Β͍͍͢ ‣ ϑΝΠϧγεςϜ͕བྷΉͱ͖ɺATQGBGFSPAΛͬͯநԽ͢Δ͜ͱΛݕ౼͠Α͏ A PT'JMFAͪΖΜͷ͜ͱɺAJP3FBEFSAͱAJP8SJUFSAͷGBLFΛͭ͘ΔΑΓ͏ҰஈམͰ͖Δ ίʔυੜΛ࣮͢Δ༧ఆ͕͋ΔͳΒTOBQTIPUUFTUJOHΦεεϝ