- 投稿日:2020-01-20T23:51:06+09:00
1時間でできるMP3音楽を再生するCLIツールを作成する
はじめに
Go言語によって1時間でMP3を再生するCLIツールを作ります!
hello worldしたから次、何か成果物を作りたい!
CLIツールの作り方を知りたい!
みたいな人にオススメです!完成品
ターミナルで自作したコマンドを実行して、音楽を流しましょう!!
作成!!
STEP:1 簡単にmp3の音楽を流してみる
今回,mp3の音声を流すために使うライブラリはこちらです!
main.gopackage main import ( "github.com/faiface/beep" "github.com/faiface/beep/mp3" "github.com/faiface/beep/speaker" "os" "time" ) func main(){ filepath := "~/Desktop/test.mp3" Sound(filepath) } func Sound(path string) error { f, _ := os.Open(path) s, format, err := mp3.Decode(f) if err != nil { return err } err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) if err != nil { return err } done := make(chan struct{}) speaker.Play(beep.Seq(s, beep.Callback(func() { close(done) }))) <-done return nil }コードの解説
(1) mp3ファイルを読み込む
f, _ := os.Open(path) s, format, err := mp3.Decode(f)ここで、ファイルパス(path)から、mp3を読み込む処理を入れています。
mp3ファイルがあるpathを指定してください!(2)ファイルを再生する
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) done := make(chan struct{}) speaker.Play(beep.Seq(s, beep.Callback(func() { close(done) }))) <-doneInit()で、音声を再生する速度を設定しています。こちらは通常再生の設定です。
Play()は再生する設定をしています。beep.Callback()は再生が終了する時に呼ばれる関数です。再生が終了した時に、doneチャネルに値を送信します。
<-done この部分はチャネルから値が入るまでブロックします。なので、音楽が流れている間は、この部分で止まっています。終了して値が入ってくると、次の処理に移ります。
チャネルはgo特有の仕組みです。とりあえず、再生が終了するまで、ここでブロックされていると思ってください(3) 実行する
$ go run main.go音楽がなりますね!簡単にmp3を流すことができます。
Step2:cliツールを作る
cliツールを簡単に作れるパッケージはこちらです。
このパッケージの使い方を簡単にご紹介します。
example(コード)
examplepackage main import ( "fmt" "os" "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Name = "test" app.Usage = "play music" app.Version = "1.0.0" app.Commands = []cli.Command{ { Name: "play", Usage: "play music", Action: play, }, } app.Run(os.Args) } func play(c *cli.Context) { for _, v := range c.Args() { fmt.Println(v) } }example解説
(1) ツールの初期化、設定出力
app := cli.NewApp() app.Name = "test" app.Usage = "play music" app.Version = "1.0.0" app.Commands = []cli.Command{ { Name: "play", Usage: "play music", Action: play, }, }
- Name:アプリの名前
- Usage:アプリの使い方
- Version:cliのバージョン
- Commands:コマンドを登録する です!
自由に設定できます。設定した情報は、コマンドツールでよくある、-h オプションで表示できます。
試しに
$go run cli.go -h
と打ってみましょうNAME: test - play music USAGE: cli [global options] command [command options] [arguments...] VERSION: 1.0.0 COMMANDS: play play music help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help --version, -v print the version簡単にクオリティの高いhelpが作れます!
(2)アクションを追加
次に
cli.Commandスライスには、アクション(command)を登録できます
- Name:コマンド名
- Usage:コマンドの使い方
- Action:コマンドの実際の動作を設定
そして、このアクションに、
func (c *cli.Context){}という関数を設定します。
試しにコマンドの後ろに繋げた文字を表示するアクションを作ってみます。func play(c *cli.Context) { for _, v := range c.Args() { fmt.Println(v) } }(3) 実行してみる
$go run cli.go play test1 test2 test3 test1 test2 test3しっかり引数が表示されてますね
次がこれらの複合です!
Step3: Step1,2を組み合わせてMp3を実行するCLIツールを実装する
ディレクトリ構成
. ├── mcli │ ├── action.go │ └── cli.go ├── cmd │ └── music-cli.go ├── go.mod ├── go.sum └── sound.go補足情報
今回、mainパッケージをcmd/に追加しています。これによって、コマンドを利用するときは、
$ go get github.com/ryomak/music-cli/cmd
でインポートできます。CLIツールではなく音楽を再生するパッケージだけを使いたいときは
$ go get github.com/ryomak/music-cli /* コード上では import( sound "github.com/ryomak/music-cli" ) */で利用できます。
自分で作成した音楽を再生するパッケージ(sound.go)を他のプロジェクトでも使う時に、見やすくなります。1:mcliパッケージ
mcliパッケージは、musicを再生するcliパッケージです!
cliの役割を記述しますmcli/cli.gopackage mcli import ( "github.com/urfave/cli" ) func New(name, usage, version string) *cli.App { app := cli.NewApp() app.Name = name app.Usage = usage app.Version = version app.Commands = getCommands() return app } func getCommands() []cli.Command { return []cli.Command{ { Name: "play", Usage: "play music", Action: play, }, } }次にcliのアクションを記述したaction.go です!
mcli/action.gopackage mcli import ( "fmt" "path/filepath" "github.com/ryomak/musicbox/sound" "github.com/urfave/cli" ) func play(c *cli.Context) { for _, v := range c.Args() { _, filename := filepath.Split(v) fmt.Printf("start %s... \n", filename) err := sound.Sound(v) if err != nil { fmt.Printf("cannot start skipping %s \n", filename) } } }補足情報
actionとcli設定で分けた理由としては、これからコマンドが増える時にactionのみの設定を記述するファイルに分けておくと、後でで改修がしやすいからです。
また、filepath.Splitはとても便利で、入力されたファイルパスを
ディレクトリとファイル名に分けてくれます。
~/Desktop/test.go => (~/Desktop) と (test.go)2:Mainパッケージ
cli/main.gopackage main import ( "os" "github.com/ryomak/music-cli/mcli" ) func main() { app := mcli.New("music-cli", "BGM", "1.0.0") app.Run(os.Args) }先ほど作った、mcliパッケージを読み込んで実行しています。
3:soundパッケージ
sound.gopackage sound import ( "github.com/faiface/beep" "github.com/faiface/beep/mp3" "github.com/faiface/beep/speaker" "os" "time" ) func Sound(path string) error { f, _ := os.Open(path) s, format, err := mp3.Decode(f) if err != nil { return err } done := make(chan struct{}) err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) if err != nil { return err } speaker.Play(beep.Seq(s, beep.Callback(func() { close(done) }))) <-done return nil }実行
$ go run cmd/music-cli play ~/Desktop/music.mp3 start music.mp3...まとめ
今回はmp3を再生するcliツールを作成してみました。
簡単に作成できて、これから派生もたくさんできるので、ぜひ改良していってください!少しでもプログラミングのお役に立てれば幸いです!
- 投稿日:2020-01-20T23:23:07+09:00
GoのGinでハンドラーに引数を渡したいとき
検索してたどり着いたみなさん、Gin やってますか?私は最近Go X Gin X LINE bot なサービスを作ろうとして結構悩みました。この経験を共有できたらいいな、と記事を書いています。
困ったこと:router.GET("/", handler.Index) に引数がない!
Goを始めてWebアプリを作ろうとしてGinに手を出した人には共感してもらえる(かもしれない)と思うんですが、私にはとても不思議に思えることがありました。アプリのルーティングを設定する場所に、Tour of Goとかでは見たことがない書き方が現れるのです。たとえば、こんな感じですね。
main.gofunc main() { router := gin.Defualt() ... router.GET("/", handler.Index) ... }素人の視点からすると、handler.Indexってなんなんだ!なんでカッコが付いてないんだ!とうならざるを得ません。そしてさらに悪いことに、LINE botを使おうとしていた私はドツボにハマることになります。LINE botはGoのためのSDKを用意していて、そのドキュメント(から類推するところ)によると最初に bot client instanceを作る必要があります。インスタンスは連発するとメモリ食いそうだから最初に呼び出したものを使いたい...
main.gofunc main() { bot, err := linebot.New(<channel secret>, <channel access token>) ... }うーん、困った。ハンドラーでbotのメソッドを使いたいのに、ハンドラーには引数がないからbotインスタンスを渡せない...
整理すると、
main.gofunc main() { router := gin.Default() bot, err := linebot.New(~~~, ~~~) ... router.GET("/", handler.Index) //ここにbotを渡したい!!!!! }(本筋に関係ない部分は適当に書いてます。また、
router.GET("/", handler.Index)
にbotを渡すことないだろう、というツッコミは甘んじて受け入れます。メソッドはGET
でもPOST
でもなんでもいいです。ルーティングも/callback
とかの方が適切かもしれません。一つの具体例として多めに見てください。ハンドラーの名前も他にあるでしょう)←長い言い訳ですが、さて、この問題どう解決すればいいでしょうか?ハンドラーの型に注目する
router.GET("/", handler.Index)
の部分は、パッケージを分けて書いてあるわけですが、一方で関数リテラルで書く方法もあります。main.gorouter.GET("/", func (c *gin.Context) { ... })一体どうなっているんだろう... ここで
GET
メソッドの中身はどうなっているんだ、と調べてみます。とくにハンドラーの型がどうなっているんでしょう。routergruoup.go... // GET is a shortcut for router.Handle("GET", path, handle). func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("GET", relativePath, handlers) } ...ハンドラーの部分は
handlers ...HandlerFunc
となっていますね。...
が前につく型はN個(Nは0を含む自然数)の引数をとることができるというルールです。詳しくは言語仕様を読んでみましょう。では、型
HandlerFunc
とはなんなのか?その定義にさかのぼってみます。gin.go... // HandlerFunc defines the handler used by gin middleware as return value. type HandlerFunc func(*Context) ...簡潔な定義です。ここで明らかになるのは、
HandlerFunc
の型は関数型func(*Context)
であるということです。(型Context
はgin.goで定義される構造体です。他のパッケージから参照する場合はgin.Context
と書きますので、この記事でも2つの書き方が現れます)。どうりで、ハンドラーを関数リテラルで書くときは必ずmain.gorouter.GET("/", func (c *gin.Context) { ... })というふうに書くわけです。ここからわかることは、ハンドラーの型は
func(*Context)
でなければならない、ということです。さて、私たちが書きたい関数
Xxx
はどんな形をしているでしょう。以下のようになって欲しいですね。main.gofunc main() { ... bot, err := linebot.New() ... router.GET("/", handler.Xxx(bot)) ... }関数
Xxx
の型を考えると、引数がbot *linebot.Client
であり、戻り値がfunc(*gin.Context)
でなければならない。つまり、関数Xxx
の宣言は次のようになるでしょう。handler.gofunc Xxx(bot *linebot.Client) func(*gin.Context) { ... }型で言うと
func(*linebot.Client) func(*gin.Context)
というふうになります。へんてこりんですね。ハンドラーの中身を考える
さて、ハンドラーの外側がわかってきたので、いよいよ中身を考えましょう。なんやこらー、とパソコンをぶん投げたくなりますが、順番に考えましょう。関数に戻り値があるということは、当然ですが、ブロックの中で
return
されるものがあります。簡単な関数の例を考えましょう。func succeess( x int) int { var y int y = x + 1 return y }少し大げさに書きましたが、この場合は戻り値は
int
型であり、return
されるのはy
です。宣言されている通り、y
はint
型です。同じように考えて、関数
Xxx
の戻り値はfunc(*gin.Context)
型であるから、次のようなものがreturn
されるはずです。handler.gofunc Xxx(bot *linebot.Client) func(*gin.Context) { ... return func(c *gin.Context){ ... } ... }こうなれば簡単ですね。
return func(c *gin.Context){ ... }
のブロックの内部でbot
を使うことができます。おわりに
LINE公式のGithubリポジトリーのサンプルはパッケージが1つしかありませんでしたため、パッケージを分ける技術を模索しました。次のサイトを参考にさせていただきました。最後ですがお礼申し上げます。
Ginのコードは公式リポジトリーから引用しました。
https://github.com/gin-gonic/ginLINE bot 公式リポジトリー
https://github.com/line/line-bot-sdk-go
- 投稿日:2020-01-20T20:14:33+09:00
標準入力のあるプログラムを delve でデバッグしたい
Goで開発していて delve は便利なデバッグツールですが、標準入力のあるプログラムはそのままではデバッグができません
標準入力をプログラム本体に入力することができないためです対処法
まず、プログラムを一度コンパイルし、
コンパイルしたプログラムを実行します$ go build -gcflags="-N -l" hoge.go $ ./hogeで、 新しい ターミナルを開き
このプロセスに対してdlvをアタッチします$ dlv attach $(pgrep -fn hoge)
dlv attach
コマンドは、引数に pid を渡すことで、その Go プログラムのデバッグを開始しますこれで無事にデバッグができるようになります。
プログラム本体側のターミナルで標準入力ができますし、dlv側のターミナルでデバッグコマンドが使えますめでたしめでたし
- 投稿日:2020-01-20T20:05:54+09:00
golangでバイナリ操作 - ヌル文字が出てくるまで取得したい
課題:
任意の文字列が与えられたとき、ヌル文字まで取得したい
え、普通にSplit使えばいいじゃんて?それに気づいたのは遥か先だった。。
- 方法1: 直接ゴリゴリ
- 方法2: バッファに入れてNextで読み出し
- 方法2: バッファに入れてReadで読み出し
- 方法3: バッファに入れてencoding/binaryのReadで1バイトずつ読み出し
- 方法4: splitsで分けちゃえ
試したもの
package main import ( "bytes" "crypto/rand" "encoding/binary" "io" "testing" ) func generateRandomBytes() []byte { token := make([]byte, 1000000) rand.Read(token) return token } func BenchmarkSplitByNullByte_Direct(b *testing.B) { token := generateRandomBytes() b.ResetTimer() var bytes [][]byte bytes = append(bytes, []byte{}) for _, v := range token{ bytes[len(bytes)-1] = append(bytes[len(bytes)-1], v) if v == 0x00 { bytes = append(bytes, []byte{}) } } } func BenchmarkSplitByNullByte_BytesNext(b *testing.B) { token := generateRandomBytes() b.ResetTimer() buf := bytes.NewBuffer(token) var bytes [][]byte bytes = append(bytes, []byte{}) v := buf.Next(1) for ; ; v = buf.Next(1) { if len(v) == 0 { break } bytes[len(bytes)-1] = append(bytes[len(bytes)-1], v[0]) if v[0] == 0x00 { bytes = append(bytes, []byte{}) } } } func BenchmarkSplitByNullByte_BytesRead(b *testing.B) { token := generateRandomBytes() b.ResetTimer() buf := bytes.NewBuffer(token) var bytes [][]byte bytes = append(bytes, []byte{}) var n int v := make([]byte, 1) n, _ = buf.Read(v) for ; ; n, _ = buf.Read(v) { if n == 0 { break } bytes[len(bytes)-1] = append(bytes[len(bytes)-1], v[0]) if v[0] == 0x00 { bytes = append(bytes, []byte{}) } } } func BenchmarkSplitByNullByte_EncodingBinaryRead(b *testing.B) { token := generateRandomBytes() b.ResetTimer() buf := bytes.NewBuffer(token) var bytes [][]byte bytes = append(bytes, []byte{}) var err error v := make([]byte, 1) binary.Read(buf, binary.BigEndian, v) for ; ; err = binary.Read(buf, binary.BigEndian, v) { if err == io.EOF { break } bytes[len(bytes)-1] = append(bytes[len(bytes)-1], v[0]) if v[0] == 0x00 { bytes = append(bytes, []byte{}) } } } func BenchmarkSplitByNullByte_Split(b *testing.B) { token := generateRandomBytes() b.ResetTimer() bytes.Split(token, []byte("\x00")) }結果
goos: linux goarch: amd64 pkg: github.com/gpioblink/sandbox BenchmarkSplitByNullByte_Direct-8 1000000000 0.00876 ns/op BenchmarkSplitByNullByte_BytesNext-8 1000000000 0.0106 ns/op BenchmarkSplitByNullByte_BytesRead-8 1000000000 0.0142 ns/op BenchmarkSplitByNullByte_EncodingBinaryRead-8 1000000000 0.101 ns/op BenchmarkSplitByNullByte_Split-8 1000000000 0.000196 ns/op PASSうん、やっぱり普通にSplit使うのが一番早いよね。。
NextよりReadの方が遅いのはちょっと意外だったかな。コード見ると内部でコピーとかやってるからかな?ゴリ押しのコードも、頑張れば絶対もっと早く出来ると思います。何か知っていたら教えてください!
結論
普通にSplit使え
- 投稿日:2020-01-20T19:30:49+09:00
GoのInterfaceを理解する
はじめに
この記事は私と同じようにGoを勉強し始めたものの
interfaceを理解できない方へ向けたものとなっております。
interfaceは2個の意味が存在しています。
- なんでも入れられる型 interface{}
- 構造体などに紐づけ出来る書き方
今回は後者のほうを説明していきます。
ちなみに関数名などは適当なので申し訳ございません。コード
package main import ( "fmt" ) type Fish interface { Place()string //住んでいる場所 } type Sanma struct {} type Aji struct {} func (s *Sanma)Place()string{ return "Japan" } func (a *Aji)Place()string{ return "Europe" } type Sea struct {} func(s *Sea)seafish(fish Fish){ fmt.Println("どこに住んでるの?:" + fish.Place()) } func main(){ sanma := &Sanma{} aji := &Aji{} sea := Sea{} fmt.Println(sanma.Place()) fmt.Println(aji.Place()) sea.seafish(sanma) }解説
上記のサンプルコードはinterfaceを使ったものとなっています。
Fishというinterfaceを宣言し、そこにPlaceという関数を宣言します。
SanmaとAjiという構造体を宣言します。作成した構造体のメソッドとして
Place関数を宣言します。interfaceにあるすべての関数を宣言するだけで紐づけすることができます。
この事によりSea構造体のseafishメソッドの引数にFishと紐づいている構造体を引数として渡すことができます。interfaceの重要性
もしinterfaceを使わない場合はseafish関数の引数が各構造体を引数とし
Aji用のseafishとSanma用のseafishを宣言しなければいけなくなり、コードが長くなってしまいます。終わりに
godoc見ていくといろいろとinterfaceで宣言されているので見てみると面白いかもしれません。
最後まで読んでいただきありがとうございました。
- 投稿日:2020-01-20T11:59:37+09:00
template.ParseFilesの省略
自分用に、いちいちtemplateを呼び出すときに2,3行かくのめんどいので1行で書こうとした。
func handleIndex(w http.ResponseWriter, r *http.Request) { v := 1 render("index").Execute(w, v) //t :=template.ParseFiles("index") t.Execute(w, v) を省略した。()の中身を変えるだけでfileが呼び出せる。 //変更するのは()の引数とvの値のみでOK!!わざわざtemplate.Parsefile書くのめんどかった。 }func render(name string) *template.Template { t, _ := template.ParseFiles(name + ".html") return t } //引数にfile名を指定し活用
- 投稿日:2020-01-20T03:35:19+09:00
Golang logrusによるロギング
概要
golangのロギングパッケージである、logrusを用いて、以下のようにログを出力させる方法について記載する。
[INFO]:2020-01-20 03:06:19 [main.go:19] - info msg1出力内容は、レベル、時刻、ファイル名、ライン位置、メッセージである。
また、以下についても記載する。
- ログファイルへの出力
- ログレベルの変更
Git: k-washi/gologger
インストール
go get github.com/sirupsen/logruslogrusの設定ファイル
まず、設定に使用する関数を作成する。
詳細はプログラム内に記載。utils.gopackage gologger import ( "os" "strings" ) //openFile ログを出力するファイルを設定する。 //ファイルが存在する場合、ファイルにログを追記。 //ファイルが存在しない場合、ファイルを作成し、ログを出力。 func openFile(fileName string) (*os.File, error) { if exists(fileName) { f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0777) return f, err } f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0777) return f, err } //formatFilePath ログに記載するファイル名の抽出 func formatFilePath(path string) string { arr := strings.Split(path, "/") return arr[len(arr)-1] } //exists ファイルが存在するか確認する。 func exists(name string) bool { _, err := os.Stat(name) return !os.IsNotExist(err) }logrusのformatを参考にして、ロギングフォーマットを設定。
詳細はプログラム内に記載。log.gopackage gologger import ( "bytes" "fmt" "io" "os" "strings" "github.com/sirupsen/logrus" ) type logFormat struct { TimestampFormat string } //Format ログの形式を設定 func (f *logFormat) Format(entry *logrus.Entry) ([]byte, error) { var b *bytes.Buffer if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } b.WriteByte('[') b.WriteString(strings.ToUpper(entry.Level.String())) b.WriteString("]:") b.WriteString(entry.Time.Format(f.TimestampFormat)) b.WriteString(" [") b.WriteString(formatFilePath(entry.Caller.File)) b.WriteString(":") fmt.Fprint(b, entry.Caller.Line) b.WriteString("] ") if entry.Message != "" { b.WriteString(" - ") b.WriteString(entry.Message) } if len(entry.Data) > 0 { b.WriteString(" || ") } for key, value := range entry.Data { b.WriteString(key) b.WriteByte('=') b.WriteByte('{') fmt.Fprint(b, value) b.WriteString("}, ") } b.WriteByte('\n') return b.Bytes(), nil } //init パッケージ読み込み時に実行される。 func init() { logrus.SetReportCaller(true) //Caller(実行ファイル(ex. main.go)を扱うため) formatter := logFormat{} formatter.TimestampFormat = "2006-01-02 15:04:05" //時刻設定 logrus.SetFormatter(&formatter) //ログ出力ファイルの設定 f, err := openFile("log.txt") if err != nil { logrus.Fatal(err) } logrus.SetOutput(io.MultiWriter(os.Stdout, f)) //ログレベルの設定 logrus.SetLevel(logrus.InfoLevel) } //SetLevelDebug Debugレベルに設定 func SetLevelDebug() { logrus.SetLevel(logrus.DebugLevel) } //SetLevelInfo Set Infoレベルに設定 func SetLevelInfo() { logrus.SetLevel(logrus.InfoLevel) }ロギング
以下のように、main.goで設定パッケージを読み込むことで、logrusの設定が読み込まれる。
もし、ログレベルを変更したい場合は、コメントアウトした関数を使用する。#出力結果 [INFO]:2020-01-20 03:32:53 [main.go:22] - info msg1 [INFO]:2020-01-20 03:32:53 [subpkg.go:9] - sub info msg1main.gopackage main import ( "github.com/k-washi/gologger/test/subpkg" _ "github.com/k-washi/gologger" log "github.com/sirupsen/logrus" ) func main() { //logSetter.SetLevelDebug() log.Info("info msg1") log.Debug("debug msg1") subpkg.SubPrint() }subpkg.gopackage subpkg import ( log "github.com/sirupsen/logrus" ) func SubPrint() { log.Info("sub info msg1") log.Debug("sub debug msg1") }