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.9k
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
みんなで育てる GraphQL スキーマ, それを支える Protobuf / GraphQL and Protobuf #tech_stand
izumin5210
5
1.8k
GraphQL 導入の反省と再挑戦 / jsconf jp 2021
izumin5210
10
5.5k
HTTP クライアントを作ろうとして学ぶ、使いやすいインタフェース / #GoCon_Sendai 2020
izumin5210
7
4.3k
個人の・組織の Go 筋を強化する / Gophers Code Reading Party
izumin5210
1
180
今あらためて読み直したい Go 基礎知識 その2 / golang.tokyo #25
izumin5210
10
7k
`cloud.google.com/go/pubsub` internal
izumin5210
5
1.3k
Case studies of designing developer friendly libraries #gocon
izumin5210
7
6.7k
How to manage tool dependencies in Go
izumin5210
2
1.3k
Consider pluggable CLI tool implementation #gocon
izumin5210
4
6.1k
Other Decks in Programming
See All in Programming
Android入門
hn410
0
310
Modern Web Apps with Spring Boot, Angular & TypeScript
toedter
12
14k
Enterprise Angular: Frontend Moduliths with Nx and Standalone Components @jax2022
manfredsteyer
PRO
0
310
実録mruby組み込み体験
coe401_
0
110
書籍『良いコード/悪いコードで学ぶ設計入門』でエンジニアリングの当たり前を変える
minodriven
3
1.1k
偏見と妄想で語るスクリプト言語としての Swift / Swift as a Scripting Language
lovee
2
270
競プロへの誘 -いざな-
u76ner
0
380
読みやすいコードを書こう
yutorin
0
430
バンドル最適化マニアクス at tfconf
mizchi
4
2.3k
近況PHP / PHP in now a days
uzulla
4
1.8k
Composing an API with Kotlin (Kotlin Dev Day 2022)
zsmb
0
280
tfcon2022_Web3Dひとめぐり.pdf
emadurandal
0
1k
Featured
See All Featured
Bash Introduction
62gerente
596
210k
KATA
mclloyd
7
8.6k
Clear Off the Table
cherdarchuk
79
280k
Writing Fast Ruby
sferik
612
57k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
212
20k
The Power of CSS Pseudo Elements
geoffreycrofte
46
3.9k
Thoughts on Productivity
jonyablonski
43
2.2k
Build your cross-platform service in a week with App Engine
jlugia
219
17k
StorybookのUI Testing Handbookを読んだ
zakiyama
4
2k
GraphQLの誤解/rethinking-graphql
sonatard
24
6.2k
A Tale of Four Properties
chriscoyier
149
20k
The Illustrated Children's Guide to Kubernetes
chrisshort
14
35k
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Φεεϝ