20200517のGoに関する記事は9件です。

【Golang】Switch文

【Golang】Switch文

Golangの基礎学習〜Webアプリケーション作成までの学習を終えたので、復習を兼ねてまとめていく。 基礎〜応用まで。

package main
//swich
//if swichの違いは?
//簡単な分岐処理ならswitchの方がすっきりする。
//型アサーションができる。

/*
まず、Goでは選択されたcaseだけを実行し、それに続く全てのcase節は実行されない。
他の言語では各case節の最後にbreakステートメントを記述しますが、Goでは自動的にbreakが入る。
*/

import (
    "fmt"
    "time"
)


func main(){
    //今の時間取得
    t := time.Now()
    //今の時間の時間だけ取得
    fmt.Println(t.Hour())

    //switch 分岐
    switch {
    case t.Hour() < 12:
        fmt.Println("午前")
    case t.Hour() > 12:
        fmt.Println("午後")
    }



    n := 1

    switch n {
    case 1:
        n += 1
        fmt.Println("3以下")
    case 2:
        n += 1
        fmt.Println("3以上")
    default:
        fmt.Println("default")
    }
    fmt.Println(n)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go 言語で QR コードを生成するコマンドを作ってみた

Go 言語で QR コードを生成するコマンドを作ってみた

はじめに

  • Go 言語で QR コードを生成するコマンドを作ってみました

hello world

  • 作業メモ的に残しておきます
  • 無駄に長いです
  • GitHub go-mkqrcode

リポジトリの作成

何はともあれ、Github でリポジトリを作成します。とりあえず go-mkqrcode という名前で作ってみました。

git clone git@github.com:yoshi389111/go-mkqrcode.git
cd go-mkqrcode

デフォルトの Initial commit に絵文字 :tada: をつけたいので rebase -i します。

git rebase -i --root
# `Inital commit` -> `:tada: Inital commit`
git push -f

QR コードを作るライブラリ

Go 言語で QR コードを作るライブラリとして有名な github.com/boombuler/barcode を使用することにしました。

go get github.com/boombuler/barcode

サンプルコードをもとに、とりあえず QR コードを出力してみます。

package main

import (
    "image/png"
    "log"
    "os"

    "github.com/boombuler/barcode"
    "github.com/boombuler/barcode/qr"
)

func main() {
    qrCode, err := qr.Encode("Hello World", qr.M, qr.Auto)
    if err != nil {
        log.Fatal(err)
    }

    qrCode, err = barcode.Scale(qrCode, 200, 200)
    if err != nil {
        log.Fatal(err)
    }

    file, err := os.Create("qrcode.png")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    err = png.Encode(file, qrCode)
    if err != nil {
        log.Fatal(err)
    }
}

ほとんどサンプルのままで、エラー処理だけ追加しました。

go run mkqrcode.go

とりあえず動かしてみて、QR コード(pngファイル)が正しく生成されていることを確認しました。

オプション引数を処理する flag

Go 言語の標準ライブラリで、コマンドライン(オプション)を処理するものは flag というもののようです。

これを使って、QR コードを作るデータや、出力先ファイル名を指定可能にしてみます。

package main

import (
    "flag"
    "fmt"
    "image/png"
    "log"
    "os"

    "github.com/boombuler/barcode"
    "github.com/boombuler/barcode/qr"
)

func main() {
    outFile := flag.String("o", "", "output file name")
    flag.Parse()
    args := flag.Args()
    if len(args) != 1 {
        flag.Usage()
        os.Exit(1)
    }
    if *outFile == "" {
        fmt.Fprintln(os.Stderr, "output file is required.")
        os.Exit(1)
    }

    message := args[0]

    qrCode, err := qr.Encode(message, qr.M, qr.Auto)
    if err != nil {
        log.Fatal(err)
    }

    qrCode, err = barcode.Scale(qrCode, 200, 200)
    if err != nil {
        log.Fatal(err)
    }

    file, err := os.Create(*outFile)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    err = png.Encode(file, qrCode)
    if err != nil {
        log.Fatal(err)
    }
}

出力ファイル名は -o オプションで指定して、コマンドライン引数を QR コードを作るメッセージとしました。
後で -o を省略可にしようと思いますが、現時点では必須のオプションという意味不明な状態です。

go run mkqrcode.go -o hello.png hello

QR コードをテキストとしても出力してみる

QR コードを png ファイルとしては出力できるようになりましたが、小さい QR コードの場合、ターミナルに直接表示できたら便利かもしれません(あんまり長いデータだとターミナルからはみ出ちゃいそうですが)。

そこで、テキストとしても出力できるようにしてみます。

package main

import (
    "flag"
    "fmt"
    "image/color"
    "image/png"
    "log"
    "os"

    "github.com/boombuler/barcode"
    "github.com/boombuler/barcode/qr"
)

func main() {
    outFile := flag.String("o", "", "output file name")
    flag.Parse()
    args := flag.Args()
    if len(args) != 1 {
        flag.Usage()
        os.Exit(1)
    }

    message := args[0]

    qrCode, err := qr.Encode(message, qr.M, qr.Auto)
    if err != nil {
        log.Fatal(err)
    }

    if *outFile != "" {
        err = outputQrCode(qrCode, 200, *outFile)
        if err != nil {
            log.Fatal(err)
        }

    } else {
        black := "\033[40m  \033[0m"
        white := "\033[47m  \033[0m"
        printQrCode(qrCode, black, white)
    }
}

func outputQrCode(qrCode barcode.Barcode, size int, fileName string) error {
    qrCode, err := barcode.Scale(qrCode, size, size)
    if err != nil {
        return err
    }

    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()

    err = png.Encode(file, qrCode)
    return err
}

func printQrCode(qrCode barcode.Barcode, black, white string) {
    rect := qrCode.Bounds()
    for y := rect.Min.Y; y < rect.Max.Y; y++ {
        for x := rect.Min.X; x < rect.Max.X; x++ {
            if qrCode.At(x, y) == color.Black {
                fmt.Print(black)
            } else {
                fmt.Print(white)
            }
        }
        fmt.Println()
    }
}

これで -o オプションは省略可能になりました。

テキストで出力する場合、白と黒はそれぞれ ANSI エスケープシーケンスで背景色を白/黒に設定して空白 2 文字を出力するようにしています。

go run mkqrcode.go hello

マージンを設定する

ターミナルに表示してみると、QR コードの周りの余白が足りない気がしてきます。

一般的に、QR コードの周りにはマージンとして 4 セル分の余白が必要になります。

もちろん、出力した画像やテキストに 4 セル分の余白がなくても、最終的に QR コードを掲示する際に余白があれば良いわけですが、出力時にマージンが指定できるとよいのではないかと思います。

オプションでマージンを指定できるようにしてみました。

package main

import (
    "flag"
    "fmt"
    "image"
    "image/color"
    "image/png"
    "log"
    "os"

    "github.com/boombuler/barcode"
    "github.com/boombuler/barcode/qr"
)

func main() {
    outFile := flag.String("o", "", "output file name")
    margin := flag.Int("m", 4, "margin of QR-code")
    flag.Parse()
    args := flag.Args()
    if len(args) != 1 {
        flag.Usage()
        os.Exit(1)
    }
    if *margin < 0 {
        fmt.Fprintln(os.Stderr, "Margin is negative")
        os.Exit(1)
    }

    message := args[0]

    qrCode, err := qr.Encode(message, qr.M, qr.Auto)
    if err != nil {
        log.Fatal(err)
    }

    if *outFile != "" {
        err = outputQrCode(qrCode, *margin, 200, *outFile)
        if err != nil {
            log.Fatal(err)
        }

    } else {
        black := "\033[40m  \033[0m"
        white := "\033[47m  \033[0m"
        printQrCode(qrCode, *margin, black, white)
    }
}

func outputQrCode(qrCode barcode.Barcode, margin, size int, fileName string) error {
    rect := qrCode.Bounds()
    cells := rect.Max.X + margin*2
    if size < cells {
        return fmt.Errorf("size is too small. required %d <= size", cells)
    }

    img := image.NewRGBA(image.Rect(0, 0, size, size))
    for y := 0; y < size; y++ {
        for x := 0; x < size; x++ {
            cx := x*cells/size - margin
            cy := y*cells/size - margin
            if rect.Min.X <= cx && cx < rect.Max.X &&
                rect.Min.Y <= cy && cy < rect.Max.Y &&
                qrCode.At(cx, cy) == color.Black {

                img.Set(x, y, color.Black)
            } else {
                img.Set(x, y, color.White)
            }
        }
    }

    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()

    err = png.Encode(file, img)
    return err
}

func printQrCode(qrCode barcode.Barcode, margin int, black, white string) {
    rect := qrCode.Bounds()
    for y := rect.Min.Y - margin; y < rect.Max.Y+margin; y++ {
        for x := rect.Min.X - margin; x < rect.Max.X+margin; x++ {
            if rect.Min.X <= x && x < rect.Max.X &&
                rect.Min.Y <= y && y < rect.Max.Y &&
                qrCode.At(x, y) == color.Black {

                fmt.Print(black)
            } else {
                fmt.Print(white)
            }
        }
        fmt.Println()
    }
}

マージンは -m オプションで指定します。

boombuler/barcode の機能としてマージンを指定することができなそうだったので、自力で画像を作ってみました。

いろいろオプションをつけてみる

ほかにもいろいろオプションをつけてみます。

package main

import (
    "flag"
    "fmt"
    "image"
    "image/color"
    "image/png"
    "log"
    "os"
    "strings"

    "github.com/boombuler/barcode"
    "github.com/boombuler/barcode/qr"
)

const versionInfo = "@(#) $Id: mkqrcode.go 0.5.0 2020-05-17 14:59 yoshi389111 Exp $"

func usage() {
    o := flag.CommandLine.Output()
    fmt.Fprintf(o, "Usage: %s [options] MESSAGE", flag.CommandLine.Name())
    flag.PrintDefaults()
}

func main() {
    outFile := flag.String("o", "", "output file name")
    margin := flag.Int("m", 4, "margin of QR-code")
    black := flag.String("b", "", "pattern of black")
    white := flag.String("w", "", "pattern of white")
    size := flag.Int("s", 200, "size of QR-code(Image)")
    optLevel := flag.String("l", "M", "error correction level [L|M|Q|H]")
    optEncoding := flag.String("e", "auto", "encoding of QR-code [auto|numeric|alphanumeric|unicode]")
    version := flag.Bool("v", false, "show version info")
    flag.Usage = usage
    flag.Parse()
    args := flag.Args()
    if *version {
        fmt.Println(versionInfo)
        os.Exit(0)
    }
    if len(args) != 1 {
        flag.Usage()
        os.Exit(1)
    }
    if *margin < 0 {
        fmt.Fprintln(os.Stderr, "margin is negative")
        os.Exit(1)
    }
    if *black == "" && *white == "" {
        *black = "\033[40m  \033[0m"
        *white = "\033[47m  \033[0m"
    } else if *black == "" || *white == "" {
        fmt.Fprintln(os.Stderr, "specify both black and white")
        os.Exit(1)
    }
    var level qr.ErrorCorrectionLevel
    var encoding qr.Encoding

    switch strings.ToUpper(*optLevel) {
    case "L":
        level = qr.L
    case "M":
        level = qr.M
    case "Q":
        level = qr.Q
    case "H":
        level = qr.H
    default:
        fmt.Fprintf(os.Stderr, "invalid error correction level. (%s)\n", *optLevel)
        os.Exit(1)
    }
    switch strings.ToLower(*optEncoding) {
    case "auto":
        encoding = qr.Auto
    case "numeric":
        encoding = qr.Numeric
    case "alphanumeric":
        encoding = qr.AlphaNumeric
    case "unicode":
        encoding = qr.Unicode
    default:
        fmt.Fprintf(os.Stderr, "invalid encoding. (%s)\n", *optEncoding)
        os.Exit(1)
    }

    message := args[0]

    qrCode, err := qr.Encode(message, level, encoding)
    if err != nil {
        log.Fatal(err)
    }

    if *outFile != "" {
        err = outputQrCode(qrCode, *margin, *size, *outFile)
        if err != nil {
            log.Fatal(err)
        }

    } else {
        printQrCode(qrCode, *margin, *black, *white)
    }
}
// 以下略

flag をやめて getopt にしてみる

Unix/Linux 系のコマンドは、最近はショートオプションとロングオプションの両方が使えるのが一般的になっている気がします。

特にヘルプ( -h--help )とバージョン情報( -v--version )の表示については、一般的に使われやすいものなので、両対応をしておきたいことが多いと思います。

しかし標準の flag では、ロングオプションとショートオプションを同時に定義ができないようです。

少し調べてみると flag 以外にも様々なコマンドライン処理用のライブラリがあるようです。
そのなかで flag によく似た pborman/getopt を使ってみようと思います。

go get github.com/pborman/getopt
package main

import (
    "fmt"
    "image"
    "image/color"
    "image/png"
    "log"
    "os"
    "strings"

    "github.com/pborman/getopt/v2"

    "github.com/boombuler/barcode"
    "github.com/boombuler/barcode/qr"
)

const versionInfo = "@(#) $Id: mkqrcode.go 0.6.0 2020-05-17 15:34 yoshi389111 Exp $"

func main() {
    getopt.SetParameters("MESSAGE")
    outFile := getopt.StringLong("output", 'o', "", "output file name", "FILE")
    margin := getopt.IntLong("margin", 'm', 4, "margin of QR-code", "NUMBER")
    black := getopt.StringLong("black", 'b', "", "pattern of black", "STRING")
    white := getopt.StringLong("white", 'w', "", "pattern of white", "STRING")
    size := getopt.IntLong("size", 's', 200, "size of QR-code(Image)", "NUMBER")
    optLevel := getopt.EnumLong("level", 'l',
        []string{"L", "M", "Q", "H"},
        "M", "error correction level", "{L|M|Q|H}")
    optEncoding := getopt.EnumLong("encoding", 'e',
        []string{"auto", "numeric", "alphanumeric", "unicode"},
        "auto", "encoding of QR-code", "{auto|numeric|alphanumeric|unicode}")
    version := getopt.BoolLong("version", 'v', "show version info")
    help := getopt.BoolLong("help", 'h', "show help message")
    getopt.Parse()
    args := getopt.Args()
    if *help {
        getopt.PrintUsage(os.Stdout)
        os.Exit(0)
    }
    if *version {
        fmt.Println(versionInfo)
        os.Exit(0)
    }
    if len(args) != 1 {
        getopt.Usage()
        os.Exit(1)
    }
// 以下略

Installation

go get github.com/yoshi389111/go-mkqrcode

demo

go-mkqrcode "hello world"

hello world

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GO言語学習ノート#2 変数型②

1、変数

1.4 基本データ型

①整数型

データ型 説明
uint8 符号なし8 ビット整数型(0 ~ 255)
uint16 符号なし16 ビット整数型(0 ~ 65535)
uint32 符号なし32 ビット整数型(0 ~ 4294967295)
uint64 符号なし64 ビット整数型(0 ~ 18446744073709551615)
int8 符号あり8 ビット整数型(-128 ~ 127)
int16 符号あり16 ビット整数型(-32768 ~ 32767)
int32 符号あり32 ビット整数型(-2147483648 ~ 2147483647)
int64 符号あり64 ビット整数型(-9223372036854775808 ~ 9223372036854775807)

特殊整数型

データ型 説明
uint 32bitシステムはuint32、64bitシステムはuint64
int  32bitシステムはint32、64bitシステムはint64
uintptr ポインタを格納するために使用される符号なし整数型

注意事項:長さを取得するlen()関数によって返される長さは、システムによって異なる場合がある。 実際の使用では、スライスまたはmapの要素の数はintで表すことができる。 バイナリ転送構造やファイル読み取り・書き込みに関しては、ファイルの構造を維持するために、異なるコンパイルターゲットプラットフォームのバイト長の影響を受けないように、intとuintを使用しないでください。

八進法と十六進法および指数表記も使える。標準ライブラリのmathは各データ型の範囲を定義する。

package main

import (
    "fmt"
    "math"
)

func main() {
    a, b, c := 100, 0144, 0x64
    fmt.Println(a, b, c)
    fmt.Printf("0b%b, %#o, %#x\n", a, a, a)

    fmt.Println(math.MinInt8, math.MaxInt8)
}

出力

100 100 100
0b1100100,0144,0x64
-128 127

②浮動小数点数
Go言語は、float32とfloat64の2つの浮動小数点数をサポートしている。 これらの2つの浮動小数点数は、IEEE754標準に準拠している。float32浮動小数点数の最大範囲は約3.4e38で、定数math.MaxFloat32で定義できる。 float64 floatの最大範囲は約1.8e308で、定数math.MaxFloat64で定義できます。

fmtパッケージと%fをを使用して、浮動小数点数を出力する。

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Printf("%f\n", math.Pi)
    fmt.Printf("%.2f\n", math.Pi)
    fmt.Println(math.MaxFloat32)
    fmt.Println(math.MaxFloat64)
}

出力

3.141593
3.14
3.4028234663852886e+38
1.7976931348623157e+308

②別名型

公式標準では、二つの別名型を述べてある。

byte alias for uint8
rune alias for int32

別名型は変換する必要がない、値を直接的に与えることができる

package main

import "fmt"

func test(x byte) {
    fmt.Println(x)
}

func main() {
    var a byte = 0x11
    var b uint8 = a
    var c uint8 = a + b

    test(c)
}

しかし、これは、同じ基本構造を持つものが別名型に属していることを意味するわけではない。64bitシステムはintとint64の構造が一致しても、異なる型に属している、明示的型変換しなければならない。

1.5 参照型変数

参照型変数(reference type)、普通にslice,map,channelを意味する。

数字やArrayと比べ、参照型変数はより複雑な構造がある。メモリの割り当てに加えて、ポインタ、長さ、ハッシュ分散、データキューなどの一連のプロパティも初期化する必要がある。

関数newは、指定された型の長さに従ってゼロ値のメモリを割り当て、ポインタを返す。型の内部構造と初期化メソッドはしない。
参照型変数は、make関数を使用して作成する必要があり、コンパイラーは, makeをターゲット関数型変換の作成関数または命令に変換して、メモリ割り当てと関連するプロパティの初期化を正しく完成させる。

package main

func mkslince() []int {
    s := make([]int, 0, 10)
    s = append(s, 100)

    return s
}

func mkmap() map[string]int {
    m := make(map[string]int)
    m["a"] = 1
    return m
}
func main() {
    m := mkmap()
    println(m["a"])

    s := mkslince()
    println(s[0])
}

もちろん、関数newは参照型にメモリを割り当てることもできるが、この作成は不完全である。

package main

import "fmt"

func main() {
    p := new(map[string]int) //関数new、ポインタを返す
    m := *p

    m["a"] = 1     //エラー  panic: assignment to entry in nil map
    fmt.Println(m)
}

1.6 型変換

暗黙的型変換によって引き起こされる問題は、それがもたらすメリットよりもはるかに大きい。
定数、別名型、と無名型以外、golangでは、明示的型変換は必須である。

    a := 10
    b := byte(a)
    c := a + int(b)  //混合型表現は型の一貫性を保つ必要がある

変換のオブジェクトがポインタ、一方向チャネル、または戻り値のない関数型の場合は、構文分解エラーを避けるために括弧を使用する必要がある。

func main() {
    x:=100
    p:=*int(&x) //エラー  cannot convert &x (type *int) to type int
    println(p)
}

括弧を使ってポインタ型だと解析させるのが正しいやり方である。

(*int)(p)            ⇒括弧なければ⇒  *(int(p))
(<-chan int)(c)      ⇒括弧なければ⇒    <-(chan int(c))
(func())(x)          ⇒括弧なければ⇒    func()x
func()int(x)         ⇒括弧なければ⇒   (func()int)(x)

最後の例だが、返す値がある関数では、括弧を省略できるが、使用すれば、読みやすくなる。

1.7 ユーザ定義型

キーワードtypeを使用すれば、ユーザ定義型を定義できる。

package main

import "fmt"

type flags byte

const (
    read flags = 1 << iota
    write
    exec
)

func main() {
    f := read | exec
    fmt.Printf("%b\n", f)
}

varやtypeと類似、いくつかのtypeをクループに構成し、関数やコードブロック内にローカル変数を定義する。

package main

import "fmt"

func main() {
    type (
        user struct {
            name string
            age  uint8
        }
        event func(string) bool
    )

    u := user{"Zhang", 20}
    fmt.Println(u)

    var f event = func(s string) bool {
        println(s)
        return s != ""
    }
    f("abc")
}

基礎型が指定されていても、それは、それらが同じ基礎型とデータ構造を持っていることを示すだけであり、2つの間には関係がなく、全く異なる型に属していることを示している。

package main

func main() {
    type data int
    var d data = 10

    var x int = d  //エラー:cannot use d (type data) as type int in assignment
    println(x)

    println(d == x) //エラー: invalid operation: d == x (mismatched types data and int)
}

T

1.8 無名型

bool, int, string などの型が明示的な識別子を持つのに対して、array, slice, dictionary, channel などの型は、特定の要素の型や長さなどのプロパティに関連しているため、無名型(unnamed type)と呼ばれている。もちろん、typeを使って、具体的な名称を与え、夏名付け型(named type)に変換する。

同じステートメントを持つ無名型は、同じ型として扱われる。
・同じ基底型のポインタ
・同じ要素の型と長さの配列(array)
・同じ要素タイプのスライス(slice)
・同じkey-valueタイプのマップ(map)
・同じデータタイプと動作方向のチャンネル(channel)
・同じフィールド名、フィールド型、ラベル、フィールド順序のコンストラクト(struct)
・同じ引数と戻り値のリスト(パラメータ名は含まない)の関数 (func)
・同じメソッド名、メソッドシグネチャ(順序を含まない)を持つインターフェース(interface)

                             

GO言語学習ノート#1  変数型(1)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Docker】GolangからMySqlへ接続してみた

やりたいこと

GolangコンテナからMySqlコンテナに接続したい!

環境

docker 19.03.8
docker-compose 1.25.5

リポジトリ

https://github.com/atsugitakuya/go-mysql-docker.git

フォルダ階層

root /
 ├ app /
 | └ main.go
 ├ docker /
 | └ Dockerfile
 └ docker-compose.yaml

main.go作成

今回はgormというORMライブラリを使って接続していく。
https://gorm.io/

/app/main.go
package main

import (
    "fmt"
    "net/http"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

func main() {
    http.HandleFunc("/", index)
    http.ListenAndServe(":80", nil)
}

// mysqlconnectionが確認できれば「DB接続成功」が表示される
// 接続に失敗時はエラーメッセージを吐く
func index(w http.ResponseWriter, r *http.Request) {
    _, err := sqlConnect()
    if err != nil {
        fmt.Fprintf(w, err.Error())
    } else {
        fmt.Fprintf(w, "DB接続成功")
    }
}

// connetion確認func
func sqlConnect() (database *gorm.DB, err error) {
    DBMS := "mysql"
    USER := "root"
    PASS := "golang"
    PROTOCOL := "tcp(mysql:3306)"
    DBNAME := "mysql"

    CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"
    return gorm.Open(DBMS, CONNECT)
}

Dockerfile作成

今回使用するgormとmysqlのdriverをインストールしている。
なんかgitがないと動かないっぽい。あとRunコマンドは少なくした方がいいが良い。
多分もっと良い方法があるはず....

/docker/Dockerfile
FROM golang:alpine
# フォルダ作成
RUN mkdir /app
# 作成したフォルダ内にmain.goを配置
COPY ./app /app
# 作業場所を/appに設定(main.goがある場所)
WORKDIR /app
# 必要ライブラリをインストール
RUN apk add --no-cache git
RUN go get "github.com/go-sql-driver/mysql"
RUN go get -u github.com/jinzhu/gorm
# main.goをbuild
RUN go build -o main . 
# port開放
EXPOSE 80
# app開始
CMD ["/app/main"]

docker-compose.yaml作成

golangとmysqlを動かすためのdocker-compose。特に説明することない。

/docker-compose.yaml
version: '3'

services:
  golang:
    build: 
      context: .
      dockerfile: ./docker/Dockerfile
    ports:
    - "80:80"
    container_name: go-sample
    depends_on:
    - mysql
  mysql:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: golang
      MYSQL_USER: golang
      MYSQL_PASSWORD: golang
      MYSQL_DATABASE: golang
    container_name: mysql

実行し、DBの接続確認を行う

zsh
# docker-compose.yamlのある階層で実行
$docker-compose up -d
....
# 立ち上がったよ!メッセージが出たらOK
Creating mysql ... done
Creating go-sample ... done

localhostに接続すると、「DB接続成功」が出るはず...

スクリーンショット 2020-05-17 17.48.01.png

接続成功!!!!
意外と簡単に接続できた...さすがGoだな...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoのcontextのValueのkeyの型を再考する

はじめに

GoのContextパッケージではctx = WithValue(ctx, key ,value)でcontextに紐付けた値をctx.Value(key)で取得します。

このときkeyとしては他のコードとの衝突をさけるため、以下のコメントにあるようにtypeでオリジナルの型を定義するのが推薦されてます。

// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {

その際、struct{}で定義することが割と多いようで、例えば、DatadogのSpanKeyは以下のように定義されています。

type contextKey struct{}

var activeSpanKey = contextKey{}

// ContextWithSpan returns a copy of the given context which includes the span s.
func ContextWithSpan(ctx context.Context, s Span) context.Context {
    return context.WithValue(ctx, activeSpanKey, s)
}

では、仮にだれかが誤ってtypeで型宣言せずにKeyをstruct{}にて定義し、そのライブラリを利用したとき、他のライブラリ(例えばdatadogのspanなど)や自分が書いたコードのkeyとそのkeyは衝突しないのでしょうか?
と疑問に思ったのでその調査と原因を共有します。

確認

package main

import (
    "context"
    "fmt"
)

type contextKey struct{}
var key = contextKey{}

// 正しく書かれたライブラリを想定
type contextKey2 struct{}
var key2 = contextKey2{}

// 誤って書かれたライブラリを想定
var key3 = struct{}{}
// 誤って書かれたライブラリを想定
var key4 = struct{}{}

func main() {
    ctx := context.WithValue(context.Background(), key, "Go")
    fmt.Println(ctx.Value(key))  // Go
    fmt.Println(ctx.Value(key2)) // nil
    fmt.Println(ctx.Value(key3)) // nil
    fmt.Println(ctx.Value(key4)) // nil
    ctx = context.WithValue(ctx, key2, "Scala")
    fmt.Println(ctx.Value(key))  // Go
    fmt.Println(ctx.Value(key2)) // Scala
    fmt.Println(ctx.Value(key3)) // nil
    fmt.Println(ctx.Value(key4)) // nil
    ctx = context.WithValue(ctx, key, "Java")
    fmt.Println(ctx.Value(key))  // Java
    fmt.Println(ctx.Value(key2)) // Scala
    fmt.Println(ctx.Value(key3)) // nil
    fmt.Println(ctx.Value(key4)) // nil
    ctx = context.WithValue(ctx, key3, "Python")
    fmt.Println(ctx.Value(key))  // Java
    fmt.Println(ctx.Value(key2)) // Scala
    fmt.Println(ctx.Value(key3)) // Python
    fmt.Println(ctx.Value(key4)) // Python


    // 後ほど説明
    // fmt.Println(key == key2)  keyとkey2は比較不可能
    fmt.Println(key == key3) // true
    ikey := interface{}(key)
    ikey2 := interface{}(key2)
    ikey3 := interface{}(key3)

    fmt.Println(ikey == ikey2) // 比較可能になるが、false
    fmt.Println(ikey == ikey3) // false
}

結論からいいます。上記コードをみればわかるように衝突しません。
keykey2はそれぞれ他のキーに影響を受けずに変更、取得できています。
一方、誤ってかかれたkey3への変更はkey3key4両方に影響しています。

原因

以下のcontextのソースの抜粋です。Valueの実装を見ればわかりますが、引数のkeyと自分が保持しているkeyをinterface型として比較し、同一でない場合は自分が保持している親のContextに委譲していることがわかります。

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

このことから、
keykey3をinteface{}型として比較したときにイコールにならなければ衝突しないといえそうです。
その確認コードは上記のコードにすでにあり、イコールにならないのです。

fmt.Println(ikey == ikey3) // false

なぜ、イコールにならないか

もう一歩踏み込んでなぜikey == ikey3がfalseになるのかGolangのspecを確認しながらみていきましょう。

まず、==で比較可能なためには、代入可能性(Assignability)がないといけません。
このあたりのことは前回のGoの代入可能性についてちゃんと理解してみる
に詳しく書いてます。

一部代入可能性の条件を抜粋すると、

A value x is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:
1. x's type is identical to T.
2. x's type V and T have identical underlying types and at least one of V or T is not a defined type.

keykey2は比較不可能である理由は、keykey2ともにdefined typeであるからであるとわかります。

でも、keykey3は比較はできてしまいます。なぜならcontextKey型とstruct{}型はunderlying typeであるstruct{}が同じで、contextKey型はdefined typeでないからです。
そして、構造体の同一性は

Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.

なので、keykey3は比較可能であるだけでなく、イコールになることがわかります。

ただ、実は上記の話は本筋から脱線してます。なぜならValueのコードではinterface{}型同士を比較しているのでした。

interfaceの型同一性は以下でした。

Two interface types are identical if they have the same set of methods with the same names and identical function types. Non-exported method names from different packages are always different. The order of the methods is irrelevant.

当然、interface{}型なので、ikey,ikey2,ikey3ともに型同一で、型同一ならば比較可能です。(ここはちょっと蛇足感はありますが、interface型の型同一性の確認のため引用しました)

では、==でイコールになるのはどのような条件でしょうか。仕様に

Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.

とあります。dynamic typesが同じである必要があるとあります。
typeで型を宣言したものは

The new type is called a defined type. It is different from any other type, including the type it is created from.

とあるように他の型と型同一になることがありません。
contextKeycontextKey2struct{}ともに異なる型となります。
そのため、ikeyikey2ikeyikey3ともにイコールにならないことがわかります。

以上です。

雑感

個人的には、keykey3を比較するとイコールなのに、それらをinterface{}でキャストして比較した途端にイコールにならないというのは自明ではないので新たな発見でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語を1から勉強してアプリを作り、仮想通貨の自動取引で1ヶ月1万円の利益が出た話

概要

元々Javaエンジニアの私がGo言語を1から学習して、仮想通貨の自動売買を行うWebアプリケーションを開発しました。
開発したアプリはクラウド上にデプロイし、1ヶ月弱運用を行った結果、1万円以上の利益が出ました。
その過程を綴った記録になります。

実際に作成したアプリのソースコードは以下
↓↓↓
https://github.com/Kohei-Sato-1221/BitcoinTrading_Golang

Go言語でのアプリ開発をしようと思った経緯

私はJavaがメインスキルだったのですが、「最近Go言語が流行っている!」という話を聞いて、
1から勉強しようと思いました。単に文法を学習するだけだとモチベーションがわかないので、
「GoでWebアプリを作って、Githubに公開する」
を目標に活動をはじめたのです。

なんで仮想通貨の自動取引を選んだのか?

理由は以下です。

  1. 元々仮想通貨やブロックチェーンに興味持っていて、いくつかの取引所にアカウントを保有していた。
  2. 各仮想通貨取引所は売買のためのAPIを豊富に用意していることが多い。
  3. 株などの金融商品に比べてわりと少額から取引できる。bitflyer使えば0.001BTC(執筆時点で約1000円)から注文可能。
  4. ボラティリティ(一日の価格変動)が高い。
  5. 24時間365日取引ができる。

Go言語の文法を学習

文法や環境構築の方法に関しては
ドットインストールの動画
スターティングGo言語の書籍
を使って学習しました。

ドットインストールのGo言語入門の動画に合わせて、実際に手を動かして環境構築や文法を一通り勉強しました。

スターティングGo言語の方がどちらかと言えばリファレンスみたいな使い方をしました。
ちょっと動画ではわかりずらかったり、復習したりしたい場合などは書籍が便利でした。

エディタは無料のVSCodeを使用しました。
デバッグもやりたかったので、GoのプラグインをVScodeに入れました。

Go言語を学習した感想 ~良いところ~

  • シンプルな記述でやりたいことが簡単にできる。数行のコードでWebサーバー作れたりして、すごいなーって思いました。
  • 記述のお作法がGoogle側で指定されていて書き方にあまり困らない。
  • 並列処理が書きやすい(goroutineがめっちゃ便利)

Go言語を学習した感想 ~苦戦したところ~

  • 変数を宣言しても使わないとコンパイルエラーになる。動作確認時とかに不便だった。
  • channel・ポインタの使い方を理解するのに苦労した。
  • 結局GOPATHをどのように設定したら良いかわからなかった(上手くビルドができなくて export GOPATH=xxxxxx を何回も実行した....:joy:)
  • ネットで検索するとき関係ない情報がヒットしがち(ポケモンGOとか笑)
  • Javaと違ってオブジェクト指向じゃないので、ファイルの構成とかどーしたら良いのか悩むことがしばしば...

自動取引アプリの開発に着手

Goの基本をおさえたところで自動売買アプリの開発に着手しました。
この時点で作ろうと思ってたアプリはざっくり以下のような感じ。

  • スケジューラーで定期的に買い注文を入れる。
  • 買い注文が約定したかを定期的にチェックし、約定したら約定価格よりも少し高い価格で売り注文を入れる。
  • 上記のデータをDBで管理し、どれくらいの利益が出たかを日次で把握できるようにする。
  • AWS(EC2) or ラズベリーパイにアプリをデプロイして、運用をしてみる。

基本的にはこのとき思い描いていた通りのアプリが出来上がりました。

取引所の選定

私は複数の仮想通貨取引所のアカウントを持っていたのですが、
このアプリ開発で私はbitflyerを選択しました。

私はZaif、bitbankのAPIを使ってJavaアプリの開発をしたことがあり(※)、APIを使ったことがない取引所を使ってみよう!、という浅い理由でbitflyerを選択しました。

※ Zaif、bitbankのAPIを使って、日次で仮想通貨の積立売買を行うSpringBootのアプリケーションを既に開発済み(https://github.com/Kohei-Sato-1221/CryptoTrading_SpringBoot)

Go言語での開発をスタート

ここからはネットの情報や書籍を利用して、ひたすら開発を進めました。
そこまで苦労した部分はなかったのですが、基本の文法学習では対応できず、ネットで調べて試行錯誤した点は以下です。

(1) configファイルの読み込み
APIキーやDBの接続情報は外部ファイルに記載するようにしたいと思いました。
gopkg.in/ini.v1というライブラリが有効ということで、これを使いました。

(2) Mysqlとの接続
github.com/go-sql-driver/mysqlというドライバーが有名らしいので採用しました。
JavaではもちろんDBの連携をやったことがあるのですが、ちょっと勝手が違う部分もあり、試行錯誤に苦労しました。

(3) 取引所APIのリクエスト
取引APIなどのAPIキーが必要なPrivateAPIを使う場合は、HTTPリクエストヘッダに認証情報を入れないといけません。
bitflyerの場合だと

"ACCESS-KEY":APIキー,
"ACCESS-TIMESTAMP": タイムスタンプ,
"ACCESS-SIGN": ※bodyやタイムスタンプを含めた値をHash化して、キーでSignした値
"Content-Type":"application/json"

が必要になります。
ACCESS-SIGNの値が少しでも間違うと認証されないので、試行錯誤して正しいリクエストを出せるようにしました。

HTTPリクエスト作成、HASH化、SIGNなどは基本的にGoのライブラリでカバーできるので、Go特有の部分での苦労はありませんでした。

自動取引アプリの構成図

最終的には以下のようなアーキテクチャのアプリが完成しました。
スクリーンショット 2020-05-17 11.12.14.png

【TABLE】
(1)買い注文テーブル
買い注文の取引ID、金額、数量、ステータスを管理するテーブルです。

ステータスに関しては、「未約定」、「約定済み」、「売り注文発注済み」の3つのステータスがあります。
注文情報同期JOBにて買い注文の情報をInsertします。この段階ではステータスは「未約定」、
約定確認JOBで買い注文の約定が確認されれば、ステータスは「約定済み」に変更、
売り注文JOBで特定の買い注文に対応する売り注文が発注された場合、ステータスを「売り注文発注済み」に変更します。

(2)売り注文テーブル
売り注文の取引ID、金額、数量、ステータス、親取引IDを管理するテーブルです。

ステータスに関しては「未約定」「約定済み」の2つで、約定確認JOBでステータス変更します。

親取引IDは該当の売り注文に対応する買い注文の取引ID、になります。
買い注文の金額に+αした金額の売り注文を出す、という仕組みなので、このようなカラムを追加しています。

【JOB】
(1)買い注文JOB
一日数回、特定の時間に買い注文を発注するJOBです。成行ではなく指値での注文です。

発注する価格に関しては、
1. 現在価格のxx%低い価格
2. 現在価格と、24時間内の最低価格の平均値
などシンプルなロジックで実装していました。

発注する数量、金額、頻度は運用しながら調整しました。
最終的には以下の通りになりました。

  • 大体2時間おきに買い注文を入れる
  • 注文する通貨は、BTCとETHの現物(レバレッジ取引はやらない)
  • BTCの1回あたり発注数量は0.006BTC or 0.009BTC
  • ETHの1回あたり発注数量は0.2ETH or 0.3ETH

(2)売り注文JOB
買い注文テーブルから「約定済み」のステータスの買い注文の情報一覧を取得します。
その後、bitflyerのAPIを叩いて売り注文を入れます。

売り注文を入れた後、買い注文テーブルのステータスupdate及び、売り注文テーブルに注文情報のinsertを行います。

売り注文の数量は、元々の買い注文の数量と同じで、価格に関しては買い注文価格の+1.5%としました。
この1.5%という数字に明確な根拠はないのですが、日々BTCやETHのチャートを見ていると、数%の上昇・下落が頻繁に見られるので、1.5%くらいならすぐ約定するかな、という想定で決めました。実際に運用してみると想定通りには売り注文が約定しているように思います。

実行頻度は45秒に1回としました。

(3)注文情報同期JOB
買い注文の情報をbitflyerから取得し、買い注文テーブルへデータを同期するJOBです。
この時、買い注文JOBで入れた注文かどうかは区別しません。つまり、手動で入れた買い注文も同期の対象としました。(この仕様にした理由は後述します)

実行頻度は40秒に1回としました。

(4)約定確認JOB
買い注文・売り注文が約定しているかどうかをbitflyerから取得し、買い注文テーブル・売り注文テーブルのステータスを変更します。

実行頻度は45秒に1回としました。

運用実績(2020年4月20日~5月17日)

2020年4月20日から、1ヶ月弱、運用を行ってみました(実際はもっと前から運用していたのですが、利益管理機能を実装していなかったので、2020年4月20日からのデータしか存在しないのです:sweat_smile:

運用実績は以下です(1日あたりの利益は、売り注文が約定した日付でカウントしています)

合計利益:1万6,765円 一日平均:605円

スクリーンショット 2020-05-17 13.51.36.png

スクリーンショット 2020-05-17 13.51.44.png

日付 1日あたり利益(円) 累計(円)
04/20 185.92 185.92
04/21 140.72 326.64
04/22 410.94 737.58
04/23 855.04 1,592.62
04/24 67.15 1,659.77
04/25 130.97 1,790.74
04/26 288.46 2,079.2
04/27 98.28 2,177.48
04/28 161.21 2,338.69
04/29 703.34 3,042.03
04/30 2,223.58 5,265.61
05/01 330.82 5,596.43
05/02 225.37 5,821.8
05/03 244.29 6,066.09
05/04 757.07 6,823.16
05/05 348.14 7,171.3
05/06 277.91 7,449.21
05/07 1,037.3 8,486.51
05/08 685.99 9,172.5
05/09 63.02 9,235.52
05/10 1,953.49 11,189.01
05/11 1,381.77 12,570.78
05/12 731.35 13,302.13
05/13 379.61 13,681.74
05/14 1,374.74 15,056.48
05/15 618.21 15,674.69
05/16 1,090.55 16,765.24
1日平均 605.18

結果的に28日間で1万5000円以上の利益が出ました。1日あたりの利益も600円超とワンコインを超えました。
毎日軽いランチ一食分は浮いてたなー、という気分です。

しかし、本当はもっと利益が出る可能性がありました。というのもの、途中で買い注文の発注数量を増やしたためです。最終的には「自動取引アプリの構成図」で説明した通りの数量になっているのですが、4月20日の時点での購入数量はもっと少なかったのです。

一ヶ月弱運用してみて、結構安定的に利益が出ていることがわかったので、今後はさらに発注数量や買い注文を入れる頻度を高めたいと思います。(単純に購入する数量を2倍にしていれば、利益も2倍になったはずですので、、、、)

工夫した点

(1) Goのプロセスが落ちる
原因が不明なのですが、Goのプロセスが落ちていることがたまーにありました。
Goに対する経験不足で、原因がわからないという状況でした。
苦肉の策として、
「プロセスが動いているか監視して、落ちていたらアプリを再起動する」
というシェルスクリプトを作り、cronで1時間に1回実行するようにしました。

しかし、これは根本的な原因解決ではないので、もう少しGOに関する知見を深め、停止しないようなアプリに改修できればと思っています。

(2) APIの呼び出し制限
bitflyerのAPIでは、Private APIの呼び出し回数が5分間で500回までと制限があるようです。
実装しはじめの頃はこの点を完全に無視して、APIリクエストを投げまくっていたため、注文情報の確認JOBなどで急にエラーが発生する事態が起きました。

注文情報等を取得する際はなるべく一括で取得(個別の注文IDに対してリクエストを一回一回投げない)

JOBの頻度を減らす

といった手段で解決を図りました。

(3) マニュアルで買い注文いれた際の挙動
基本は

JOBによる買い注文 → 買い注文の約定チェック → 約定したら売り注文

をひたすら繰り返すアプリなのですが、買い注文を手動で入れた場合も、約定チェックの対象としたい
と考えました。

つまり、bitflyerの注文画面から買い注文を入れた場合でも、その情報をDBに格納し、その注文を約定確認JOBの対象として、約定後にJOBで売り注文を発注するようにしたい、ということです。

アプリの初回の実装時は、買い注文JOBにてDBに買い注文のデータをinsertするようにしていたのですが、その方法だとJOBで入れた買い注文のみがDBに登録されます。この仕組みを変更し、定期的に注文情報をDBに同期する「注文情報同期JOB」を新規実装することで対応しました。

今後の課題

  • テクニカル分析を用いて売買のタイミング等を判別したい。
  • レバレッジ取引に挑戦(現状は現物取引のみ)
  • バックテストの機能を実装したい。→ 新しい売買ロジック作ったときにシュミレーションできるように。
  • 他の取引所(海外取引所を含む)を使って幅広くAPI取引。→ 最近ではOKEXという海外取引所のAPIを呼び出す機能も実装してみました。
  • 通貨価格が高騰・暴落した際に、買い注文・売り注文が約定せずにずっと残ってしまう。なんらかの対応を考えたい、、、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

11rep - contextパッケージでテンプレ

contextパッケージとは何か

  • Context は、APIのサーバーやクライアントを使うときに、コンテキストを提供してキャンセルや、タイムアウト、値を渡したり出来る仕組み

流れ

  • (*sql.DB)Begin()でトランザクションを開始
  • (*sql.Tx)Commit()でコミット、(*sql.Tx)RollBack()でロールバック
package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "context"
    "time"
)

func main() {
    db, _ := sql.Open("mysql", "user:password@tcp(host:port)/dbname")

    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, 1*time.Second)

    tx, error := db.Begin()
    if error != nil {
        log.Fatal(error)
        return
    }

    _, err := db.ExecContext(ctx, {Query})
    if err != nil {
      defer cancel()
      tx.Rollback()
    } else {
      tx.Commit()
    }
}

  • defer cancel() 上記はタイムアウトの実装
  • 1秒以上かかる場合はDone
  • TimeOutの設定は下記

Microsecond = 1000 * Nanosecond

Millisecond = 1000 * Microsecond

Second = 1000 * Millisecond

Minute = 60 * Second

Hour = 60 * Minute

具体的な利用例からcontextとは何かを説明

  • Goの典型的な利用例であるWebアプリケーションを考える
  • Goのサーバにおいてリクエストはそれぞれ個別のgoroutineで処理される
  • そしてリクエストHandlerは新たなgoroutineを生成しバックエンドのDBや別のサーバにリクエストを投げ結果を得てユーザに対してレスポンスを返す.

最初に注意するべきはその処理に適切なTimeoutやDeadlineを設定して処理が停滞するのを防ぐことである

  • 例えば別のサーバにリクエストを投げる場合にネットワークの問題でリクエストに時間がかかってしまうことは大いに考えられる.リクエストにTimeoutを設定して早めにレスポンスを返しリトライを促すべきである.

次に注意するべきは生成したgoroutineを適切にキャンセルしリソースを解放することである.(※Must)

  • 例えば別のサーバにリクエストを投げる場合に適切なキャンセル処理を行わないとTimeout後もネットワークリソースが使われ続けることになる(CPUやメモリを使い続けるかもしれない)
  • この場合net/httpパッケージレベルでリクエストをキャンセルするべきである.
  • さらにそのgoroutineは別のgoroutineを呼び出しそれがまた別の…と呼び出しの連鎖は深くなることが考えられる
    • その場合も親のTimeoutに合わせてその子は全て適切にキャンセルされリソースは解放されるべきである > contextパッケージはこのキャンセルのためのシグナルをAPIの境界を超えて受け渡すための仕組みである.ある関数から別の関数へと,親から子へと,キャンセルを伝搬させる.

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang】for文

【Golang】for文

Golangの基礎学習〜Webアプリケーション作成までの学習を終えたので、復習を兼ねてまとめていく。 基礎〜応用まで。

package main
//for

import "fmt"


func main() {
    //パターン1
    //for 初期化、 条件、 繰り返し式
    for i := 0; i < 100; i++{
        if i == 3 {
            fmt.Println(3)
            //繰り返し forの中に戻る
            continue
        }
        if i > 30 {
            fmt.Println("抜けた")
            //強制終了
            break
        }
        fmt.Println("30まで")
    }
    //パターン2
    //書き方、別パターン
    sum := 1
    for sum < 10 {
        sum += sum
        fmt.Println(sum)
    }
    fmt.Println(sum)

    //無限ループ
    /*for {
        fmt.Println("hello")
    }*/





    //range
    //配列、スライス、マップ
    /*arr := [3]int{1,2,3}
    slise := []int{4,5,6}
    map1 := map[string]int{"AAA":1, "BBB":2}
    fmt.Println(arr)
    fmt.Println(slise)
    fmt.Println(map1)

    for i := range arr {
        fmt.Println(arr[i])
    }*/

    //スライス
    l := []string{"python", "go", "java"}

    //中身を取り出す。長さ分になるまで
    for i := 0; i < len(l); i++ {
        fmt.Println(i, l[i])
    }
    /*
    0 python
    1 go
    2 java
    */

    //長さ分繰り返す
    for i, v := range l {
        fmt.Println(i, v)
    }
    /*
    0 python
    1 go
    2 java
    */

    //index省略
    for _, v := range l {
        fmt.Println(v)
    }
    /*
    python
    go
    java
    */

    //map
    m := map[string]int{"apple": 100, "banana": 200}

    //map分繰り返す
    for k, v := range m {
        fmt.Println(k, v)
    }
    /*
    apple 100
    banana 200
    */

    //値省略
    for k := range m {
        fmt.Println(k)
    }
    /*
    banana
    apple
    */

    //key省略
    for _, v := range m {
        fmt.Println(v)
    }
    /*
    100
    200
    */
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TinyGo で goroutine + pin interrupts を使う

TinyGo を使うと Go でマイコン開発ができます。
組込ソフトで goroutine + pin interrupts が使ってみたので紹介します。
(個人的には) 気持ちよく記載することができます。

完成品

  • 開始時にスクロールで TinyGo demo と表示
  • 以降ループ
    • 77ms 毎に白い LED を点滅、 LCD 左下のカウンタを更新
    • 500ms 毎に LCD 右下のカウンタを更新
    • LCD の 1 行目にボタン状態を表示
    • PyPortal 本体の LED にボタン状態を表示

環境

goroutine 部分

以下のように go timer77ms() などにより 3 つの goroutine を起動しています。
goroutine からは chan 経由で for / select に処理を依頼します。

aqm0802 (I2C 接続のキャラクタ液晶) に対しては単一の main() 関数のみからアクセスするため、安全に更新できます。

Go らしいすっきりとした記載になっていて、個人的には非常に書きやすいです。

func main() {
    // 省略

    go timer77ms(chCnt1, led2)
    go timer500ms(chCnt2)
    go disp(chBtn, chDisp, led1)

    for {
        select {
        case d := <-chDisp:
            aqm0802.SetCursor(0, 0)
            aqm0802.Print(d.String())

        case cnt := <-chCnt1:
            aqm0802.SetCursor(0, 1)
            aqm0802.Print(fmt.Sprintf("%4d", cnt))

        case cnt := <-chCnt2:
            aqm0802.SetCursor(4, 1)
            aqm0802.Print(fmt.Sprintf("%4d", cnt))
        }
        time.Sleep(1 * time.Millisecond)
    }
}

pin interrputs 部分

※2020/05/17 時点では、まだ merge されていないので注意が必要です
※API も変更になるかもしれません

以下の関数は、入力 pin (実際のマイコンの足に対応する) を引数にとって、 pin の値が変わった時に通知する chan を返す という動きをします。

pin interrupts では、割込発生時には func(p machine.Pin) がコールされます。
この func(p machine.Pin) は、割込にて処理されるため長時間ブロックするような処理を書いてはいけません。
後述のコード例では ch <- b している個所について select を使っていますが、万が一の際にブロックしないようにするためです。
また、 ch := make(chan bool, 3) により少しバッファを持たせているのもブロック対策です。
空きメモリに合わせて多めに取っておくのは一つの手ですし、チャタリングしないようにしておくのも良いです。

内部で var state chvolatile.Register8 という見慣れない表記があります。
その中では p により、該当ピンの情報を知ることができますが、該当ピンの状態は (処理するまでに) 既に更新されている可能性があります。
なので、ソフト的に処理するようにしています。
#割込が発生しない場合があると、内部状態が食い違ってしまいますが・・・

func pinInterruptChan(pin machine.Pin) <-chan bool {
    var state volatile.Register8
    ch := make(chan bool, 3)

    pin.SetInterrupt(machine.PinToggle, func(p machine.Pin) {
        b := false
        if state.Get() != 1 {
            state.Set(1)
            b = true
        } else {
            state.Set(0)
        }
        select {
        case ch <- b:
        default:
        }
    })

    return ch
}

まとめ

TinyGO で goroutine + pin interrupts を使ってみました。
ソースコードは以下にあります。
現状は、 pin interrupts がマージされていないので、開発版の tinygo を自分でビルドする必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む