Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Masayuki Izumi

May 20, 2019
Tweet

More Decks by Masayuki Izumi

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

  3. ©2019 Wantedly, Inc.

    View full-size slide

  4. ©2019 Wantedly, Inc.

    View full-size slide

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

    View full-size slide

  6. ©2019 Wantedly, Inc.

    View full-size slide

  7. ϖʔδλΠτϧ ϖʔδαϒλΠτϧ
    ©2019 Wantedly, Inc.

    ͔ͿΒͳ͍Α͏ͳ࿩Λ͠·͢


    View full-size slide

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

    View full-size slide

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

    View full-size slide

  10. ©2019 Wantedly, Inc.
    Talk abst and desc

    View full-size slide

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

    View full-size slide

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

    ATQGQqBHA TUBST JNQPSUFECZQLHT

    AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT

    ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT

    #VJMEJOH$-*UPPMT
    ਺ࣈ͸͢΂ͯ࣌఺ͷ΋ͷ
    TUBS਺͸HJUIVCDPNɺඃJNQPSU਺͸HPEPDPSHΛࢀর

    View full-size slide

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

    View full-size slide

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

    ATQGQqBHA TUBST JNQPSUFECZQLHT

    AKFTTFWELHPqBHTA TUBST JNQPSUFECZQLHT

    ABMFDUIPNBTLJOHQJOA TUBST JNQPSUFECZQLHT

    #VJMEJOH$-*UPPMT
    ਺ࣈ͸͢΂ͯ࣌఺ͷ΋ͷ
    TUBS਺͸HJUIVCDPNɺඃJNQPSU਺͸HPEPDPSHΛࢀর

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  24. ©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

    View full-size slide

  25. ©2019 Wantedly, Inc.
    Configuration
    Section subhead

    View full-size slide

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

    View full-size slide

  27. ©2019 Wantedly, Inc.
    Ͳ͔͜ΒɾͲ͏͍͏༏ઌॱҐͰઃఆΛಡΈࠐΉʁ

    ઃఆϑΝΠϧʢ)0.&ʣ ઃఆϑΝΠϧʢ18%ʣ ؀ڥม਺ ࣮ߦ࣌Φϓγϣϯ
    $POGJHVSBUJPO

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. ©2019 Wantedly, Inc.
    Debuggability
    Section subhead

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. ©2019 Wantedly, Inc.
    -v (--verbose) / --debug

    σόοάΦϓγϣϯΛͭͬͯ͘ɺ։ൃதʹ͔ͭͬͨ1SJOU%FCVH͸ͦͷ··࢒͢
    $POGJHVSBUJPO

    View full-size slide

  37. ©2019 Wantedly, Inc.
    ‣ W WFSCPTF

    */'0MFWFM͘Β͍·ͰͷϩάΛग़͢
    ‣ EFCVH
    %(MFWFM΋;͘Ίɺ͢΂ͯͷϩάΛग़͢
    %FCVHHBCJMJUZ -PHMFWFM

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  43. ©2019 Wantedly, Inc.
    Testability
    Section subhead

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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͕ఆٛ͞Ε͍ͯΕ͹ɺ

    εφοϓγϣοτΛߋ৽͢Δ

    View full-size slide

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

    View full-size slide

  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)

    View full-size slide

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

    View full-size slide

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

    View full-size slide