Golden Road

好きだという熱こそが最低限で最高の希望

golang.tokyo #4に行ってきた。 #golang #golangtokyo

inabaです。

golang.tokyo #4に補欠からの当日滑り込み繰り上がりという運の良さで参加してきました。
会場はペアーズの株式会社エウレカさん。

倍率が2倍ということもあり、狭き門という印象のgolang.tokyoですが、今回から動画撮影も始まったようです。
次回のgolang.tokyoの前あたりに公開されるそうです。
自分も#2,#3と2回補欠だったのでこれは助かりますね!!

また、@tenntennさんからブログ枠の紹介がありました。
毎回3名分のブログ枠が用意されていて、

  • 先着順なので参加確定
  • まとめることで頭の中の整理になる
  • connpassの資料のページに掲示されるので多くの人にブログを見てもらえる

のが主なメリットだそうです。

今回はConcurrency(コンカレンシー、並行性)がテーマでした。
今回はブログ枠で参加ではないですが、ブログ枠で参加するときのためにまとめてみます。

Concurrency for distributed Web crawlers

末田 卓巳さん (@puhitaku) / フラー株式会社

WebクローラーのためのGo言語の話です。

  • アプリのデータをストアからクローラーで24時間毎に取ってくる。
    • 単一のサイト(シングルドメイン)なので間隔が短いとBANされる可能性がある。
    • 10万以上のアプリのデータをクロールする。
    • 画像のダウンロードがあるので1回のクロール時間が異なってくる。
  • コマンダークローラーから構成されるクロールプログラム。
  • 1回のクロール時間が異なるので、例えば100回クロールする2個のクローラーがあっても直列で実行すると処理終了時間がまちまちになる。
    • Goroutineを使って時間をずらして並行に実行していくと実行時間が同じになる。

f:id:i178inaba:20170302021108p:plain

  • SlackBotとしても動いている。
    • クローラのステータスを通知させる目的。
    • 普通のSlackBotは1回コマンドを打つと1回返答が返ってくる。
      • これだと何回もコマンドを打たないとならなくて面倒。
    • クローラーに実装されているSlackBotはSlackの編集機能を使う。
      • コマンドを受けた最初の投稿は普通に。
      • 次の投稿から編集機能を使用し、最初の投稿を編集する。
    • 普通の投稿か編集かの判断をしてPostをするGoroutineと投稿する文字列を生成するGoroutineが分けてあり、chanで通信している。

f:id:i178inaba:20170302022728p:plain

  • 並行処理時のTCPコネクションの枯渇問題
    • DBからデータを吸い出すのも並行で行っている。
    • 何らかの理由でqueryが遅く、並行処理が重なってしまいTCPコネクションを使い果たしてエラーが出ることがある。
    • それを防ぐため、バッファサイズを指定したchan(make(chan int, 100))を使用して一度に処理する回数を制限する。
    • 制限中にGoroutineが1つ終了するとブロックされていたchanのブロックが解除され、次のGoroutineが動作する仕組み。

Slackの編集機能をBotに使わせるというアイデアには「その手があったか!」と目からウロコでした!

Goのスケジューラー実装とハマりポイント

井手 康貴さん (@niconegoto) / 株式会社メルカリ/株式会社ソウゾウ

https://talks.godoc.org/github.com/niconegoto/talks/concurrency.slide#1

参考記事: qiita.com

The Go scheduler - Morsing's blog

  • GoroutineはOSスレッドに対して多対多。
    • 1対1、多対1に比べて全てのコアを使えてコンテキストスイッチも高速でいいとこ取り。
    • ただし、スケジューラーへの追加が煩雑になる。
      • Goはこの煩雑さをラップしていて、簡単(go f()と書くだけ。)にGoroutineが使えるようになっている。
  • 用語(runtimeパッケージ以下を読む時に覚えておくと役に立つ。)
    • M(Machine): OSスレッド
    • G(Goroutine): ゴルーチン
    • P(Processor): プロセッサ。4コアなら4
  • Pは自分にスケジューリングされたGが無くなると、別のPからGを半分奪う。
    • これにより、全てのスレッドが最大パフォーマンスで動作できる。
  • runtime.GOMAXPROCS()が1でもGoは複数のスレッドで実行される。
  • ハマりどころ
    • ループしてGoroutine外の変数を参照する場合。
    • runtime.GOMAXPROCS()が大きいほど切り替わりやすくなる。

今までなんとなく使っていたgo f()の仕組みがとてもわかりやすく理解できました。

Ridge - A framework like GAE/Go on AWS

藤原 俊一郎さん (@fujiwara) / 株式会社カヤック

GAE/GoをAWSで動かすためのフレームワークの話です。

  • 下記を使用
    • APEX
      • 通常、LambdaでGoを実行することはできないが、APEXを経由させることにより実行できる。
    • AWS API Gateway
    • AWS Lambda
    • Go
  • API Gateway × Lambdaのデメリット
    • 独自オブジェクト
    • ルーティング・・・
    • テスト・・・
    • Goで書きたい!
  • Ridgeが行う事
    • API Gateway(JSON)とnet/http(Request, Response)の相互変換
    • Lambdaで動かさない時は通常と同じくサーバとして立ち上げられる。
  • 流れはリクエスト→API GatewayAWS Lambda→APEX→Go(Ridge→Handler)
  • メリット
    • 独自オブジェクトではなく、いつも使っているRequest/Responseが使える
      • Request/Responseで書くからテストにhttptestが使える。
    • ルーティングもGo内で行える。
    • ローカルで動かせるからlocalhostを叩いて結果を受けるとかができる。
    • EC2でも普通に動かせる。
    • API Gateway × Lambdaのメリットが享受できる。
      • 高負荷時自動スケール
      • EC2の管理無し!
      • デプロイもコマンド一発。
      • つまりGAE/Go!
  • できないこと
    • 同一名クエリ(foo=a&foo=b)や同一名ヘッダが受け取れない。
    • バイナリ(画像等)が返せない。
    • Goroutineを動かし続ける
    • 1リクエスト10秒を超える処理
  • 向いていない事
    • 低レイテンシが要求されるような処理
  • 向いている事
    • 頻繁にアクセスが無い。
    • レイテンシ要求がシビアでない。
      • ChatBotやゲームの終了告知(POSTを受けてJSONを返す)等
  • ログはCloudWatch Logsに送られるが、それをLambdaに送ってRidgeでパースしている。

github.com

既存のWebアプリもちょこっといじってmux.HandleFuncを使うようにするだけでハンドラの中身を変えなくてもRidgeを使い始められるのがすごいですね!
制限はありますが、ChatBotくらいから試しに使ってみるのはいいかもしれません。

懇親会

途中、ピザとビールが出る懇親会がありました。

美味しかったです!ごちそうさまでした!!

Goを使ったtailコマンドの実装(ライブコーディング)

金子 慎太郎さん (@kaneshin0120) / 株式会社エウレカ

コード: https://gist.github.com/kaneshin/a398720b8e20722a83bc6903e4017435

発表予定だった牧さんが残念ながら都合がつかず来られなくなってしまったため、急遽かねしんさんがライブコーディングをすることに。
懇親会しながらも、みんなの目線はスクリーンに釘付けでした。
tenntennさんのツッコミも入りつつ、かなり盛り上がっていました。

嫁に怒られずにGoを書く技術

@teitei_tkさん / freee株式会社

嫁だけでなく、非エンジニアの友人駆動開発にも使えますね。
嫁が居ないのでそんなことを考えてました(泣)
質問の時、tenntennさんが
「質問無いですか?」
「嫁はどうやって作るんですかとかでもいいですよ。」
「ペアーズ使いましょう!」
と自分でツッコんで会場が沸いてましたwww

Gogland

Sergey Ignatovさん (@sergey__ignatov) / JetBrains

JetBrains製のIDEの紹介です。

英語だったので必死に聞きましたが半分はツイッター#golangtokyoを追っていました。
何件か気になるツイートを紹介します。

自分は、こういうのどうやって作るんだろうっていうのを考えながら見ていました。

まとめ

ブログを書くのは結構大変だけど、頭の中が整理できるのでおすすめです。
長めに書いてしまったのは反省点で、今後はもうちょっとまとめられるようにしていきたいと思います。

運営の皆さんご苦労様でした!!
また参加させて頂きます!!

CentOS 7のファイアウォールで80番ポートを許可する

inabaです。

サーバにnginxをインストールして、localhostからはアクセスできるのに、外部(グローバルIPや設定したドメイン)からはアクセスできなくて困っている人はファイアウォールの設定を疑ってみるとよいです。

firewall-cmd

ファイアウォールを操作するコマンドです。

www.firewalld.org

コマンドの使い方を全て覚えるのは大変なのでポート許可に必要な使い方を紹介していきます。

アクティブなゾーンを確認する

ファイアウォールは「事前に定義されたゾーンに対して、指定されたルールの通信を通す。」という事をしています。
なので、まずはどのゾーンへのファイアウォールが動いているかを確認します。

$ firewall-cmd --get-active-zones
public
  interfaces: eth0

--get-active-zonesというオプションをつけると、現在アクティブなゾーンが表示されます。

上記だとpublicというゾーンがアクティブな事がわかります。
複数ある場合は、eth0が外部公開されているインタフェースなのでeth0が定義されているゾーンが外部向けポートを追加する対象になります。

許可されているポートの確認

次は先程確認したアクティブなゾーンpublicの確認です。
現在許可されているポートを確認します。

$ firewall-cmd --info-zone public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: dhcpv6-client ssh
  ports:
  protocols:
  masquerade: no
  forward-ports:
  sourceports:
  icmp-blocks:
  rich rules:

servicesには定義されているサービス(ルールのような物)が表示されます。
portsにはサービスとは別に開放されているポートが表示されます。
今回はportsには何も表示されていませんが、例えば8080番ポートが開放されていると、ports: 8080/tcpのように表示されます。

上の例だとservicesにdhcpv6-clientとsshが設定されています。

サービスの内容を確認します。

$ firewall-cmd --info-service dhcpv6-client
dhcpv6-client
  ports: 546/udp
  protocols:
  source-ports:
  modules:
  destination: ipv6:fe80::/64
$ firewall-cmd --info-service ssh
ssh
  ports: 22/tcp
  protocols:
  source-ports:
  modules:
  destination:

portsに開放されているポートが表示されます。
dhcpv6-clientでは546番ポート、sshでは22番ポートが開放されています。

定義されているサービスを確認

サービスの一覧は--get-servicesオプションで確認できます。

$ firewall-cmd --get-services
RH-Satellite-6 amanda-client amanda-k5-client bacula bacula-client ceph ceph-mon dhcp dhcpv6 dhcpv6-client dns docker-registry dropbox-lansync freeipa-ldap freeipa-ldaps freeipa-replication ftp high-availability http https imap imaps ipp ipp-client ipsec iscsi-target kadmin kerberos kpasswd ldap ldaps libvirt libvirt-tls mdns mosh mountd ms-wbt mysql nfs ntp openvpn pmcd pmproxy pmwebapi pmwebapis pop3 pop3s postgresql privoxy proxy-dhcp ptp pulseaudio puppetmaster radius rpc-bind rsyncd samba samba-client sane smtp smtps snmp snmptrap squid ssh synergy syslog syslog-tls telnet tftp tftp-client tinc tor-socks transmission-client vdsm vnc-server wbem-https xmpp-bosh xmpp-client xmpp-local xmpp-server

結構量が多いのですが、この中にhttpというそれっぽいサービスを見つけることができます。
httpサービスの内容を確認します。

$ firewall-cmd --info-service http
http
  ports: 80/tcp
  protocols:
  source-ports:
  modules:
  destination:

ビンゴです!
80番ポートがルールとして定義されています。

サービスをゾーンに追加

httpサービスが80番ポートを開放するサービスという事がわかったので、publicゾーンにhttpサービスを追加します。

$ firewall-cmd --permanent --zone public --add-service http
success
$ firewall-cmd --reload
success

--permanentは設定の永続化。(無いと再起動時に設定が消えます。)
--zone publicはゾーンの指定。
--add-service httpは追加するサービスの指定です。

--add-serviceだけでは設定が反映されないので、設定をした後に--reloadファイアウォールに設定を反映させます。

$ firewall-cmd --info-zone public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: dhcpv6-client http ssh
  ports:
  protocols:
  masquerade: no
  forward-ports:
  sourceports:
  icmp-blocks:
  rich rules:

設定が反映されました。

サービスに許可したいポートが無い場合

例えば8080等のポートを開けたい場合があると思います。
しかし、サービスには一般的に使用されているポートしか定義されておらず、8080番のポートは定義されていません。
そういう場合は--add-serviceの代わりに--add-portを使います。

$ firewall-cmd --permanent --zone public --add-port 8080/tcp
success

このような感じです。

今日はここまで。

ConoHaでサーバ立ててユーザ追加したりSSHの設定したり

inabaです。

ConoHaが安くてサーバ立ち上げが早くて良さげです。

www.conoha.jp

なのでVPSを借りて、いじって遊ぶ用のサーバを作ります。

参考記事:

qiita.com

大体は上記に書いてある通りに進めます。
使用するOSはCentOS 7です。
ここには気になった事や追加でやった事等を書いていきます。

wheelグループって何?

wheelグループに追加と書かれている箇所があります。

# wheelグループに追加
$ gpasswd -a newuser wheel

wheelグループとはデフォルトでsudoができるグループです。
所属しているとユーザ個別に設定しなくてもsudoができます。 設定は/etc/sudoersに記述されています。

$ cat /etc/sudoers
...
## Allows people in group wheel to run all commands
%wheel  ALL=(ALL)   ALL
...

gpasswd

いつもグループへの追加はusermodでやっていて、gpasswdは知りませんでした。
usermod-a忘れるとグループが置き換えられる事がありますが、gpasswdはその心配が無いので良さげです。

qiita.com

パスワード無しでsudo

先程の/etc/sudoersのwheelグループの設定の下にパスワード無し用の設定がコメントアウトされているのでそのコメントアウトを外し、元の設定をコメントアウトします。
sudoは結構重要な操作なので自分は一旦パスワード有りで使ってみて、面倒になってきたら外そうと思います。

## Allows people in group wheel to run all commands
# %wheel    ALL=(ALL)   ALL

## Same thing without a password
%wheel  ALL=(ALL)   NOPASSWD: ALL

SSHログイン許可の設定はデフォルトを使う

参考サイトのSSHログインを許可の項目ですが、設定ファイル(/etc/ssh/sshd_config)はデフォルトだとこうなっていました。

#RSAAuthentication yes
#PubkeyAuthentication yes
AuthorizedKeysFile  .ssh/authorized_keys

RSAAuthenticationPubkeyAuthenticationコメントアウトされています。
コメントアウトされているという事はデフォルト値が使われるという事。

ここにデフォルト値が書いてあるのですが、

sshd の設定(sshd_config)

RSAAuthenticationPubkeyAuthenticationのデフォルト値はyesです。
自分は極力デフォルトで行けるならデフォルトを使いたい人間なので、設定はいじらず、コメントアウトのまま使います。

SSHキーはローカルにあるものを使う

SSH鍵を作成の項目でホスト側でキーを新たに作ってローカルに秘密鍵をコピーしていますが、自分はローカルにSSHキーが生成してあるのでこれを使います。

ローカル:

$ cat ~/.ssh/id_rsa.pub # 公開鍵
ssh-rsa foobarbaz...
# 出た文字列をコピー

サーバ(rootではなく、新しく作ったユーザで実行。):

$ ssh-keygen # ~/.sshディレクトリを手で作るのが面倒なのでコマンドで作る。
# デフォルトで良いのでEnter3回

$ echo 'ssh-rsa foobarbaz...' > ~/.ssh/authorized_keys # ローカルでコピーした公開鍵を登録

$ chmod 600 ~/.ssh/authorized_keys # パーミッションを設定

ssh-keygenを使っちゃってますが、これは補完が効くコマンドの方が打つのが楽だから使ってます。
作った鍵は今は使わないので、普通にmkdir ~/.sshでもいいです。

authorized_keysパーミッションの設定が重要

これで結構ハマった。
SSHキーを使ってでログインしようとしても、パスワードを聞かれるので、sshのログが書かれる /var/log/secureあたりを調べて

Authentication refused: bad ownership or modes for file /home/newuser/.ssh/authorized_keys

と出てきたのでこれで検索。

takuya-1st.hatenablog.jp

authorized_keysパーミッションオーナー以外の書き込み権限がついているとダメなようだった。
実際にauthorized_keysパーミッションを見てみるとrw-rw-r–になっていたので上記の通りchmod 600 ~/.ssh/authorized_keysパーミッションを設定してやると、パスワードを聞かれなくなった!

ログイン攻撃がヤバい

lastbでログイン失敗履歴が見られるのだが、身に覚えのない失敗が約1000回くらいあった。
怖くなってlastでログイン成功履歴を見たが、そっちには身に覚えの無い履歴は無かったので良かったが、こんなに攻撃されるのかとビビるね。
精神衛生上良くない。

なので、rootからのログインを禁止パスワードによるログインを禁止は絶対やるべき。

/etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no

まとめ

何点か書きましたが、sshauthorized_keysパーミッション以外は特にハマること無く進められました。
このサーバで何しよっかな〜。

今日はここまで。

bufio.Scannerのエラーと解決策 #golang

こんばんわ。
inabaです。

今日はAtCoder Beginner Contest 055に参加していました。

その時に起こったエラーを調査したので参考までに。

Runtime Error

ある問題を解いて提出したら何件かでRuntime Errorが出た。
Runtime Errorの内容は見られないのでコードをちょこちょこ変えながらデバッグ

原因

読み込み文字列が長いのが原因。
その問題の前提条件には10万文字が最大で入ってくるという事が書いてあったが、bufio.Scannerがデフォルトで最大サイズが決まっている事を知らずハマってた。

最大サイズの定数:

MaxScanTokenSize = 64 * 1024

64 * 1024だから約6万5千文字がデフォルトで読み込める最大文字数。
10万文字なので余裕で超える。
デフォルトを超えるサイズの文字列を(*Scanner).Scanすると(*Scanner).Errbufio.ErrTooLongが返される。

var sc = bufio.NewScanner(os.Stdin)
sc.Split(bufio.ScanWords)
sc.Scan()
if err := sc.Err(); err == bufio.ErrTooLong {
    panic(err)
}

みたいな感じで調査してやっと見つけた。

解決策

(*Scanner).Bufferで最大サイズを指定すると65536文字より大きい文字列も読み込んでくれた。

var sc = bufio.NewScanner(os.Stdin)
sc.Buffer([]byte{}, math.MaxInt64)

こんな感じです。
とりあえずint64の最大値を指定してますが、いきなり指定サイズまでアロケーションされるという事は無く、現行のサイズ*2で読み込みを再試行していくようなのでまぁいいかなと。

src/bufio/scan.go - The Go Programming Language

今日はここまで。

外部キーが設定されているとTRUNCATEできない #MariaDB #MySQL

inabaです。

最近はgolangでDBをいじっています。
テストもテスト用DBを作り、実際に書き込んで行うようにしていました。

テストをするたびにAUTO_INCREMENTの値が増えていく

まぁそうでしょう。
なので、テストデータのクリアをするためにTRUNCATEしようと思ってエラー出てハマりました。

エラー

2つのエラーが出ました。

プレースホルダ

_, err = db.Exec("TRUNCATE TABLE ?", tableName)

上記コードの時は下記のエラーが出ました。

Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '?' at line 1

テーブル名にプレースホルダは使えないらしい。
元々プレースホルダはキャッシュ目的だからFROM句は変えないですし、
セキュリティ的な使用でも、ユーザの入力をFROM句には使わないから妥当な動きですよね。

外部キー制約

プレースホルダをやめてfmt.Sprintfを使ってやるようにしてみたらエラー内容が変わりました。

_, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName))

下記がエラー。

Error 1701: Cannot truncate a table referenced in a foreign key constraint ...

これは、外部キー制約によってTRUNCATEができないというエラーでした。

外部キー制約の回避

外部キー制約を回避する方法があります。

_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 0")
_, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName))
_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 1")

上記コードではエラーは出ません。
FOREIGN_KEY_CHECKS0に設定すると、外部キー制約の回避ができます。

別の解決法

AUTO_INCREMENTが初期化されればよいのであれば、DELETEしてからAUTO_INCREMENTをリセットすることもできます。

_, err = db.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
_, err = db.Exec(fmt.Sprintf("ALTER TABLE %s AUTO_INCREMENT = 1", tableName))

これなら2行で済みます。
ただ、子テーブルにデータが入っているとエラーになるので、テストで子テーブルも使っている場合は子テーブルの方から消していってください。

今日はここまで。

【Go】structをJSONにする時、ゼロ値を含む/含まないを切り替える方法と注意点 #golang

inabaです。

Go言語でstructをJSONにする時、ゼロ値を含まない場合はタグにomitemptyをつけますよね。

type stampCard struct {
    Stamp int `json:"stamp,omitempty"`
}

しかし、同じstructを使っている時にゼロ値を含む/含まないを切り替えたい時があります。
具体的には下記のようなstructです。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

上記のstructではInStampRallyがtrueの時だけStampの値をJSONに含めたいです。
しかし、このstructのままだとInStampRallyがtrueでもStampが0なら含まれません。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

func main() {
    sc := stampCard{InStampRally: true, Stamp: 0}
    jsc, err := json.Marshal(pc)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jsc)) // {"in_stamp_rally":true}
}

解決策

解決策はゼロ値を含む/含まないを切り替えたい値をポインタ化します。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

これで

  • nilでないならJSONに値を含める
  • nilならJSONに値を含めない

という切り替えができます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    scs := []stampCard{
        {InStampRally: false},
        {InStampRally: true},
    }

    for i, sc := range scs {
        if sc.InStampRally {
            scs[i].Stamp = intPointer(0)
        }
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs)) // [{"in_stamp_rally":false},{"in_stamp_rally":true,"stamp":0}]
}

func intPointer(v int) *int {
    return &v
}

上記サンプルコードではstampCardのスライスをループさせてInStampRallyがtrueなら値が0のintポインターStampに代入しています。
こうすることで下記のようにomitemptyが指定されていてもゼロ値をJSONに出力することができます。

[
  {
    "in_stamp_rally": false
  },
  {
    "in_stamp_rally": true,
    "stamp": 0
  }
]

注意点

rangeループのvalueからポインターを取ると、ループでvalueの値が変わってもポインターの値は同じなので、注意しないと意図しない値が入ってしまいます。
(ポインターの値が同じ理由は、ループのたびに新しいメモリを確保しないためでしょう。)

DBから取得した値をループさせてJSON化する時等、注意が必要です。

例として、下記は順番に0, 1, 2, 3のスタンプ数のスタンプカードができる事を期待しているコードです。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        log.Infof("%p", &stamp)
        sc := stampCard{InStampRally: true, Stamp: &stamp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

しかし、上記の結果は下記になります。

$ go run main.go | jq
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
[
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

まず、ループ中のvalueのポインター値を表示していますが、すべて同じになっています。
その後のJSONは全てのstamp3になってしまいました。
全ての値が最後の値に書き換わってしまっています。

修正後のコード

ループのvalueを一旦新しい変数に入れてその変数のポインターを取れば正確な値をJSONに出力できます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        tmp := stamp // important!!
        sc := stampCard{InStampRally: true, Stamp: &tmp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

結果:

[
  {
    "in_stamp_rally": true,
    "stamp": 0
  },
  {
    "in_stamp_rally": true,
    "stamp": 1
  },
  {
    "in_stamp_rally": true,
    "stamp": 2
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

今日はここまで。

npm loginは空いているユーザ名を指定するとアカウントを作成する

inabaです。

npm owner addで自分のパッケージにオーナーを追加できると知り、
試すためにログインが必要だったのでnpm loginでログインした時の話。

$ npm login
Username: foobar
Password: 
Email: (this IS public) foobar@example.com

上記を見てもらうとわかりますが、ユーザ名、パスワードの次にEmailの入力があります。
「なんでユーザ名入力してるのにEmailの入力も必要なのかな〜」という疑問が湧いてきたので調べてみた。

実はnpm loginは取得されていないユーザ名を指定すると新しいアカウントを作る挙動になっていた。

github.com

loginという名前だったのでこの挙動にはちょっと驚いた。

loginはnpm adduserエイリアスなんだそう。
上記issueではloginとadduserは別々のコマンドに分割する方向で考えているというような事が書いてあった。

今日はここまで。