20200112のGoに関する記事は12件です。

MarkdownでAA入力を対応してみた 【ぼくのがんがえたさいきょうの掲示板】

個人で作っている掲示板サイトがある。最近日本語対応を実装しているが、途中であることに気づいてしまった。

「せっかく和訳したところでAA貼れないなら意味ない!」

AAは日本語の掲示板の不可欠な文化である。しかし現代のパソコンやスマホではAAが正しく表示されない場合が多い。これはどうにか解決したい。

やりたいこと

私の掲示板はMarkdown記法で書き込む形だ。AAをそのまま入力すると、太字として認識されたり自動的に改行が入ったりする。2chの専ブラみたいにいい感じにAAを表示したい。

<aa>(´・ω・`)</aa>

自動的にAAを認識するのは大変そうなので、AAタグを用意する。AAタグの中身をAA用のフォントに変えて、Markdown記法の*bold*などを無視する。

無理矢理AAタグを対応する

利用しているMarkdownライブラリはrussross/blackfridayだ。諸々があって今だにv1を使ってる。
このライブラリはHTMLの入力を対応しているが、中身をMarkdown記法としてパースしてしまう。

まず、フォークしよう。そして、markdown.goblockTagsというマップを見てみる。

markdown.go
// blockTags is a set of tags that are recognized as HTML block tags.
// Any of these can be included in markdown text without special escaping.
var blockTags = map[string]struct{}{
    "blockquote": {},
    "del":        {},
    "div":        {},
    // ...
    "aa":         {}, // ← 追加した
}

blockTagsに入っているタグはブロック(パラグラフ)になり、エスケープされないとコメントが丁寧に教えてくれた。よし、"aa": {},を追加する。これで<aa>の中身はMarkdownにならずそのまま出力される。

XSS対策としてmicrocosm-cc/bluemondayを使っている。blackfridayでMarkdownをHTMLに変換した後に、bluemondayでsanitizeをかける。bluemondayにAAタグを許可する必要がある。

var sanitizer *bluemonday.Policy

func init() {
    sanitizer = bluemonday.UGCPolicy() // よくある書式設定用のタグなどを許可
    sanitizer.AllowNoAttrs().OnElements("aa") // 属性なしの<aa>を許可
}

こうしないと<aa>が丸ごと消されてしまう。

しかし非公式のHTMLタグをそのまま出力するのは気持ち悪いので、<aa><p class="aa">に変えよう。PuerkitoBio/goqueryはHTMLをいじるのに便利だ。

上記の処理を組み合わせるとこうなる。

func renderMarkdown(content string) template.HTML {
    // Markdownをrender
    renderer := blackfriday.HtmlRendererWithParameters(commonHtmlFlags, "", "", blackfriday.HtmlRendererParameters{
        // ...
    })
    md := blackfriday.MarkdownOptions([]byte(content), renderer, blackfriday.Options{
        // ...
    })
    // XSS対策としてsanitize
    sanitized := sanitizer.Sanitize(string(md))

    // goqueryを使ってHTMLをいじる
    doc, err := goquery.NewDocumentFromReader(strings.NewReader(sanitized))
    if err != nil {
        panic(err)
    }
    // <aa>を<p class="aa">に変換
    doc.Find("aa").Each(func(_ int, sel *goquery.Selection) {
        sel.SetAttr("class", "aa")
        sel.Nodes[0].Data = "p"
    })

    // <html><body>が勝手に追加されるので省略
    html, err := doc.Find("body").Html()
    if err != nil {
        panic(err)
    }
    return template.HTML(html)
}

※ 本当はpanicせずにerrorを返すべき

たぶん、こういう処理はmarkdownのライブラリの中でやるべきだが、blackfridayのコードはヤバイのでこっちの方が楽である。実際のコードでは<aa>の対応だけじゃなくてYouTubeのembedなどは似た手段で対応している。

AAをいい感じに表示する

@scrpgilさんは素敵なAAフォントのまとめを提供してくれているので参考にした。めっちゃ助かる。ありがとうございます!

aahub_light1というフォントは軽くて綺麗なので使うことにした。woffファイルをS3にアップてして、キャッシュするようにCache-Controlヘッダをpublic, max-age=31536000, immutableにした。AA用のCSSも作った。これも永遠にキャッシュかけたいのでメインのCSSと分けてCache-Controlを指定した。

aa.css
@font-face {
  font-family: "aahub_light";
  src: url("[CDNのURL]/aahub_light.woff") format("woff");
  font-display: swap;
}

p.aa {
  font-family: "aahub_light";
  white-space: pre;
  overflow: scroll;
  word-break: keep-all;
  overflow-wrap: normal;
  /*font-size: 16px;*/
  /*line-height: 18px;*/
}

スマホなどで大きいAAがoverflowしがちなのでoverflow: scrollにした。もっとも正しい表示の仕方はfont-sizeline-heightを指定する必要があるがスマホだと大きすぎるのでとりあえず消した。それでもなんとなく大丈夫だった。

結果

Screen Shot 2020-01-12 at 22.34.31.png

スマホでもAAはいい感じに表示される。やったぜ!

「ぼくのかんがえたさいきょうの掲示板」をシリーズとしてまた記事を書きたいと思うので次もよろしくお願いします!

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

MarkdownでのAA入力を対応してみた 【ぼくのがんがえたさいきょうの掲示板】

個人で作っている掲示板サイトがある。最近日本語対応を実装しているが、途中であることに気づいてしまった。

「せっかく和訳したところでAA貼れないなら意味ない!」

AAは日本語の掲示板の不可欠な文化である。しかし現代のパソコンやスマホではAAが正しく表示されない場合が多い。これはどうにか解決したい。

やりたいこと

私の掲示板はMarkdown記法で書き込む形だ。AAをそのまま入力すると、太字として認識されたり自動的に改行が入ったりする。2chの専ブラみたいにいい感じにAAを表示したい。

<aa>(´・ω・`)</aa>

自動的にAAを認識するのは大変そうなので、AAタグを用意する。AAタグの中身をAA用のフォントに変えて、Markdown記法の*bold*などを無視する。

無理矢理AAタグを対応する

利用しているMarkdownライブラリはrussross/blackfridayだ。諸々があって今だにv1を使ってる。
このライブラリはHTMLの入力を対応しているが、中身をMarkdown記法としてパースしてしまう。

まず、フォークしよう。そして、markdown.goblockTagsというマップを見てみる。

markdown.go
// blockTags is a set of tags that are recognized as HTML block tags.
// Any of these can be included in markdown text without special escaping.
var blockTags = map[string]struct{}{
    "blockquote": {},
    "del":        {},
    "div":        {},
    // ...
    "aa":         {}, // ← 追加した
}

blockTagsに入っているタグはブロック(パラグラフ)になり、エスケープされないとコメントが丁寧に教えてくれた。よし、"aa": {},を追加する。これで<aa>の中身はMarkdownにならずそのまま出力される。

XSS対策としてmicrocosm-cc/bluemondayを使っている。blackfridayでMarkdownをHTMLに変換した後に、bluemondayでsanitizeをかける。bluemondayにAAタグを許可する必要がある。

var sanitizer *bluemonday.Policy

func init() {
    sanitizer = bluemonday.UGCPolicy() // よくある書式設定用のタグなどを許可
    sanitizer.AllowNoAttrs().OnElements("aa") // 属性なしの<aa>を許可
}

こうしないと<aa>が丸ごと消されてしまう。

しかし非公式のHTMLタグをそのまま出力するのは気持ち悪いので、<aa><p class="aa">に変えよう。PuerkitoBio/goqueryはHTMLをいじるのに便利だ。

上記の処理を組み合わせるとこうなる。

func renderMarkdown(content string) template.HTML {
    // Markdownをrender
    renderer := blackfriday.HtmlRendererWithParameters(commonHtmlFlags, "", "", blackfriday.HtmlRendererParameters{
        // ...
    })
    md := blackfriday.MarkdownOptions([]byte(content), renderer, blackfriday.Options{
        // ...
    })
    // XSS対策としてsanitize
    sanitized := sanitizer.SanitizeBytes(md)

    // goqueryを使ってHTMLをいじる
    doc, err := goquery.NewDocumentFromReader(bytes.NewReader(sanitized))
    if err != nil {
        panic(err)
    }
    // <aa>を<p class="aa">に変換
    doc.Find("aa").Each(func(_ int, sel *goquery.Selection) {
        sel.SetAttr("class", "aa")
        sel.Nodes[0].Data = "p"
    })

    // <html><body>が勝手に追加されるので省略
    html, err := doc.Find("body").Html()
    if err != nil {
        panic(err)
    }
    return template.HTML(html)
}

※ 本当はpanicせずにerrorを返すべき

たぶん、こういう処理はmarkdownのライブラリの中でやるべきだが、blackfridayのコードはヤバイのでこっちの方が楽である。実際のコードでは<aa>の対応だけじゃなくてYouTubeのembedなどは似た手段で対応している。

AAをいい感じに表示する

@scrpgilさんは素敵なAAフォントのまとめを提供してくれているので参考にした。めっちゃ助かる。ありがとうございます!

aahub_light1というフォントは軽くて綺麗なので使うことにした。woffファイルをS3にアップてして、キャッシュするようにCache-Controlヘッダをpublic, max-age=31536000, immutableにした。AA用のCSSも作った。これも永遠にキャッシュかけたいのでメインのCSSと分けてCache-Controlを指定した。

aa.css
@font-face {
  font-family: "aahub_light";
  src: url("[CDNのURL]/aahub_light.woff") format("woff");
  font-display: swap;
}

p.aa {
  font-family: "aahub_light";
  white-space: pre;
  overflow: scroll;
  word-break: keep-all;
  overflow-wrap: normal;
  /*font-size: 16px;*/
  /*line-height: 18px;*/
}

スマホなどで大きいAAがoverflowしがちなのでoverflow: scrollにした。もっとも正しい表示の仕方はfont-sizeline-heightを指定する必要があるがスマホだと大きすぎるのでとりあえず消した。それでもなんとなく大丈夫だった。

結果

Screen Shot 2020-01-12 at 22.34.31.png

スマホでもAAはいい感じに表示される。やったぜ!

「ぼくのかんがえたさいきょうの掲示板」をシリーズとしてまた記事を書きたいと思うので次もよろしくお願いします!

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

sqlxでのselect例

sqlxでのselect例

Select

    import (
        "fmt"

        "github.com/jmoiron/sqlx"
        _ "github.com/go-sql-driver/mysql"
    )

    type Hoge {
        ID int `db:"id"`
        Name string `db:"name"`
    }

    tx, err := db.Beginx()
    if err != nil {
        fmt.Println(err)
    }

    query := `
SELECT
    *
FROM
    hoge
WHERE
    name in (?)
    `

    names := []string{"foo", "bar"}
    query args, err := sqlx.In(query, names)
    if err != nil {
        fmt.Println(err)
    }

    query = tx.Rebind(query)

    stmt, err := tx.Preparex(query)
    if err != nil {
        fmt.Println(err)
    }

    var hoges []Hoge
    err = stmt.Select(&hoge, args...)
    if err != nil {
        fmt.Println(err)
    }

    .
    .
    .

    err = tx.Commit()
    if err != nil {
        fmt.Println(err)
    }

Get

    import (
        "fmt"

        "github.com/jmoiron/sqlx"
        _ "github.com/go-sql-driver/mysql"
    )

    type Hoge {
        ID int `db:"id"`
        Name string `db:"name"`
    }

    tx, err := db.Beginx()
    if err != nil {
        fmt.Println(err)
    }

    stmt, err := tx.PrepareNamed(`
SELECT
    *
FROM
    hoge
WHERE
    name = :name
`)
    if err != nil {
        fmt.Println(err)
    }

    args := map[string]interface{}{
        "name": "foo"
    }

    var hoge Hoge
    err = stmt.Get(&hoge, args)
    if err != nil {
        fmt.Println(err)
    }

    .
    .
    .

    err = tx.Commit()
    if err != nil {
        fmt.Println(err)
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

眺めて覚えるGo言語 その12 Map & Json

json文字列([]byte)を定義してvar adrs []map[string]interface{}形式にマーシャルします。

json.go
package main
import (

    "encoding/json"
    "fmt"
    "log"
)
func main() {
b:=[]byte(`[{
"ID":1,
"Name":"関波子",
"Furigana":"セキナミコ",
"Sex":"女",
"Blood":"AB",
"Date":"1980/07/31",
"Tel":"03-3060-4716",
"MTel":"090-7787-3784",
"Mail":"sk@eaccess.net"
},
{
"ID":2,
"Name":"小倉準司",
"Furigana":"コクラジュンジ",
"Sex":"男",
"Blood":"A",
"Date":"1973/10/08",
"Tel":"0166-36-3522",
"MTel":"",
"Mail":"junzi-kokura@eaccess.net"
},
{
"ID":3,
"Name":"西村有紀子",
"Furigana":"ニシムラユキコ",
"Sex":"女",
"Blood":"O",
"Date":"1972/12/05",
"Tel":"0745-67-2723",
"MTel":"090-5165-2074",
"Mail":"okikuy1972@livedoor.com"
}
]`)

    var adrs []map[string]interface{}
    if err := json.Unmarshal(b, &adrs); err != nil {
        log.Fatal(err)
    }
    for _,a := range adrs {
        for k,v :=range a{
            fmt.Println(k,v)
        }
        fmt.Println("----------------------------------------------")
    }
}
実行結果
go run json.go
Name 関波子
Sex 
Blood AB
Date 1980/07/31
ID 1
Furigana セキナミコ
Tel 03-3060-4716
MTel 090-7787-3784
Mail sk@eaccess.net
----------------------------------------------
ID 2
Tel 0166-36-3522
MTel
Mail junzi-kokura@eaccess.net
Name 小倉準司
Furigana コクラジュンジ
Sex 
Blood A
Date 1973/10/08
----------------------------------------------
Name 西村有紀子
Furigana ニシムラユキコ
Date 1972/12/05
Mail okikuy1972@livedoor.com
ID 3
Blood O
Tel 0745-67-2723
MTel 090-5165-2074
Sex 
----------------------------------------------

日本語jsonでも動作します。

jp_json.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
)
func main() {
b:=[]byte(`[{
    "ID":1,
    "名前":"関波子",
    "名前フリガナ":"セキナミコ",
    "性別":"女",
    "血液型":"AB",
    "生年月日":"1980/07/31",
    "電話番号":"03-3060-4716",
    "携帯番号":"090-7787-3784",
    "メール":"sk@eaccess.net"
    },
    {
    "ID":2,
    "名前":"小倉準司",
    "名前フリガナ":"コクラジュンジ",
    "性別":"男",
    "血液型":"A",
    "生年月日":"1973/10/08",
    "電話番号":"0166-36-3522",
    "携帯番号":"",
    "メール":"junzi-kokura@eaccess.net"
    },
    {
    "ID":3,
    "名前":"西村有紀子",
    "名前フリガナ":"ニシムラユキコ",
    "性別":"女",
    "血液型":"O",
    "生年月日":"1972/12/05",
    "電話番号":"0745-67-2723",
    "携帯番号":"090-5165-2074",
    "メール":"okikuy1972@livedoor.com"
    }
]`)
    var adrs []map[string]interface{}
    if err := json.Unmarshal(b, &adrs); err != nil {
        log.Fatal(err)
    }
    for _,a := range adrs {
        for k,v :=range a{
            fmt.Println(k,v)
        }
        fmt.Println("----------------------------------------------")
    }
}

実行結果
>go run jp_json.go
名前フリガナ セキナミコ
生年月日 1980/07/31
電話番号 03-3060-4716
メール sk@eaccess.net
ID 1
名前 関波子
性別 
血液型 AB
携帯番号 090-7787-3784
----------------------------------------------

C:\Users\hirat\go-work\go-json>go run ex3.go
ID 1
性別 
血液型 AB
生年月日 1980/07/31
携帯番号 090-7787-3784
メール sk@eaccess.net
名前 関波子
名前フリガナ セキナミコ
電話番号 03-3060-4716
----------------------------------------------
ID 2
名前フリガナ コクラジュンジ
血液型 A
電話番号 0166-36-3522
携帯番号
メール junzi-kokura@eaccess.net
名前 小倉準司
性別 
生年月日 1973/10/08
----------------------------------------------
血液型 O
生年月日 1972/12/05
電話番号 0745-67-2723
メール okikuy1972@livedoor.com
携帯番号 090-5165-2074
ID 3
名前 西村有紀子
名前フリガナ ニシムラユキコ
性別 
----------------------------------------------

注意

type Adr struct {
    ID        int
    名前       string
    名前フリガナ string
    性別       string
    血液型     string
    生年月日   string
    電話番号   string
    携帯番号   string
    メール    string
}

上記のようなstructを定義して

        var adrs []Adr
    if err := json.Unmarshal(bytes, &adrs); err != nil {
        log.Fatal(err)
    }

を実行すると文字通りUnmarshal(無法者)の結果が得られます。

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

Golangでの0-1ナップサック問題の実装

Goで0-1ナップサック問題を解く

ナップサック?ナップザック?どちらも見かけるような気がします.
英語ではKnapsack(nˈæpsæk)なのでサックなような気がします.

目的・理由

最近仕事でナップサック問題で解決出来る問題と遭遇した.
仕事全体の中では非常に小さい問題ではあったがアルゴリズムが直接役に立ったことを大変嬉しく思っており,記念に初めて記事を書く.

Goで書くのはGoを書きたいというささやかで非常に素朴なアピールである.

想定読者

未来のわたし

ナップサック問題とは

ナップサック問題には制約によって様々な解法パターンがあるが今回の対象となるのは,もっともオーソドックスな0-1ナップサック問題と呼ばれるケース.
以下に簡単な問題設定を記載する.

容量$W$のナップサック1つと,$N$個の荷物$a_i (1\le i\le N)$がある.
各荷物には重さ$w_i$と価値$v_i$が設定されており,$N$個の荷物からいくつかを選びナップサックに詰め込んでいく.
ただし,ナップサックに入れる荷物の重さの総和は容量$W$以下とならなければならない.
上記の条件を満たすとき一度にナップサックに詰め込める価値の総和の最大値を求める.

各荷物については

  • ナップサックに入れる
  • ナップサックに入れない

の2通りであるから,0-1ナップサック問題と呼ばれる.これは組合わせ最適化の問題でもある.
これを全通り試そうとすると$2^N$通りのパターンがあり,ナイーブに実装すると$N=40$程度でも現実的な時間では解くことはできない.そこで以下の擬似多項式時間で解くことが可能な動的計画法がよく使われる.

動的計画法による解法

ナップサック問題は多くの場合動的計画法で解かれる.詳細は後回しにするが,先に結果を述べる.

$a_0$から$a_i (0\le i\le N)$までを使って(荷物$a_0$は荷物なしを意味する)容量$j$のナップサックに荷物を詰め込むとき詰め込める価値の総和の最大値を$dp_{i, j} (1\le i\le N, 0\le j \le W)$であるとする.このとき,$dp_{i, j}$は以下の式で与えられる.この$dp_{N, W}$が求めたいナップサック問題の解,容量Wのナップサックに詰め込める価値の総和の最大値である.

\begin{aligned}
    dp_{i, j} = \begin{cases}
&\mathrm{max}(dp_{i-1, j}, dp_{i-1, j-w_i}+v_i)\hspace{20px} &(i\gt0, j \ge w_i)\\
    & dp_{i-1, j} &(i\gt0, j \lt w_j)\\
    & 0 &(i=0)
    \end{cases}
\end{aligned}\\

上記の式では,問題サイズを小さくした部分問題の解を記録しそれを利用して解を求めている.
これをもう少しみていく.

${dp}_{i, j}$は荷物$a_i$まで使って容量$j$のナップサックに詰め込める価値の総和の最大値である.

このとき容量$j$のナップサックに荷物$a_{i}$を入れることを考える.
$w_i > j$であるならば,荷物$a_i$を入れることはできないため,入れられる価値の総和の最大値は$a_1$から$a_{i-1}$を使って容量$j$のナッサックにいれられる価値の最大値($=dp_{i-1,j}$)と等しい.
一方で$w_i \le j$である場合は,$a_i$を入れるか入れないかの2パターンが存在しどちらかがこの場合における最大値となる.

まず,入れないときは先ほどと同様に$a_1$から$a_{i-1}$を用いて得られる最大値が値となる.つまり$dp_{i-1, j}$となる.
入れる場合は,$j-w_i$の容量のナップサックに$a_1$から$a_{i-1}$を使ってナップサックに入れられる価値の最大値に$a_i$の価値$v_i$を足したものが値となる.つまり$dp_{i-1, j-w_i} + v_i$となる.
これら2つの値を比較して価値が大きいものを選ぶことになる.

これらを数式にすると上述の更新式になる.初期値として,$i=0$つまり荷物を全く使わないで得られる価値を初期条件としてやると求めたい$dp_{i,j}$を更新式を辿っていけば求めることができる.

計算量としては,時間計算量および空間計算量ともに$\mathrm{O}(NW)$ となる.
空間計算量については,長さ$M+1$の配列2つを再利用すれば$\mathrm{O}(W)$とすることも可能になる.

実装

ちょうどAtCoderのDPまとめコンテスト0-1ナップサック問題があったので,コードの検証および入手力形式はこちらに準じることにした.
このコードでACを取れること確認した.

package main

import (
    "fmt"
    "math"
)

// mathパッケージのMaxはfloat型しかないためint用にラップしておく
func max(lhs, rhs int) int {
    return int(math.Max(float64(lhs), float64(rhs)))
}

func main() {
    var (
        N, M int64
    )
    // 入力
    fmt.Scanf("%d %d", &N, &M)
    values, weights := make([]int64, N), make([]int64, N)
    for i := 0; i < N; i++ {
        fmt.Scanf("%d %d", &weights[i], &values[i])
    }
    // 32bitだとオーバーフローする場合があるので64bit
    // Goはデフォルトで0で初期化される.
    dp := make([][]int64, N+1)
    for i := 0; i < N+1; i++ {
        dp[i] = make([]int64, M+1)
    }
    // 動的計画法部分
    // 緩和式にしたがって値を更新していく
    for i := 1; i <= N; i++ {
        for j := int64(0); j <= M; j++ {
            dp[i][j] = dp[i-1][j]
            if j >= weights[i-1] {
                dp[i][j] = max(dp[i][j], dp[i-1][j-weights[i-1]]+values[i-1])
            }
        }
    }
    // 出力
    fmt.Println(dp[N][M])
}

ちょっとした応用

今までの問題は容量W以下で価値を最大化する問題であったが,更新式を少し変更してやることで重さがちょうどWでもっとも価値が高い組み合わせというものも探せる.最大部分和集合問題を混ぜたような形になる.

$dp_{i, j}$を

dp_{i, j} = \left\{
\begin{array}{ll}
-1 & ((i, j) \neq (0, 0)) \\
0 & ((i, j) = (0, 0)) \\
\end{array}
\right.

で初期化してやり,更新式を以下の様に変更してやると$i$番目までの荷物を用いて重さがちょうど$j$の時の価値の総和の最大値を求めることができる.

dp_{i, j} = \left\{
\begin{array}{ll}
dp_{i-1, j} & (j \lt w_i\ \mathrm{and}\ dp_{i-1, j}\ge 0\  \mathrm{and}\ dp_{i-1, j-w_i}\lt 0) \\
dp_{i-1, j-w_i} + v_i & (j \lt w_i\ \mathrm{and}\ dp_{i-1, j}\lt 0\  \mathrm{and}\ dp_{i-1, j-w_i}\ge 0) \\
\mathrm{max}(dp_{i-1, j}, dp_{i-1, j-w_i}+v_i) & (j \lt w_i\ \mathrm{and}\ dp_{i-1, j}\ge 0\  \mathrm{and}\ dp_{i-1, j-w_i}\ge 0) \\
\end{array}
\right.

この式だと$dp_{N, W}=-1$の場合もある(すなわちどのような組み合わせを使ってもちょうど$W$の重さにならない).

この場合の問題設定としては価値が一番大事な要素ではなく,総計重量と容量の差が小さいことが一番大事で同じ重さを実現する組み合わせの中でどれを選ぶかを価値の総和に基づいて判断するような形になる.

こちらもGoで実装してみる.ここではどの組み合わせになるのかを確認するためのトレースバックも追加している.

package main

import "fmt"
import "math"

func max(lhs, rhs int64) int64 {
    return int64(math.Max(float64(lhs), float64(rhs)))
}

// Route トレースバック用のpair
type Route struct {
    x, y int
}

func main() {
    var (
        N, M int
    )
    fmt.Scanf("%d %d", &N, &M)
    values, weights := make([]int64, N), make([]int, N)
    for i := 0; i < N; i++ {
        fmt.Scanf("%d %d", &weights[i], &values[i])
    }
    dp := make([][]int64, N+1)
    // トレースバック用のテーブル
    trace := make([][]Route, N+1)
    for i := 0; i < N+1; i++ {
        dp[i] = make([]int64, M+1)
        trace[i] = make([]Route, M+1)
    }
    // 初期化
    for i := 0; i <= N; i++ {
        for j := 1; j <= M; j++ {
            dp[i][j] = -1
        }
    }
    dp[0][0] = 0
    // traceにはどこの(i, j)から来たかの情報を保持させる
    for i := 1; i <= N; i++ {
        for j := 0; j <= M; j++ {
            if dp[i-1][j] >= 0 {
                dp[i][j] = dp[i-1][j]
                trace[i][j] = Route{i - 1, j}
            }
            if j >= weights[i-1] && dp[i-1][j-weights[i-1]] >= 0 && dp[i][j] < dp[i-1][j-weights[i-1]]+values[i-1] {
                dp[i][j] = dp[i-1][j-weights[i-1]] + values[i-1]
                trace[i][j] = Route{i - 1, j - weights[i-1]}
            }
        }
    }
    // 出力
    fmt.Println(dp[N][M])
    // DPテーブルの確認
    fmt.Println(dp)
    pnt := Route{N, M}
    routes := []Route{}
    res := []int{}
    // トレースバック
    for pnt.x != 0 {
        routes = append(routes, pnt)
        pre := pnt
        pnt = trace[pnt.x][pnt.y]
        // 異なる重さのところから値が来ている場合は荷物iを追加したとき
        if pre.y != pnt.y {
            res = append(res, pre.x)
        }
    }
    fmt.Println(routes)
    // 重さがWになるときのベストな荷物の組み合わせの出力
    fmt.Println(res)
}


終わりに

Goを書きたいといったが,競技プログラミングでGoを使うのは中々骨が折れるなと感じた.暗黙の型変換を少しもしてくれないので,forループのj:=0がint32型だとint64型の数値との比較j>weits[i-1]ですらキャストをしないといけないのは,競技プログラミングみたいなのだと多少面倒臭い.
一方で業務で使うのには安心感があるだろうなという直観も生えた.

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

1byteをstringに変換

Go Tourで分からなかったため

Go Tourやってて調べても分からなかったため。実務で使うかは不明。
https://go-tour-jp.appspot.com/methods/18
もっといい方法があったら教えていただけると嬉しいです!

fmt.Sprintで変換

str := fmt.Sprint(byte(255))
fmt.Println(str) // -> 255
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerのscratchイメージ上でgolangのWebアプリを動かす際は、スタティックリンクが必要

Best practices for writing Dockerfiles (参考訳v18.09ベース)を参考に、golangで書いたWebアプリを動かそうとしたらハマったので、その記録を残します。

ざっくりいうと

  1. golangで書いたwebアプリ(サンプル)を、dockerのscratchイメージ上で動かそうとしたら起動で失敗
  2. 調べたらダイナミックリンクでビルドされており、scratchイメージではファイルが足りなかったのが原因
  3. スタティックリンクでビルドし直したら解決した

環境

  • go 1.13.5
  • docker 19.03.5

ハマるまでの流れ

"Use multi-stage builds"にマルチステージビルドを使うgolangのサンプルがあります。ビルドするときに使うイメージとリリースするイメージを分けることで、リリースイメージに余計なもの入れなくてすみ、イメージのサイズも小さくできます。

それはよさそうだと言うことで、手元にあったWebアプリをサンプルにして次のようなDockerfileを用意しました。

FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]

sample-go-serverは、hello worldを返す簡単なものです。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        _, _ = fmt.Fprintf(w, "hello world")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

このDockerfileをビルドします。

❯ docker build -t sample-go-server .
❯ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
sample-go-server         latest              0acf7d1e1ed7        42 minutes ago      7.45MB

runで起動すると思いきや次のようなエラーで失敗します。

❯ docker run -d -p 8080:8080 --name sample sample-go-server
1e097919ec5ac31839228646e2b14bd0434f56d74787d73b6afa172154829250

❯ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

❯ docker logs sample
standard_init_linux.go:211: exec user process caused "no such file or directory"

"no such file or directory"と言われましても…。何が足りないかを言ってくれ〜?

解決方法

ググったところ以下が見つかりました。ありがとうインターネッツ。

netパッケージを含む場合はダイナミックリンクでビルドされ、それが原因でno such fileになるらしいです。
なので、さっそくこれに該当しているかを確認してみます。
ビルドで使ったalpineイメージにはfileコマンドが入っていないため、まずそれを入れます。パッケージは Alpine Linux packagesで探せます。

/bin # apk update && apk add file
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
v3.11.2-25-g58afcd742e [http://dl-cdn.alpinelinux.org/alpine/v3.11/main]
v3.11.2-24-g7cfe3a1534 [http://dl-cdn.alpinelinux.org/alpine/v3.11/community]
OK: 11261 distinct packages available
(1/2) Installing libmagic (5.37-r1)
(2/2) Installing file (5.37-r1)
Executing busybox-1.31.1-r8.trigger
OK: 12 MiB in 17 packages
/bin # which file
/usr/bin/file

fileコマンドで確認してみると、たしかにdinamically linkedとなっていました。

/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, Go BuildID=5E6Qy3Li7DELFoSZUyv5/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/wYNglNx2pwV9Z_45IRLn, not stripped

参考サイトにあったCGO_ENABLED=0でビルドをし直してみます。

FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN CGO_ENABLED=0 go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]

たしかにstatically linkedに変わりました。

/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=cD81ASWTt8bngTyIxfpe/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/rmgAW8vnYv0XAugzIkMj, not stripped

docker runでも無事起動できました。

❯ docker run -d -p 8080:8080 --name sample sample-go-server
c27e29aaa697ac7131995472377bb2ff58e6a6ebe6a8174fb8a9de64efc767d5

❯ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
c27e29aaa697        sample-go-server    "/bin/sample-go-serv…"   2 seconds ago       Up 1 second         0.0.0.0:8080->8080/tcp   sample

❯ curl http://localhost:8080
hello world

参考

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

dockerのscratchイメージでgolangのWebアプリを動かすときは、スタティックリンクしてるかを確認

Best practices for writing Dockerfiles (参考訳v18.09ベース)を参考に、golangで書いたWebアプリを動かそうとしたらハマったので、その記録を残します。

ざっくりいうと

  1. golangで書いたwebアプリ(サンプル)を、dockerのscratchイメージ上で動かそうとしたら起動で失敗
  2. 調べたらダイナミックリンクでビルドされており、scratchイメージではファイルが足りなかったのが原因
  3. スタティックリンクでビルドし直したら解決した

環境

  • go 1.13.5
  • docker 19.03.5

ハマるまでの流れ

"Use multi-stage builds"にマルチステージビルドを使うgolangのサンプルがあります。ビルドするときに使うイメージとリリースするイメージを分けることで、リリースイメージに余計なもの入れなくてすみ、イメージのサイズも小さくできます。

それはよさそうだと言うことで、手元にあったWebアプリをサンプルにして次のようなDockerfileを用意しました。

FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]

sample-go-serverは、hello worldを返す簡単なものです。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        _, _ = fmt.Fprintf(w, "hello world")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

このDockerfileをビルドします。

❯ docker build -t sample-go-server .
❯ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
sample-go-server         latest              0acf7d1e1ed7        42 minutes ago      7.45MB

runで起動すると思いきや次のようなエラーで失敗します。

❯ docker run -d -p 8080:8080 --name sample sample-go-server
1e097919ec5ac31839228646e2b14bd0434f56d74787d73b6afa172154829250

❯ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

❯ docker logs sample
standard_init_linux.go:211: exec user process caused "no such file or directory"

"no such file or directory"と言われましても…。何が足りないかを言ってくれ〜?

解決方法

ググったところ以下が見つかりました。ありがとうインターネッツ。

netパッケージを含む場合はダイナミックリンクでビルドされ、それが原因でno such fileになるらしいです。
なので、さっそくこれに該当しているかを確認してみます。
ビルドで使ったalpineイメージにはfileコマンドが入っていないため、まずそれを入れます。パッケージは Alpine Linux packagesで探せます。

/bin # apk update && apk add file
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
v3.11.2-25-g58afcd742e [http://dl-cdn.alpinelinux.org/alpine/v3.11/main]
v3.11.2-24-g7cfe3a1534 [http://dl-cdn.alpinelinux.org/alpine/v3.11/community]
OK: 11261 distinct packages available
(1/2) Installing libmagic (5.37-r1)
(2/2) Installing file (5.37-r1)
Executing busybox-1.31.1-r8.trigger
OK: 12 MiB in 17 packages
/bin # which file
/usr/bin/file

fileコマンドで確認してみると、たしかにdinamically linkedとなっていました。

/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, Go BuildID=5E6Qy3Li7DELFoSZUyv5/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/wYNglNx2pwV9Z_45IRLn, not stripped

参考サイトにあったCGO_ENABLED=0でビルドをし直してみます。

FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN CGO_ENABLED=0 go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]

たしかにstatically linkedに変わりました。

/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=cD81ASWTt8bngTyIxfpe/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/rmgAW8vnYv0XAugzIkMj, not stripped

docker runでも無事起動できました。

❯ docker run -d -p 8080:8080 --name sample sample-go-server
c27e29aaa697ac7131995472377bb2ff58e6a6ebe6a8174fb8a9de64efc767d5

❯ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
c27e29aaa697        sample-go-server    "/bin/sample-go-serv…"   2 seconds ago       Up 1 second         0.0.0.0:8080->8080/tcp   sample

❯ curl http://localhost:8080
hello world

参考

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

dockerのscratchイメージでgolangのWebアプリを動かす

Best practices for writing Dockerfiles (参考訳v18.09ベース)を参考に、golangで書いたWebアプリを動かそうとしたらハマったので、その記録を残します。

ざっくりいうと

  1. golangで書いたwebアプリ(サンプル)を、dockerのscratchイメージ上で動かそうとしたら起動で失敗
  2. 調べたらダイナミックリンクでビルドされており、scratchイメージではファイルが足りなかったのが原因
  3. スタティックリンクでビルドし直したら解決した

環境

  • go 1.13.5
  • docker 19.03.5

ハマるまでの流れ

"Use multi-stage builds"にマルチステージビルドを使うgolangのサンプルがあります。ビルドするときに使うイメージとリリースするイメージを分けることで、リリースイメージに余計なもの入れなくてすみ、イメージのサイズも小さくできます。

それはよさそうだと言うことで、手元にあったWebアプリをサンプルにして次のようなDockerfileを用意しました。

FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]

sample-go-serverは、hello worldを返す簡単なものです。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        _, _ = fmt.Fprintf(w, "hello world")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

このDockerfileをビルドします。

❯ docker build -t sample-go-server .
❯ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
sample-go-server         latest              0acf7d1e1ed7        42 minutes ago      7.45MB

runで起動すると思いきや次のようなエラーで失敗します。

❯ docker run -d -p 8080:8080 --name sample sample-go-server
1e097919ec5ac31839228646e2b14bd0434f56d74787d73b6afa172154829250

❯ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

❯ docker logs sample
standard_init_linux.go:211: exec user process caused "no such file or directory"

"no such file or directory"と言われましても…。何が足りないかを言ってくれ〜?

解決方法

ググったところ以下が見つかりました。ありがとうインターネッツ。

netパッケージを含む場合はダイナミックリンクでビルドされ、それが原因でno such fileになるらしいです。
なので、さっそくこれに該当しているかを確認してみます。
ビルドで使ったalpineイメージにはfileコマンドが入っていないため、まずそれを入れます。パッケージは Alpine Linux packagesで探せます。

/bin # apk update && apk add file
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
v3.11.2-25-g58afcd742e [http://dl-cdn.alpinelinux.org/alpine/v3.11/main]
v3.11.2-24-g7cfe3a1534 [http://dl-cdn.alpinelinux.org/alpine/v3.11/community]
OK: 11261 distinct packages available
(1/2) Installing libmagic (5.37-r1)
(2/2) Installing file (5.37-r1)
Executing busybox-1.31.1-r8.trigger
OK: 12 MiB in 17 packages
/bin # which file
/usr/bin/file

fileコマンドで確認してみると、たしかにdinamically linkedとなっていました。

/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, Go BuildID=5E6Qy3Li7DELFoSZUyv5/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/wYNglNx2pwV9Z_45IRLn, not stripped

参考サイトにあったCGO_ENABLED=0でビルドをし直してみます。

FROM golang:1.13.5-alpine AS build
WORKDIR /go/src/sample-go-server
COPY ./app /go/src/sample-go-server
RUN CGO_ENABLED=0 go build -o /bin/sample-go-server

FROM scratch
COPY --from=build /bin/sample-go-server /bin/sample-go-server
EXPOSE 8080
ENTRYPOINT ["/bin/sample-go-server"]

たしかにstatically linkedに変わりました。

/bin # file sample-go-server 
sample-go-server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=cD81ASWTt8bngTyIxfpe/TdC1EDiTsXrg0i0ta2Xx/49v4RWXcyEm12mEJPV8f/rmgAW8vnYv0XAugzIkMj, not stripped

docker runでも無事起動できました。

❯ docker run -d -p 8080:8080 --name sample sample-go-server
c27e29aaa697ac7131995472377bb2ff58e6a6ebe6a8174fb8a9de64efc767d5

❯ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
c27e29aaa697        sample-go-server    "/bin/sample-go-serv…"   2 seconds ago       Up 1 second         0.0.0.0:8080->8080/tcp   sample

❯ curl http://localhost:8080
hello world

参考

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

Golang + gRPCで開発を始められるDockerfileを作る

概要

Go + gRPCで開発を行う為の環境構築方法を解説します。

環境構築はDockerを利用します。

gRPCの詳しい説明はこの記事では行いません。

想定読者

  • Dockerの基礎知識をお持ちの方
  • GoLangの基礎知識をお持ちの方
  • Go + gRPCでの開発に興味がある方

サンプルコードをGitHubに登録してあります

以下にサンプルコードを用意してあります。

https://github.com/keitakn/golang-grpc-server

こちら がDockerfileの中身になります。

以後はこのDockerfileを作り上げるまでに実行した手順を解説しますので、 こちら を見て理解出来る方はこの記事をこれ以上読む必要はありません。

Dockerfileを作るまでの手順

Alpine 3.11系のGoLangイメージをベースにします。

  • ホストOS(MacOS Catalina 10.15.2)
  • Dockerの実行環境(Docker for Mac 2.1.0.5)

protobufのインストール

gRPCでは初めにインタフェース定義言語 (IDL) でAPIの定義ファイルを作成します。

ここではgRPCでデファクトスタンダードになっている Protocol Buffers を利用出来るようにします。

https://github.com/protocolbuffers/protobuf/releases から最新版ダウンロードを行いインストールを行います。

packageの最新化と関連packageのインストール

packageの最新化とコンパイルに必要なpackageをインストールします。

apk update
apk add git curl build-base autoconf automake libtool

/tmptar.gz をダウンロードする

curl -L -o /tmp/protobuf.tar.gz https://github.com/protocolbuffers/protobuf/releases/download/v3.11.2/protobuf-cpp-3.11.2.tar.gz

Build、インストールを行う

cd /tmp/tmp に移動します。

tar xvzf protobuf.tar.gz を実行し .tar.gz を解凍します。

その後 cd protobuf-3.11.2 で移動します。

以下のコマンドを順番に実行します。

./autogen.sh
./configure
make -j 3
make check
make install

補足ですが make -j は並行処理を行う事でコンパイルを高速化する為のオプションです。
make -j 3 は3並行処理を行うという意味になります。
お使いの環境の「CPUの数×2」を目安に調整するのが良いと思います。

ちなみに、Build、インストールにはかなりの時間がかかります。

protoc --version で以下のように表示されれば成功です。

libprotoc 3.11.2

protocのGo用のプラグインをインストール

protocをGoで利用する為にプラグインをインストールします。

go get -u github.com/golang/protobuf/protoc-gen-go

protocによるdocument生成(任意)

こちらは任意ですが、 .proto からHTML形式等でドキュメントを生成出来るのでインストールしておくと良いでしょう。

go get -u github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc

protobufの動作確認

こちら に動作確認用のGitリポジトリを用意しました。

プロジェクトルートの pb/ 配下に dog.proto を作成します。

pb/dog.proto
syntax = "proto3";

service Dog {
    rpc FindCuteDog (FindCuteDogMessage) returns (FindCuteDogResponse) {}
}

message FindCuteDogMessage {
    string DogId = 1;
}

message FindCuteDogResponse {
    string name = 1;
    string kind = 2;
}

プロジェクトルート(私のサンプルプロジェクトだと /go/app )で以下のコマンドを実行します。

protoc --go_out=plugins=grpc:. pb/dog.proto

そうすると pb/dog.pb.go というファイルが作成されているかと思います。

以下のようなファイルです。(内容を一部載せています。)

pb/dog.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: pb/dog.proto

package dog

import (
    context "context"
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    grpc "google.golang.org/grpc"
    codes "google.golang.org/grpc/codes"
    status "google.golang.org/grpc/status"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

type FindCuteDogMessage struct {
    DogId                string   `protobuf:"bytes,1,opt,name=DogId,proto3" json:"DogId,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

func (m *FindCuteDogMessage) Reset()         { *m = FindCuteDogMessage{} }
func (m *FindCuteDogMessage) String() string { return proto.CompactTextString(m) }
func (*FindCuteDogMessage) ProtoMessage()    {}
func (*FindCuteDogMessage) Descriptor() ([]byte, []int) {
    return fileDescriptor_993586c250cd4a03, []int{0}
}

// 以下略

ここまでの手順でGolang + gRPCで開発出来る環境が整いました。

最終的なDockerfileを載せておきます。(こちら と同様の内容です)

Dockerfile
FROM golang:1.13-alpine3.11 as build

WORKDIR /tmp

ENV PROTOBUF_VERSION 3.11.2
ENV PROTOBUF_URL https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VERSION/protobuf-cpp-$PROTOBUF_VERSION.tar.gz

RUN set -eux && \
  apk update && \
  apk add --no-cache git curl build-base autoconf automake libtool && \
  curl -L -o /tmp/protobuf.tar.gz $PROTOBUF_URL && \
  tar xvzf protobuf.tar.gz

WORKDIR /tmp/protobuf-$PROTOBUF_VERSION

RUN set -eux && \
  ./autogen.sh && \
  ./configure && \
  make -j 3 && \
  make install && \
  go get -u github.com/golang/protobuf/protoc-gen-go && \
  go get -u github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc

WORKDIR /go/app

COPY . .

RUN set -eux && \
  go build -o golang-grpc-server && \
  go get gopkg.in/urfave/cli.v2@master && \
  go get github.com/oxequa/realize && \
  go get -u github.com/go-delve/delve/cmd/dlv && \
  go build -o /go/bin/dlv github.com/go-delve/delve/cmd/dlv

FROM alpine:3.11

WORKDIR /app

COPY --from=build /go/app/golang-grpc-server .

RUN set -x && \
  addgroup go && \
  adduser -D -G go go && \
  chown -R go:go /app/golang-grpc-server

CMD ["./golang-grpc-server"]

以前 Docker上のGoLangをリモートデバッグする という記事を書きましたが、この記事で紹介しているデバッガー等のインストールもこの Dockerfile で行っています。

protocによるdocument生成

こちらのプロジェクト を例に説明します。

docs 配下にHTML形式のドキュメントを生成する為には下記のコマンドを実行します。

protoc --doc_out=html,index.html:./docs pb/*.proto

結果として docs/index.html が出力されます。

見た目はこんな感じです。

ProtocolDocumentation.png

おまけ gRPCサーバーの動作確認(grpc_cli を利用)

gRPCはcurl等で手軽に動作確認が出来ませんでしたが、gRPC command line tool を利用すると動作確認が簡単です。

Mac上にインストールします。

brew install gflags

brew tap grpc/grpc

brew install grpc

which grpc_cli を実行して /usr/local/bin/grpc_cli 等が表示されればインストール出来ています。

grpc_cliの簡単な使い方

以下でServiceの内容を取得します。

grpc_cli ls localhost:9998

以下のようにgRPCサーバーの内容が表示されます。

Cat
grpc.reflection.v1alpha.ServerReflection

次のようにメソッドを指定するとそのインターフェースを確認出来ます。

grpc_cli ls localhost:9998 Cat.FindCuteCat -l

これらのgRPCサーバーの情報を取得する為にはgRPCサーバー側で reflection.Register が実行されている必要があります。

詳しくは こちらのコード を確認して下さい。

実際にgRPCのメソッドを呼び出す際は下記のように実行します。

grpc_cli call localhost:9998 Cat.FindCuteCat 'catId: "moko"'

おわり

以上がGolang + gRPCでの開発環境構築方法です。

最近はマイクロサービスでの開発が多いので、Golang + gRPCという組み合わせの採用例が増えているように感じます。

この記事が少しでもお役に立てたら幸いです。

今回の記事を書く為に以下の記事を参考にさせて頂きました。

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

golang で aws s3v4 の署名キーを作成する方法

aws s3v4 署名バージョン

aws s3 は version4 より署名プロセスが変わっています
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html

署名作成の流れ

  1. 署名に必要なキーを作成する(本記事はここ)
  2. 1.で作成したキーを使って署名を作成する

各言語での署名キー作成方法

各言語の署名キー作成フローは以下の文書にまとまっています。しかしながら、Java, .NET (C#), Python, Ruby, Javascript のサンプルコードはあっても、Golangのサンプルコードは存在しません
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-v4-examples.html

そのため、本記事ではGolangで version4 用の署名取得処理のサンプルコードを載せることとします
ちなみにですが、本記事では署名キーを作成する部分の説明であり、署名作成のために前もって準備しておかなければならない処理が存在します。それについては、後日記事を執筆しようと思います。

公式のサンプルを乗せて比較できるようにしていますが、さっさとGolangでの実装方法を知りたい人は、こちらから

署名に必要なキーを作成する(Java-公式サンプルコード)

公式のサンプルコードにならって説明します
言語は何でも良いのですが、Java を載せておきます

HmacSHA256 はある文字列を、あるキー(byte配列)を使ってhash化する関数です
getSignatureKey は特定の文字列をhash化して署名を得る処理です
ある文字列をハッシュ化した[]byteをキーとして、ハッシュを重ねていくことで、最終生成物の署名を得ることができます。
version4では、
- date
- region
- service
- s3のsecretキー
を使って署名を作成します

static byte[] HmacSHA256(String data, byte[] key) throws Exception {
    String algorithm="HmacSHA256";
    Mac mac = Mac.getInstance(algorithm);
    mac.init(new SecretKeySpec(key, algorithm));
    return mac.doFinal(data.getBytes("UTF-8"));
}

static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
    byte[] kSecret = ("AWS4" + key).getBytes("UTF-8");
    byte[] kDate = HmacSHA256(dateStamp, kSecret);
    byte[] kRegion = HmacSHA256(regionName, kDate);
    byte[] kService = HmacSHA256(serviceName, kRegion);
    byte[] kSigning = HmacSHA256("aws4_request", kService);
    return kSigning;
}

署名に必要なキーを作成する(Golang)

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func main() {
    s3Secretkey := "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
    dateStamp := "20120215"
    regionName := "us-east-1"
    serviceName := "iam"
    signatureKey := getSignature(s3Secretkey, dateStamp, regionName, serviceName)
    fmt.Println(signatureKey) // f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d

}

func getBinaryByMakeHMAC(msg string, key []byte) ([]byte, error) {
    mac := hmac.New(sha256.New, key)
    _, err := mac.Write([]byte(msg))
    return mac.Sum(nil), err
}

func getSignature(s3SecretKey, dateStamp, regionName, serviceName string) string {
    kSecret := []byte("AWS4" + s3SecretKey)
    kDate, _ := getBinaryByMakeHMAC(dateStamp, kSecret)
    kRegion, _ := getBinaryByMakeHMAC(regionName, kDate)
    kService, _ := getBinaryByMakeHMAC(serviceName, kRegion)
    kSigning, _ := getBinaryByMakeHMAC("aws4_request", kService)
    return hex.EncodeToString(kSigning)
}

サンプルコード
https://play.golang.org/p/91EMyZsbvsx

公式が用意している以下のサンプルデータの場合に、出力される値を確認しながら進めるとデバッグしやすいです

key = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
dateStamp = '20120215'
regionName = 'us-east-1'
serviceName = 'iam'

署名キーを得るまでの中間値

kSecret  = '41575334774a616c725855746e46454d492f4b374d44454e472b62507852666943594558414d504c454b4559'
kDate    = '969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d'
kRegion  = '69daa0209cd9c5ff5c8ced464a696fd4252e981430b10e3d3fd8e2f197d7a70c'
kService = 'f72cfd46f26bc4643f06a11eabb6c0ba18780c19a8da0c31ace671265e3c87fa'
kSigning = 'f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d'

サンプルコードでこの中間値を得るためには、ハッシュ化した後の[]byteを hex.EncodeToString([]byte) にて[]byteをhex(16進数)へと変換する必要があることに注意してください

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

golang で aws s3v4 の署名を作成する方法(署名キーを作成する方法)

aws s3v4 署名バージョン

aws s3 は version4 より署名プロセスが変わっています
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html

署名作成の流れ

  1. 署名に必要なキーを作成する(本記事はここ)
  2. 1.で作成したキーを使って署名を作成する

各言語での署名キー作成方法

各言語の署名キー作成フローは以下の文書にまとまっています。しかしながら、Java, .NET (C#), Python, Ruby, Javascript のサンプルコードはあっても、Golangのサンプルコードは存在しません
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-v4-examples.html

そのため、本記事ではGolangで version4 用の署名取得処理のサンプルコードを載せることとします
ちなみにですが、本記事では署名キーを作成する部分の説明であり、署名作成のために前もって準備しておかなければならない処理が存在します。それについては、後日記事を執筆しようと思います。

公式のサンプルを乗せて比較できるようにしていますが、さっさとGolangでの実装方法を知りたい人は、こちらから

署名に必要なキーを作成する(Java-公式サンプルコード)

公式のサンプルコードにならって説明します
言語は何でも良いのですが、Java を載せておきます

HmacSHA256 はある文字列を、あるキー(byte配列)を使ってhash化する関数です
getSignatureKey は特定の文字列をhash化して署名を得る処理です
ある文字列をハッシュ化した[]byteをキーとして、ハッシュを重ねていくことで、最終生成物の署名を得ることができます。
version4では、
- date
- region
- service
- s3のsecretキー
を使って署名を作成します

static byte[] HmacSHA256(String data, byte[] key) throws Exception {
    String algorithm="HmacSHA256";
    Mac mac = Mac.getInstance(algorithm);
    mac.init(new SecretKeySpec(key, algorithm));
    return mac.doFinal(data.getBytes("UTF-8"));
}

static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
    byte[] kSecret = ("AWS4" + key).getBytes("UTF-8");
    byte[] kDate = HmacSHA256(dateStamp, kSecret);
    byte[] kRegion = HmacSHA256(regionName, kDate);
    byte[] kService = HmacSHA256(serviceName, kRegion);
    byte[] kSigning = HmacSHA256("aws4_request", kService);
    return kSigning;
}

署名に必要なキーを作成する(Golang)

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func main() {
    s3Secretkey := "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
    dateStamp := "20120215"
    regionName := "us-east-1"
    serviceName := "iam"
    signatureKey := getSignature(s3Secretkey, dateStamp, regionName, serviceName)
    fmt.Println(signatureKey) // f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d

}

func getBinaryByMakeHMAC(msg string, key []byte) ([]byte, error) {
    mac := hmac.New(sha256.New, key)
    _, err := mac.Write([]byte(msg))
    return mac.Sum(nil), err
}

func getSignature(s3SecretKey, dateStamp, regionName, serviceName string) string {
    kSecret := []byte("AWS4" + s3SecretKey)
    kDate, _ := getBinaryByMakeHMAC(dateStamp, kSecret)
    kRegion, _ := getBinaryByMakeHMAC(regionName, kDate)
    kService, _ := getBinaryByMakeHMAC(serviceName, kRegion)
    kSigning, _ := getBinaryByMakeHMAC("aws4_request", kService)
    return hex.EncodeToString(kSigning)
}

サンプルコード
https://play.golang.org/p/91EMyZsbvsx

公式が用意している以下のサンプルデータの場合に、出力される値を確認しながら進めるとデバッグしやすいです

key = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
dateStamp = '20120215'
regionName = 'us-east-1'
serviceName = 'iam'

署名キーを得るまでの中間値

kSecret  = '41575334774a616c725855746e46454d492f4b374d44454e472b62507852666943594558414d504c454b4559'
kDate    = '969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d'
kRegion  = '69daa0209cd9c5ff5c8ced464a696fd4252e981430b10e3d3fd8e2f197d7a70c'
kService = 'f72cfd46f26bc4643f06a11eabb6c0ba18780c19a8da0c31ace671265e3c87fa'
kSigning = 'f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d'

サンプルコードでこの中間値を得るためには、ハッシュ化した後の[]byteを hex.EncodeToString([]byte) にて[]byteをhex(16進数)へと変換する必要があることに注意してください

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