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

LINE風チャットWebアプリを作ってみる(初心者向け)

自己紹介

バックエンドエンジニアとして働いています!
仕事では主にJavaを使っていますが、趣味でGoやVueを触っています!最近は、flutterも触り始めました!twittergithubもやってます!お話ししましょうー

成果物

スクリーンショット 2020-01-19 22.20.02.png

スクリーンショット 2020-01-19 22.19.39.png

最後にGithubのコードも上がっているので参考にしてください!

できること

  • ルーム名とユーザ名を指定してルームに入ることができます。
  • そのルーム内でリアルタイムチャットができます。

内容はそこまで濃くはないのですが、
Webアプリをどういう風に作っていったかを見ていくことで
少しでもお役に立てればと思います。

使用技術

websocket

websocketはサーバとクライアントの間で双方向通信する仕組みです!リアルタイム通信で使われます。
今回はこの仕組みを用いて、ユーザの投稿を同じ部屋にいるユーザに対して送ってあげる処理を実現します。
line-gazo.jpg

解説

 ディレクトリ

.(line-chat-go)
├── Makefile
├── README.md
├── cmd
│   └── main.go
├── go.mod
├── go.sum
├── router.go
└── view
    ├── index.html
    ├── room.html
    └── static
        ├── css
        │   └── common.css
        ├── img
        │   └── icon.png
        └── js
            └── index.js

プロジェクトを作成する

$ cd line-chat-go
$ go mod init chat

まず、Goでは(version 1.11以上から)
go mod init (モジュール名)でモジュールを生成します。このモージュル名は、importする際のルートの名前になります。
例えば、 上のディレクトリのmain.goを指す場合は
import "chat/cmd"

となります。(今回はcmd以下がmainパッケージなので、importすることはありませんが。。。)

main.go を作る

cmd/main.go
package main

import (
    "chat"
)

func main() {
    chat.Run()
}         

たったこれだけです。
では、Run()メソッドを実装しているrouter.goを見てみましょう

router.go を作る

router.go
package chat

import (
    "github.com/gin-gonic/gin"
    melody "gopkg.in/olahol/melody.v1"
    "net/http"
)

func Run() {
    r := gin.Default()
    m := melody.New()

    r.Static("/static", "./view/static")
    r.LoadHTMLGlob("view/*.html")

    r.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{})
    })

    r.GET("/room/:name", func(c *gin.Context) {
        c.HTML(http.StatusOK, "room.html", gin.H{
            "Name": c.Param("name"),
        })
    })

    r.GET("/room/:name/ws", func(c *gin.Context) {
        m.HandleRequest(c.Writer, c.Request)
    })

    m.HandleMessage(func(s *melody.Session, msg []byte) {
        m.BroadcastFilter(msg, func(q *melody.Session) bool {
            return q.Request.URL.Path == s.Request.URL.Path
        })
    })

    r.Run(":8080")
}

まずimportを見てみましょう
ここでは
- ginというWebフレームワークを用いています。
- melodyは、WebSocketを簡単に利用することができるパッケージです。

少しだけginの書いている所を解説していきます。
いくつか抜き出します。

func Run() {
    r := gin.Default()
    r.Static("/static", "./view/static")
    r.LoadHTMLGlob("view/*.html")
    r.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{})
    })
    r.Run(":8080")
}

r := gin.Default()で、ginを初期化します。

r.Static("/static", "./view/static")は、view/staticフォルダ以下のファイルをhttp://xxxxx/static
に割り当てることができます
(サーバを実行した状態で
http://localhost:8080/static/img/icon.png
とブラウザに打つと、view/static/img/icon.pngを返してくれます)

r.LoadHTMLGlob("view/*.html")は、HTMLファイルを読み込みます。
staticとの違いは、テンプレートとして利用できるようになります。
テンプレートはGoの仕組みで文字列を埋め込んだりすることができます。

次に,ここがルーティングを設定します。

r.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{})
})

ルーム参加するためのページを表示する時、このページがリクエストされます。
urlで
http://xxxxx/
にGetアクセスが来たときに、index.htmlを返すという処理を書いています。
このindex.htmlはr.LoadHTMLGlob()でロードされているファイルから読みこみます。

最後に
r.Run(":8080")
は、8080ポートで待ち受けるということです。
ポートは、ネットを繋ぐ時の穴の番号で、urlに表示されていない(省略されている)ときは、80か443です。

次はginの応用編です。

r.GET("/room/:name", func(c *gin.Context) {
        c.HTML(http.StatusOK, "room.html", gin.H{
            "Name": c.Param("name"),
        })
})

/room/(名前)にリクエストが来たときに、room.htmlを返却するという意味です。
特定のルームに参加するときに、このページがリクエストされます。
この(名前)はc.Param("name")で動的に取得できます。
gin.H{}とは、テンプレートに渡す値を格納しておくものです。
今回だと"Name"というパラメータに:nameの値を代入しています。

<body>{{.Name}}</body>

と書いてあると、ginが自動的に名前を埋め込んでから、レスポンスを返してくれます。

残りはWebSocketの処理です。

m := melody.New()

r.GET("/room/:name/ws", func(c *gin.Context) {
    m.HandleRequest(c.Writer, c.Request)
})

m.HandleMessage(func(s *melody.Session, msg []byte) {
    m.BroadcastFilter(msg, func(q *melody.Session) bool {
        return q.Request.URL.Path == s.Request.URL.Path
    })
})

/room/:name/wsにリクエストが来たときは、webSocketの通信として、HandleMessageの処理を行います。
BroadcastFilter()は、ブロードキャストなので、クライアントから/room/:name/wsにリクエストが来ると、送られてきた値をサーバとつながっているクライアント全員に、送信します。
今回は、部屋名が同じクライアントのみに送りたいので、Filterをかけています。

index.html

view/index.html
<html>
  <head>
    <title>LINE</title>
    <link rel="stylesheet" href="/static/css/common.css">
  </head>

  <body>

    <div class="line">
      <div id="room" class="line-header">Join Room</div>
      <div id="chat" class="line-container"></div>
      <div class="line-form">
        <input placeholder="room" id="channel" type="text">
        <input placeholder="user name" id="user" type="text">
        <button id="join" class="line-form-button" onclick="send_data()">Join</button>
      </div>
    </div>

    <script>
      const chan = document.getElementById("channel");
      const join = document.getElementById("join");
      const user = document.getElementById("user");

      join.onclick = function () {
        if (chan.value != "") {
          localStorage.setItem("user",user.value)
           window.location = "/room/" + chan.value;
        }
      };

    </script>
  </body>
</html>

jsを使った処理を書いていますが、
joinしたときに、ローカルストレージにuserの名前を保存しています。
ローカルストレージとは、ブラウザにあるキーバリュー形式の保存場所です。ページが切り替わっても保持されています。

そのままだと見た目が悪いので、cssを追加しましょう

view/static/css/common.css
* {
    margin: 0px;
    padding: 0px;
}

.line-header{
    top:0;
    left: 0;
    height: 50px;
    width: 100vw;
    background-color:#253749;
    color:white;
    display: table-cell;
    text-align: center;
    vertical-align: middle;
}
.line-container{
    background-color:#49F;
    height: calc(100% - 100px);
    overflow: scroll;
}


.line-form{
    bottom: 0;
    left: 0;
    height: 50px;
    background-color: #eee;
    display:flex;
}

.line-form-button{
  color: #FFF;
  background: #03A9F4;/*色*/
  border: solid 1px #039fAA;/*線色*/
  border-radius: 5px;
  margin:8px;
  padding:1px 5px;
}

input{
    width: 80%;
    border-radius:5px;
    margin:8px 5px;
}
.line-right{
    position: relative;
    margin-right: 5%;
    float: right;
    display: block;
    max-width: 75%;
    margin: 5px 30px;
    clear: both;
}
.line-right::after{
    content: "";
    position: absolute;
    top: 3px; 
    right: -19px;
    border: 8px solid transparent;
    border-left: 18px solid #3c3;
    -webkit-transform: rotate(-35deg);
    transform: rotate(-35deg);
}
.line-right .line-right-text{
    background: #3c3;
    border-radius: 10px;
    padding:0.5em 1em ;
    word-break: break-all;
}

.line-right-time{
    color: white;
    size:0.2em;
    float: left;
}

.line-left{
  position: relative;
  padding: 10px;
  float: left;
    display: flex;
    margin: 2px 0;
    max-width: 75%;
    clear: both;
}

.line-left-container{
    margin-left:15px; 
    overflow: hidden;
}

.line-left .line-left-text{
    background: #eee;
    border-radius: 10px;
    padding:0.5em 1em ;
    word-break: break-all;
}
.line-left .line-left-text::after{
  content: "";
  display: block;
  position: absolute;
  top: 30px; 
  left: 50px;
  border: 8px solid transparent;
  border-right: 18px solid #edf1ee;
  -webkit-transform: rotate(35deg);
  transform: rotate(35deg);
}

.line-left-time{
    color: white;
    size:0.2em;
    float: right;
}

.line-left-name{
    color: white;
}

.line-left img{
    border-radius: 50%;
    width: 40px;
    height: 40px;
    border: #333;
    background-color: #eee;
}

@media screen and (max-width:600px){
    .line{
        height: 100%;
    }
}
@media screen and (min-width:601px){
    .line {
        height: 500px;
        width: 300px;
        margin: calc((100vh - 500px)/2) auto;
        border: solid 30px  #aaa;
        border-radius: 1em;
    }
}

すると
スクリーンショット 2020-01-19 23.13.39.png

こんな感じになってます。

room.html

room.html
<html>
  <head>
    <title>LINE</title>
    <meta name="viewport" content="width=device-width">
    <link rel="stylesheet" href="/static/css/common.css">
  </head>
  <body>
    <div class="line">
      <div id="room" class="line-header">{{.Name}}</div>
      <div id="chat" class="line-container"></div>
      <div class="line-form">
        <input id="text" type="text">
        <button class="line-form-button" onclick="send_data()">Send</button> 
      </div>
    </div>
    <script type="text/javascript" src="/static/js/index.js"></script>
  </body>
</html>

見た目は、index.htmlと同じです。
特徴としては、room.htmlはテンプレートとして読み込んでいるため、Goのサーバ側で{{.Name}}にルーム名が埋め込まれています。

<div id="room" class="line-header">{{.Name}}</div>

さらにここでは、webSocketでサーバと通信する必要があるため、その処理をjs/index.jsに記述していきます。

view/static/js/index.js
const url = "ws://" + window.location.host + window.location.pathname + "/ws";
const ws = new WebSocket(url);
const name = localStorage.getItem("user")
const chat = document.getElementById("chat");

const text = document.getElementById("text");

ws.onmessage = function (msg) {
  let obj = JSON.parse(msg.data);
  obj.message = escape_html(obj.message);
  let line ="";
  if (obj.name==name){
    line =`<div class='line-right'>
            <p class='line-right-text'>${obj.message} </p>
            <div class="line-right-time">${now()}</div>
           </div>`
  }else{
    let image = '<img src="/static/img/icon.png"/>'
    line =`<div class='line-left'>
                ${image}
                <div class='line-left-container'>
                    <p class='line-left-name'>
                    ${obj.name}
                    </p>
                    <p class='line-left-text'>
                    ${obj.message}
                    </p>
                    <div class='line-left-time'>
                        ${now()}
                    </div>
                </div>
           </div>`
  }
  chat.innerHTML += line;
};

text.onkeydown = function (e) {
  if (e.keyCode === 13) {
    send_data();
  }
};

function send_data(){
    if (text.value == "")return;
    text.value = escape_html(text.value);
    let sendData = `{"name":"${name}","message":"${text.value}"}`;
    ws.send(sendData);
    text.value = "";
}

function now() {
    let date = new Date();
    let min = (date.getMinutes()<10)?`0${date.getMinutes()}`:date.getMinutes();
    let hour = (date.getHours()<10)?`0${date.getHours()}`:date.getHours();
    return `${hour}:${min}`
};

function escape_html (string) {
    if(typeof string !== 'string') {
      return string;
    }
    return string.replace(/[&'`"<>]/g, function(match) {
      return {
        '&': '&amp;',
        "'": '&#x27;',
        '`': '&#x60;',
        '"': '&quot;',
        '<': '&lt;',
        '>': '&gt;',
      }[match]
    });
}

少し説明していきます。

const url = "ws://" + window.location.host + window.location.pathname + "/ws";
const ws = new WebSocket(url);

jsファイルが読み込まれるときに、websocketでサーバに通信しています。

ws.onmessage = function (msg) {
  let obj = JSON.parse(msg.data);
};

ここでは、Goのサーバからブロードキャストでデータが送られてきたときに実行されます。

function send_data(){
    if (text.value == "")return;
    text.value = escape_html(text.value);
    let sendData = `{"name":"${name}","message":"${text.value}"}`;
    ws.send(sendData);
    text.value = "";
}

逆にサーバに値を送るときは、ws.send()を利用して送信します。

{
    "name":"user",
    "message":"メッセージ"
}

基本的にデータ構造はjson型で以下のように送受信されています。

実行してみる

$ go run cmd/main.go

ブラウザで、http://localhost:8080/
とうって確認してみてください。
タブで二つ開いて、同じ部屋にJoinすると、LINEの見た目でリアルタイム通信ができるはずです。
line2.gif

これで以上です。

最後に

今回はWebSocketを使って、Line風チャットを作ってみました。
githubにコードをあげていますー!
https://github.com/ryomak/line-chat-go
少しでもWeb制作のお役に立てれば良いなと思います!

Go生活楽しみましょー!

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

AtCoder Beginner Contest 152 参戦記

AtCoder Beginner Contest 152 参戦記

ABC152A - AC or WA

1分半で突破. 書くだけ.

N, M = map(int, input().split())

if N == M:
    print('Yes')
else:
    print('No')

ABC152B - Comparing Strings

2分で突破. 書くだけ.

a, b = map(int, input().split())

if a < b:
    print(str(a) * b)
else:
    print(str(b) * a)

ABC152C - Low Elements

5分で突破. 流石に二重ループは TLE なので、現時点の最小値を持ち回す必要あり. 題意を理解するのに少し時間を使った.

N = int(input())
P = list(map(int, input().split()))

result = 0
m = P[0]
for i in range(N):
    if P[i] <= m:
        result += 1
        m = P[i]
print(result)

ABC152D - Handstand 2

26分で突破. N回ループ回しても大丈夫だと見切れればさして難しくはない. 先頭と末尾の組み合わせで個数を集計すれば一発.

N = int(input())

t = [[0] * 10 for _ in range(10)]
for i in range(1, N + 1):
    s = str(i)
    t[int(s[0])][int(s[-1])] += 1

result = 0
for i in range(1, 10):
    for j in range(1, 10):
        result += t[i][j] * t[j][i]
print(result)

ABC152E - Flatten

敗退. 最小公倍数を求めて集計するナイーブな実装では TLE だった.

追記: 解説通り実装した.

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
)

func mpow(x int, n int) int {
    result := 1
    for n != 0 {
        if n&1 == 1 {
            result *= x
            result %= 1000000007
        }
        x *= x
        x %= 1000000007
        n >>= 1
    }
    return result
}

func main() {
    maxA := 1000000

    N := readInt()
    A := make([]int, N)
    for i := 0; i < N; i++ {
        A[i] = readInt()
    }

    sieve := make([]int, maxA+1)
    sieve[0] = -1
    sieve[1] = -1
    for i := 2; i <= maxA; i++ {
        if sieve[i] != 0 {
            continue
        }
        sieve[i] = i
        for j := i * i; j <= maxA; j += i {
            if sieve[j] == 0 {
                sieve[j] = i
            }
        }
    }

    lcmFactors := map[int]int{}
    for i := 0; i < N; i++ {
        t := map[int]int{}
        a := A[i]
        for a != 1 {
            t[sieve[a]]++
            a /= sieve[a]
        }
        for k, v := range t {
            if lcmFactors[k] < v {
                lcmFactors[k] = v
            }
        }
    }

    lcm := 1
    for k, v := range lcmFactors {
        for i := 0; i < v; i++ {
            lcm *= k
            lcm %= 1000000007
        }
    }

    result := 0
    for i := 0; i < N; i++ {
        result += lcm * mpow(A[i], 1000000007-2)
        result %= 1000000007
    }
    fmt.Println(result)
}

const (
    ioBufferSize = 1 * 1024 * 1024 // 1 MB
)

var stdinScanner = func() *bufio.Scanner {
    result := bufio.NewScanner(os.Stdin)
    result.Buffer(make([]byte, ioBufferSize), ioBufferSize)
    result.Split(bufio.ScanWords)
    return result
}()

func readString() string {
    stdinScanner.Scan()
    return stdinScanner.Text()
}

func readInt() int {
    result, err := strconv.Atoi(readString())
    if err != nil {
        panic(err)
    }
    return result
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

test投稿

test投稿

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

atomic, mutex どうなんだろう・・・ mutex が早いだと・・・ ニャ

所感

ちゃんと調べないとダメそう・・・
Lock 有りが何もしないより早いなんて・・・

概要

var num int64
num++

単純な数字カウントアップ atomic が早そうだなーなのでベンチマーク撮ってみよ

  • 何もせずに num++
  • mutex.Loc() num++ mutex.Unlock()
  • atomic.AddInt64
  • atomic.ValueLoad(), Store()

コード

カウントアップ

main.go
import (
    "sync"
    "sync/atomic"
)

var mutex sync.Mutex
var value atomic.Value

type Counter int64

func (c *Counter) Inc() {
    *c++
}

func (c *Counter) AtomicInc() {
    atomic.AddInt64((*int64)(c), 1)
}

func (c *Counter) LockInc() {
    mutex.Lock()
    defer mutex.Unlock()
    *c++
}

func incValue() {
    cnt := value.Load().(Counter)
    cnt++
    value.Store(cnt)
}

func countup(inc func(), n int) {
    var wg sync.WaitGroup
    for i := 0; i < n; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            inc()
        }()
    }
    wg.Wait()
}

ベンチマーク

main_test.go
package main

// go test -bench . | grep 'ns/op'

import (
    "testing"
)

func BenchmarkInc_Countup(b *testing.B) {
    var cnt Counter
    b.ResetTimer()
    countup(cnt.Inc, b.N)
    b.Logf("%d -> %d", b.N, cnt)
}

func BenchmarkLock_Countup(b *testing.B) {
    var cnt Counter
    b.ResetTimer()
    countup(cnt.LockInc, b.N)
    b.Logf("%d -> %d", b.N, cnt)
}

func BenchmarkAtomic_Countup(b *testing.B) {
    var cnt Counter
    b.ResetTimer()
    countup(cnt.AtomicInc, b.N)
    b.Logf("%d -> %d", b.N, cnt)
}

func BenchmarkAtomicValue_Countup(b *testing.B) {
    var cnt Counter
    value.Store(cnt)
    b.ResetTimer()
    countup(incValue, b.N)
    cnt = value.Load().(Counter)
    b.Logf("%d -> %d", b.N, cnt)
}

実行結果

go test -bench .

  • atomic.Value が速い事は、いいとして Mutexatomic.AddInt64 より速いことが解せぬ
  • 何もしないとカウントアップの数合わないです(想定通り)他は大丈夫
$ go test -bench .
goos: linux
goarch: amd64
BenchmarkInc_Countup-4               2000000           957 ns/op
--- BENCH: BenchmarkInc_Countup-4
    acounter_test.go:13: 1 -> 1
    acounter_test.go:13: 100 -> 100
    acounter_test.go:13: 10000 -> 9752
    acounter_test.go:13: 1000000 -> 982973
    acounter_test.go:13: 2000000 -> 1962072
BenchmarkLock_Countup-4              2000000           936 ns/op
--- BENCH: BenchmarkLock_Countup-4
    acounter_test.go:20: 1 -> 1
    acounter_test.go:20: 100 -> 100
    acounter_test.go:20: 10000 -> 10000
    acounter_test.go:20: 1000000 -> 1000000
    acounter_test.go:20: 2000000 -> 2000000
BenchmarkAtomic_Countup-4            2000000           943 ns/op
--- BENCH: BenchmarkAtomic_Countup-4
    acounter_test.go:27: 1 -> 1
    acounter_test.go:27: 100 -> 100
    acounter_test.go:27: 10000 -> 10000
    acounter_test.go:27: 1000000 -> 1000000
    acounter_test.go:27: 2000000 -> 2000000
BenchmarkAtomicValue_Countup-4       2000000           904 ns/op
--- BENCH: BenchmarkAtomicValue_Countup-4
    acounter_test.go:36: 1 -> 1
    acounter_test.go:36: 100 -> 54
    acounter_test.go:36: 10000 -> 7125
    acounter_test.go:36: 1000000 -> 791697
    acounter_test.go:36: 2000000 -> 1586774
PASS

複数回の実行結果

複数回実行すれば・・・ atomic.AddInt 早くなるはずでは・・・

atomic.Value > Mutex > なにもしない > atomic.AddInt こ、これは・・・詳細なチェックしないとフラグでは・・・

$ for i in $(seq 1 3); do go test -bench . | grep 'ns/op'; done
BenchmarkInc_Countup-4               2000000           998 ns/op
BenchmarkInc_Countup-4               2000000           989 ns/op
BenchmarkInc_Countup-4               1000000          1018 ns/op

BenchmarkLock_Countup-4              1000000          1113 ns/op
BenchmarkLock_Countup-4              2000000           986 ns/op
BenchmarkLock_Countup-4              1000000          1311 ns/op

BenchmarkAtomic_Countup-4            1000000          1010 ns/op
BenchmarkAtomic_Countup-4            2000000          1130 ns/op
BenchmarkAtomic_Countup-4            1000000          1051 ns/op

BenchmarkAtomicValue_Countup-4       2000000          1013 ns/op
BenchmarkAtomicValue_Countup-4       2000000           987 ns/op
BenchmarkAtomicValue_Countup-4       1000000          1125 ns/op

Recap

  • 実装依存なら、コードを見て原因を探す
  • OS依存なら Profile とかしながら、ググったりして原因を探す
  • ハード依存なら・・・
  • ドキュメントに書いてあるとうれしいな

参考

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

goroutineが難しい

結論

selectを忘れずにつける

目的

0.5秒ごとにカウント 0, 1, 2, ... を送るチャネルを取得するライブラリを作りたい。

実装1. 単純な実装

lib.go
package main

import "time"

// 0.5秒ごとにカウントを送るチャネルを返す
func NewInterval() <-chan int {
    ch := make(chan int)

    count := 0
    go func() {
        for {
            time.Sleep(500 * time.Millisecond)
            ch <- count
            count++
        }
    }()

    return ch
}

使用例

main.go
package main

import (
    "fmt"
)

func main() {
    ch := NewInterval()
    for {
        fmt.Println(<-ch)
    }
}

実装1の問題点

チャネルからのデータの受け取りをやめた後もNewInterval関数内のgoroutineが生き続け,メモリを圧迫する(goroutine リーク)。

main.go
package main

import (
    "fmt"
    "runtime"
)

// こんな感じで関数内で呼び出すと,goroutine リークがあることに気づきにくい
func f() {
    ch := NewInterval()

    // 最初の3つだけ受け取る
    for i := 0; i < 3; i++ {
        fmt.Println(<-ch)
    }
}

func main() {
    // goroutineは1個
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())

    f()

    // ここで何らかの処理をする...
    time.Sleep(500 * time.Millisecond)

    // goroutineは2個 ... goroutine リーク!
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())
}

実装2. goroutineをキャンセル可能にする

公式の A Tour of Go のコードと同様に <-chan int を使って,goroutineをキャンセル可能にする。

lib.go
package main

import "time"

// 0.5秒ごとにカウントを送るチャネルを返す。quitにデータが送られたら終了する。
func NewInterval(quit <-chan int) <-chan int {
    ch := make(chan int)

    count := 0
    go func() {
        for {
            // 0.5秒待つ。もし待っている間にquitからデータが送られてきたらgoroutineを終了する。
            // ※ time.NewTimer(500 * time.Millisecond).C は0.5秒たつとデータが送られてくるチャネル。
            select {
            case <-time.NewTimer(500 * time.Millisecond).C:
            case <-quit:
                return
            }

            ch <- count
            count++
        }
    }()

    return ch
}

使用例

main.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func f() {
    quit := make(chan int)
    ch := NewInterval(quit)

    defer func() {
        // 関数fを抜けた時点で,NewInterval内のgoroutineを終了させる
        quit <- 0
    }()

    // 最初の3つだけ受け取る
    for i := 0; i < 3; i++ {
        fmt.Println(<-ch)
    }
}

func main() {
    // goroutineは1個
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())

    f()

    // ここで何らかの処理をする...
    time.Sleep(500 * time.Millisecond)

    // goroutineは1個 ... goroutine リークを防げた!
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())
}


実装2の問題点(1)

ch からデータを受け取る間隔が長くなると,デッドロックが起きる。

main.go
package main

import (
    "fmt"
    "time"
)

func main() {
    quit := make(chan int)
    ch := NewInterval(quit)

    fmt.Println(<-ch)

    // ここで0.5秒以上かかる処理をする(0.5秒だと長いが,たとえばNewIntervalの間隔が0.001秒とかに設定されていたら,容易に超える)
    time.Sleep(time.Second)

    // fatal error: all goroutines are asleep - deadlock!
    quit <- 0
}

quitの容量を1以上にするとdead lockを防げるように見えるが,そうするとgoroutine リークが起きる。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    // goroutineの数: 1
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())

    // quitの容量を1にした
    quit := make(chan int, 1)
    ch := NewInterval(quit)

    fmt.Println(<-ch)

    // 少し待つ
    time.Sleep(time.Second)

    quit <- 0

    // 少し待つ
    time.Sleep(time.Second)

    // goroutineの数: 2
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())
}

実装3. データの送信にもselectをつける

すべてのチャネル操作をきちんとselectで囲む必要がある。

lib.go
package main

import "time"

// 0.5秒ごとにカウントを送るチャネルを返す。quitにデータが送られたら終了する。
func NewInterval(quit <-chan int) <-chan int {
    ch := make(chan int)

    count := 0
    go func() {
        for {
            // 0.5秒待つ。もし待っている間にquitからデータが送られてきたらgoroutineを終了する。
            // ※ time.NewTimer(500 * time.Millisecond).C は0.5秒たつとデータが送られてくるチャネル。
            select {
            case <-time.NewTimer(500 * time.Millisecond).C:
            case <-quit:
                return
            }

            // countを送信する。送信を試行している間にquitからデータが送られてきたらgoroutineを終了する。
            select {
            case ch <- count:
            case <-quit:
                return
            }

            count++
        }
    }()

    return ch
}

実装4. contextを使う

チャネルにデータを送ってgoroutineを終了させるのはわかりにくいため,contextを使うと良いかもしれない。contextは標準ライブラリに入っている。

lib.go
package main

import (
    "context"
    "time"
)

// 0.5秒ごとにカウントを送るチャネルを返す。quitにデータが送られたら終了する。
func NewInterval(ctx context.Context) <-chan int {
    ch := make(chan int)

    count := 0
    go func() {
        for {
            // 0.5秒待つ。もし待っている間にquitからデータが送られてきたらgoroutineを終了する。
            // ※ time.NewTimer(500 * time.Millisecond).C は0.5秒たつとデータが送られてくるチャネル。
            select {
            case <-time.NewTimer(500 * time.Millisecond).C:
            case <-ctx.Done():
                return
            }

            // countを送信する。送信を試行している間にquitからデータが送られてきたらgoroutineを終了する。
            select {
            case ch <- count:
            case <-ctx.Done():
                return
            }

            count++
        }
    }()

    return ch
}

main.go
package main

import (
    "context"
    "fmt"
    "runtime"
    "time"
)

func f() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    ch := NewInterval(ctx)

    fmt.Println(<-ch)
}

func main() {
    // goroutineの数: 1
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())

    f()

    // 少し待つ
    time.Sleep(time.Second)

    // goroutineの数: 1
    fmt.Printf("goroutineの数: %v\n", runtime.NumGoroutine())
}

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

jsを書かずにGoだけでgithubのトレンドを表示するwebサイトをつくってみた(WebAssembly)

自己紹介

仕事では主にJavaを使っていますが、趣味でGoを触っています。

twitter:@kurisuuu3
github:ryomak

成果物

WebAssemblyの勉強をする目的でしたが、どうせならとスローガンを掲げて作りました!

GoのGoによるGoのためのWebサイト

を掲げ、javascriptを書かず
Goで書かれたGithubリポジトリのトレンドを表示するWebサイトを作りました。
(https://github.com/trending/go を見ればそもそもトレンド見れますw)

wasm-gif.gif

成果物のコード

どう実現するのか

WebAssembly

ブラウザからアセンブリを実行できるようにする技術です
モダンなWebブラウザは対応してるそうです!

生まれた経緯は
WebAssemblyとは?〜実際にC言語をブラウザで動かす〜【2019年6月版】
がわかりやすかったです

以下wasmとします。

構成

スクリーンショット 2020-01-19 14.15.41.png

wasmファイルがJsと同様の処理を行います。今回はdom操作とhttp通信です。
※ 直接Githubにはアクセスできません。javascript等と同じで、ブラウザのCORSに引っかかります。
そのため、同じオリジンのapi経由で取得しにきます。

CORSについて

動作環境

go1.12 darwin/amd64
Google Chrome バージョン: 79.0.3945.130(Official Build) (64 ビット)

です!
Go1.11からWebAssemblyが使えるのですが、Go1.12からいくつかの関数名が変わったりしているので、気をつけてください
今回はバージョンは影響する関数は用いていません。

作る

ファイル構成

.
├── Makefile
├── README.md
├── common
│   └── trend.go
├── go.mod
├── go.sum
├── main.go
├── server.go
└── static
    ├── index.css
    ├── index.html
    ├── main.wasm
    └── wasm_exec.js

server.go(サーバを作る)

必要な機能は以下です。

  • 静的ファイル(index.html...)用のファイルサーバ
  • Githubからトレンドを取得する
  • 取得したトレンドを返すAPI

静的ファイル(index.html...)用のファイルサーバ

server.go
package main

import(
 "net/http"
)

func main() {
    http.Handle("/", http.FileServer(http.Dir("./static")))

    http.ListenAndServe(":8080", nil)
}

これで、staticフォルダ以下のファイルをhttp://localhost.com:8080/
にマッピングできます。

Githubからトレンドを取得する

Githubからスクレイピングで、トレンドを持ってきます。
トレンドをスクレイピングするパッケージを予め用意しておきました。

このパッケージを用いて、トレンドを取得します。
トレンドを今回のAPI用に構造体を作っておきます。

common/trend.go
package common


type Repository struct{
    Name string `json:"name"`
    User string `json:"user"`
    Star uint`json:"star"`
    URL string  `json:"url"`
    Description string `json:"description"`
}

type Repositories []Repository

json:"hogehoge"
としておくことで、レスポンスのパラメータ名を設定できます

取得したトレンドを返すAPI

server.go
func trendHandler(w http.ResponseWriter, r *http.Request) {
  client := trending.NewClient()
    repos, err := client.GetRepository(trending.TIME_WEEK, "go")
    if err != nil {
        json.NewEncoder(w).Encode([]common.Repository{})
        return
    }
    list := []common.Repository{}
    for _, repo := range repos {
        list = append(list, common.Repository{
            Name: repo.Name,
            User: repo.Owner,
            URL:  repo.URL,
            Star: repo.Star,
            Description: repo.Description,
        })
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(list)
    return
}

最初にトレンドを取得してきてから、
先ほど作成したRepository構造体にデータを詰め込みます。
json.NewEncoder(w).Encode(list)でjson形式で返しています。

server.go完成

server.go
package main

import(
 "net/http"
 "encoding/json"
 "example/common"
 trending "github.com/ryomak/go-trending"
)

var client = trending.NewClient()

func main() {
    http.HandleFunc("/trend", trendHandler)
    http.Handle("/", http.FileServer(http.Dir("./static")))

    http.ListenAndServe(":8080", nil)
}

func trendHandler(w http.ResponseWriter, r *http.Request) {
    // Show projects of today
    repos, err := client.GetRepository(trending.TIME_WEEK, "go")
    if err != nil {
        json.NewEncoder(w).Encode([]common.Repository{})
        return
    }
    list := []common.Repository{}
    for _, repo := range repos {
        list = append(list, common.Repository{
            Name: repo.Name,
            User: repo.Owner,
            URL:  repo.URL,
            Star: repo.Star,
            Description: repo.Description,
        })
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(list)
    return
}

staticフォルダ以下の作成

server.goを作成したことで,staticフォルダ以下を配信できるようになりました。
まず、index.htmlファイルを見ていきます

index.html

<html>
    <head>
       <meta charset="utf-8">
       <meta name="viewport" content="width=device-width,initial-scale=1.0">
       <link rel="stylesheet" type="text/css" href="/index.css">
       <script src="wasm_exec.js"></script>
       <script>
         const go = new Go();
           WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
               go.run(result.instance);
           });
       </script>
   </head>
    <body>
      <div id="app"></div>
    </body>
</html>   
wasm_exec.js
<script src="wasm_exec.js"></script>

wasmはwasm単体では動作しません。wasmはjsから読み込む処理が必要です。
読み込む処理をwasm_exec.jsが行います。このファイルは提供されてるので、ダウロードします

$ cd static && curl -sO https://raw.githubusercontent.com/golang/go/release-branch.go1.12/misc/wasm/wasm_exec.js
script処理
index.html
<script>
         const go = new Go();
           WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
               go.run(result.instance);
           });
</script>

ここでは、wasmファイルを取得して実行するところです。

index.css

スタイルを当てます

h1{
  text-align: center;
  font: #f1f1f1;
}

.trend{
    width:80vw;
  padding: 1em;
  margin:0 auto;
    display:flex;
  flex-wrap: wrap;
  justify-content:center;
}

.item {
  background: #f1f1f1;
  margin: .5em;
    width:100%;
}


@media (min-width: 801px) {
  .item {
    width: 30%;
  }
}

@media (min-width: 600px) and (max-width: 800px){
  .item {
    width: 40%;
  }
}

.item-title {
    font-size: 1.2em;
    background: #24292e;
    text-align: center;
    display: table-cell;
  vertical-align: middle;
    color: #fafafa;
    font-weight: bold;
  height: 100px;
  width:10%;
  padding:1em;
}

.item:hover .item-description {
  display: block;
}

.item:hover .item-title {
  display: none;
}

.item-description {
  display:none;
  color: #24292e;
    height: 100px;
  word-break : break-all;
    overflow: scroll;
}

a {
  text-decoration: none;
    color: #111;
}
a:hover {
  text-decoration: none;
}

/* loading */
.sk-chase {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 45%;
  left: 45%;
  margin: :auto;
  animation: sk-chase 2.5s infinite linear both;
}

.sk-chase-dot {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0; 
  animation: sk-chase-dot 2.0s infinite ease-in-out both; 
}

.sk-chase-dot:before {
  content: '';
  display: block;
  width: 25%;
  height: 25%;
  background-color: #3a5;
  border-radius: 100%;
  animation: sk-chase-dot-before 2.0s infinite ease-in-out both; 
}

.sk-chase-dot:nth-child(1) { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2) { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3) { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4) { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5) { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6) { animation-delay: -0.6s; }
.sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; }

@keyframes sk-chase {
  100% { transform: rotate(360deg); }
}

@keyframes sk-chase-dot {
  80%, 100% { transform: rotate(360deg); }
}

@keyframes sk-chase-dot-before {
  50% {
    transform: scale(0.4);
  } 100%, 0% {
    transform: scale(1.0);
  }
}

main.wasm

goで書かれたwasmファイルです。
生成は後ほど説明します。

wasmファイルの作成

ここから本題のwasmを作成していきます。
wasmはGoで書かれたファイルをbuildした生成物です。
GOOSとGOARCHをwasm用にする以外は通常のbuildと同じです。

$ GOOS=js GOARCH=wasm go build -o static/main.wasm main.go

main.go

基本的にはjsで利用しているメソッドを使えるので、普段jsを書く人ならそこまで苦労しないかなと思いました。
(普段書かないので、手間取りました)

package main

import (
    "encoding/json"
    "example/common"
    "fmt"
    "net/http"
    "syscall/js"
)

var document = js.Global().Get("document")
var app = js.Global().Get("document").Call("getElementById", "app")

func main() {
    done := make(chan bool)
    go loading(done)
    title()
    go fetch(done)
    select {}
}

func title() {
    t := createElement("h1")
    t.Set("textContent", "go-trending in github")
    app.Call("appendChild", t)
}


func getTrend() (common.Repositories, error) {
    r, err := http.Get("/trend")
    defer r.Body.Close()
    if err != nil {
        return common.Repositories{}, err
    }
    repos := common.Repositories{}
    err = json.NewDecoder(r.Body).Decode(&repos)
    return repos, err
}

func fetch(done chan bool) {
    trendListDiv := createElement("div")
    class := trendListDiv.Get("classList")
    class.Call("add", "trend")
    app.Call("appendChild", trendListDiv)

    trends, _ := getTrend()
    done <- true
    for _, trend := range trends {
        trendListDiv.Call("appendChild", createItemByTrend(trend))
    }
}

func createItemByTrend(repo common.Repository) js.Value {
    item := createElement("div")
    class := item.Get("classList")
    class.Call("add", "item")

    itemLink := createElement("a")
    itemLink.Set("href", repo.URL)

    itemTitle := createElement("div")
    itemTitle.Set("textContent", fmt.Sprintf("%s/%s (☆ %d)", repo.User, repo.Name, repo.Star))
    class = itemTitle.Get("classList")
    class.Call("add", "item-title")

    itemDescription := createElement("div")
    itemDescription.Set("textContent", repo.Description)
    class = itemDescription.Get("classList")
    class.Call("add", "item-description")

    item.Call("appendChild", itemLink)
    itemLink.Call("appendChild", itemTitle)
    itemLink.Call("appendChild", itemDescription)
    return item
}

func loading(done chan bool) {
    chase := createElement("div")
    class := chase.Get("classList")
    class.Call("add", "sk-chase")
    app.Call("appendChild", chase)
    dots := []js.Value{}
    for i := 0; i < 6; i++ {
        chaseDot := createElement("div")
        class = chaseDot.Get("classList")
        class.Call("add", "sk-chase-dot")
        chase.Call("appendChild", chaseDot)
        dots = append(dots, chaseDot)
    }
    <-done
    style := chase.Get("style")
    style.Set("display", "none")

}

func createElement(elementName string) js.Value {
    return document.Call("createElement", elementName)
}

main処理

機能は、トレンドを取得する間ローディングを表示し、取得し終わるとトレンド一覧が出てきます。

func main() {
    done := make(chan bool)
    title()
    go loading(done)
    go fetch(done)
    select {}
}
  • title()は、サイトタイトルを表示するための関数です。中では、h1タグを生成しています。
  • fetch()はトレンドをサーバから取得して、一覧を表示します。中では、apiを叩き、domを生成していきます。
  • loading()はトレンドを取得中にローディングを出します。

doneについて、
Go特有のチャネルを使って、fetch()の内部で、トレンドを取得し終わるのを監視して、終わったことをloading()に伝えます。
select{}を用いて、main関数の処理が終了するのをブロックしています。

共通処理

先にこのコード内での共通処理を説明しておきます。

var document = js.Global().Get("document")
var app = js.Global().Get("document").Call("getElementById", "app")

func createElement(elementName string) js.Value {
    return document.Call("createElement", elementName)
}
  • documentはjsのdocumentと同じです。
  • appは、id=appのelementを取得しています。(getElementByIdはjsのメソッド名です)
  • createElement(name)はdomを生成します。
title()
func title() {
    t := createElement("h1")
    t.Set("textContent", "go-trending in github")
    app.Call("appendChild", t)
}

h1タグを生成し、中に"go-trending in github"を挿入し、appの子要素として挿入します。
書き方は違いますが、jsと同じメソッド名で処理をしています。

loading()
func loading(done chan bool) {
    chase := createElement("div")
    class := chase.Get("classList")
    class.Call("add", "sk-chase")
    app.Call("appendChild", chase)
    dots := []js.Value{}
    for i := 0; i < 6; i++ {
        chaseDot := createElement("div")
        class = chaseDot.Get("classList")
        class.Call("add", "sk-chase-dot")
        chase.Call("appendChild", chaseDot)
        dots = append(dots, chaseDot)
    }
    <-done
    style := chase.Get("style")
    style.Set("display", "none")

}

appの子要素に以下のようなelementを生成しています。

<div class="sk-chase">
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
</div>

特筆事項としては、

    <-done
    style := chase.Get("style")
    style.Set("display", "none")

doneチャネルに値が入ってくるまで<-doneでブロックし、値が入ってくると、style属性にdisplay:noneを追加し、見えないようにします・

fetch
func fetch(done chan bool) {
    trendListDiv := createElement("div")
    class := trendListDiv.Get("classList")
    class.Call("add", "trend")
    app.Call("appendChild", trendListDiv)

    trends, _ := getTrend()
    done <- true
    for _, trend := range trends {
        trendListDiv.Call("appendChild", createItemByTrend(trend))
    }

}

func getTrend() (common.Repositories, error) {
    r, err := http.Get("/trend")
    defer r.Body.Close()
    if err != nil {
        return common.Repositories{}, err
    }
    repos := common.Repositories{}
    err = json.NewDecoder(r.Body).Decode(&repos)
    return repos, err
}

getTrend()で、http通信でトレンドを取得し、構造体にマッピングしています。
取得したレポジトリ毎にdomを生成しています。
取得してから、doneチャネルにtrueという値を送信しています。
fetchでは、以下のようなElementを生成しています。

<div class="trend">
    <div class="item">
        <a href="">
            <div class="item-title"></div>
            <div class="item-description"></div>
        </a>
    </div>
    ...
</div>

実行

$ GOOS=js GOARCH=wasm go build -o static/main.wasm main.go
$ go run server.go

感想

共通処理が書ける

今回、common/trend.goをserverとwasm両方で利用しました。
そのためapiの通信の際、同じ構造体を利用できるので、マッピングが楽でした。
grpcと同じ感じがしました。

Vugu

https://github.com/vugu/vugu
という、wasmでReactやVue風にUIを書くことができるパッケージがあるらしいので、そっちを使うと管理が楽っぽいです

終わり

初めてwasmを触りましたが、wasmのファイルサイズが大きかったり、まだ課題はありそうですが、js以外にもウェブ制作の手段が広がったのは良いことだなと思いました。

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

jsを書かずにGoだけでwebサイトをつくってみた(WebAssembly)

自己紹介

バックエンドエンジニアとして働いています!
仕事では主にJavaを使っていますが、趣味でGoやVueを触っています!最近は、flutterも触り始めました!twittergithubもやってます!お話ししましょうー

成果物

WebAssemblyの勉強をする目的でしたが、どうせならとスローガンを掲げて作りました!

GoのGoによるGoのためのWebサイト

を掲げ、javascriptを書かず
Goで書かれたGithubリポジトリのトレンドを表示するWebサイトを作りました。
(https://github.com/trending/go を見ればそもそもトレンド見れますw)

wasm-gif.gif

成果物のコード

どう実現するのか

WebAssembly

ブラウザからアセンブリを実行できるようにする技術です
モダンなWebブラウザは対応してるそうです!

生まれた経緯は
WebAssemblyとは?〜実際にC言語をブラウザで動かす〜【2019年6月版】
がわかりやすかったです

以下wasmとします。

構成

スクリーンショット 2020-01-19 14.15.41.png

wasmファイルがJsと同様の処理を行います。今回はdom操作とhttp通信です。
※ 直接Githubにはアクセスできません。javascript等と同じで、ブラウザのCORSに引っかかります。
そのため、同じオリジンのapi経由で取得しにきます。

CORSについて

動作環境

go1.12 darwin/amd64
Google Chrome バージョン: 79.0.3945.130(Official Build) (64 ビット)

です!
Go1.11からWebAssemblyが使えるのですが、Go1.12からいくつかの関数名が変わったりしているので、気をつけてください
今回はバージョンは影響する関数は用いていません。

作る

ファイル構成

.
├── Makefile
├── README.md
├── common
│   └── trend.go
├── go.mod
├── go.sum
├── main.go
├── server.go
└── static
    ├── index.css
    ├── index.html
    ├── main.wasm
    └── wasm_exec.js

server.go(サーバを作る)

必要な機能は以下です。

  • 静的ファイル(index.html...)用のファイルサーバ
  • Githubからトレンドを取得する
  • 取得したトレンドを返すAPI

静的ファイル(index.html...)用のファイルサーバ

server.go
package main

import(
 "net/http"
)

func main() {
    http.Handle("/", http.FileServer(http.Dir("./static")))

    http.ListenAndServe(":8080", nil)
}

これで、staticフォルダ以下のファイルをhttp://localhost.com:8080/
にマッピングできます。

Githubからトレンドを取得する

Githubからスクレイピングで、トレンドを持ってきます。
トレンドをスクレイピングするパッケージを予め用意しておきました。

このパッケージを用いて、トレンドを取得します。
トレンドを今回のAPI用に構造体を作っておきます。

common/trend.go
package common


type Repository struct{
    Name string `json:"name"`
    User string `json:"user"`
    Star uint`json:"star"`
    URL string  `json:"url"`
    Description string `json:"description"`
}

type Repositories []Repository

json:"hogehoge"
としておくことで、レスポンスのパラメータ名を設定できます

取得したトレンドを返すAPI

server.go
func trendHandler(w http.ResponseWriter, r *http.Request) {
  client := trending.NewClient()
    repos, err := client.GetRepository(trending.TIME_WEEK, "go")
    if err != nil {
        json.NewEncoder(w).Encode([]common.Repository{})
        return
    }
    list := []common.Repository{}
    for _, repo := range repos {
        list = append(list, common.Repository{
            Name: repo.Name,
            User: repo.Owner,
            URL:  repo.URL,
            Star: repo.Star,
            Description: repo.Description,
        })
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(list)
    return
}

最初にトレンドを取得してきてから、
先ほど作成したRepository構造体にデータを詰め込みます。
json.NewEncoder(w).Encode(list)でjson形式で返しています。

server.go完成

server.go
package main

import(
 "net/http"
 "encoding/json"
 "example/common"
 trending "github.com/ryomak/go-trending"
)

var client = trending.NewClient()

func main() {
    http.HandleFunc("/trend", trendHandler)
    http.Handle("/", http.FileServer(http.Dir("./static")))

    http.ListenAndServe(":8080", nil)
}

func trendHandler(w http.ResponseWriter, r *http.Request) {
    // Show projects of today
    repos, err := client.GetRepository(trending.TIME_WEEK, "go")
    if err != nil {
        json.NewEncoder(w).Encode([]common.Repository{})
        return
    }
    list := []common.Repository{}
    for _, repo := range repos {
        list = append(list, common.Repository{
            Name: repo.Name,
            User: repo.Owner,
            URL:  repo.URL,
            Star: repo.Star,
            Description: repo.Description,
        })
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(list)
    return
}

staticフォルダ以下の作成

server.goを作成したことで,staticフォルダ以下を配信できるようになりました。
まず、index.htmlファイルを見ていきます

index.html

<html>
    <head>
       <meta charset="utf-8">
       <meta name="viewport" content="width=device-width,initial-scale=1.0">
       <link rel="stylesheet" type="text/css" href="/index.css">
       <script src="wasm_exec.js"></script>
       <script>
         const go = new Go();
           WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
               go.run(result.instance);
           });
       </script>
   </head>
    <body>
      <div id="app"></div>
    </body>
</html>   
wasm_exec.js
<script src="wasm_exec.js"></script>

wasmはwasm単体では動作しません。wasmはjsから読み込む処理が必要です。
読み込む処理をwasm_exec.jsが行います。このファイルは提供されてるので、ダウロードします

$ cd static && curl -sO https://raw.githubusercontent.com/golang/go/release-branch.go1.12/misc/wasm/wasm_exec.js
script処理
index.html
<script>
         const go = new Go();
           WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
               go.run(result.instance);
           });
</script>

ここでは、wasmファイルを取得して実行するところです。

index.css

スタイルを当てます

h1{
  text-align: center;
  font: #f1f1f1;
}

.trend{
    width:80vw;
  padding: 1em;
  margin:0 auto;
    display:flex;
  flex-wrap: wrap;
  justify-content:center;
}

.item {
  background: #f1f1f1;
  margin: .5em;
    width:100%;
}


@media (min-width: 801px) {
  .item {
    width: 30%;
  }
}

@media (min-width: 600px) and (max-width: 800px){
  .item {
    width: 40%;
  }
}

.item-title {
    font-size: 1.2em;
    background: #24292e;
    text-align: center;
    display: table-cell;
  vertical-align: middle;
    color: #fafafa;
    font-weight: bold;
  height: 100px;
  width:10%;
  padding:1em;
}

.item:hover .item-description {
  display: block;
}

.item:hover .item-title {
  display: none;
}

.item-description {
  display:none;
  color: #24292e;
    height: 100px;
  word-break : break-all;
    overflow: scroll;
}

a {
  text-decoration: none;
    color: #111;
}
a:hover {
  text-decoration: none;
}

/* loading */
.sk-chase {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 45%;
  left: 45%;
  margin: :auto;
  animation: sk-chase 2.5s infinite linear both;
}

.sk-chase-dot {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0; 
  animation: sk-chase-dot 2.0s infinite ease-in-out both; 
}

.sk-chase-dot:before {
  content: '';
  display: block;
  width: 25%;
  height: 25%;
  background-color: #3a5;
  border-radius: 100%;
  animation: sk-chase-dot-before 2.0s infinite ease-in-out both; 
}

.sk-chase-dot:nth-child(1) { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2) { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3) { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4) { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5) { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6) { animation-delay: -0.6s; }
.sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; }

@keyframes sk-chase {
  100% { transform: rotate(360deg); }
}

@keyframes sk-chase-dot {
  80%, 100% { transform: rotate(360deg); }
}

@keyframes sk-chase-dot-before {
  50% {
    transform: scale(0.4);
  } 100%, 0% {
    transform: scale(1.0);
  }
}

main.wasm

goで書かれたwasmファイルです。
生成は後ほど説明します。

wasmファイルの作成

ここから本題のwasmを作成していきます。
wasmはGoで書かれたファイルをbuildした生成物です。
GOOSとGOARCHをwasm用にする以外は通常のbuildと同じです。

$ GOOS=js GOARCH=wasm go build -o static/main.wasm main.go

main.go

基本的にはjsで利用しているメソッドを使えるので、普段jsを書く人ならそこまで苦労しないかなと思いました。
(普段書かないので、手間取りました)

package main

import (
    "encoding/json"
    "example/common"
    "fmt"
    "net/http"
    "syscall/js"
)

var document = js.Global().Get("document")
var app = js.Global().Get("document").Call("getElementById", "app")

func main() {
    done := make(chan bool)
    go loading(done)
    title()
    go fetch(done)
    select {}
}

func title() {
    t := createElement("h1")
    t.Set("textContent", "go-trending in github")
    app.Call("appendChild", t)
}


func getTrend() (common.Repositories, error) {
    r, err := http.Get("/trend")
    defer r.Body.Close()
    if err != nil {
        return common.Repositories{}, err
    }
    repos := common.Repositories{}
    err = json.NewDecoder(r.Body).Decode(&repos)
    return repos, err
}

func fetch(done chan bool) {
    trendListDiv := createElement("div")
    class := trendListDiv.Get("classList")
    class.Call("add", "trend")
    app.Call("appendChild", trendListDiv)

    trends, _ := getTrend()
    done <- true
    for _, trend := range trends {
        trendListDiv.Call("appendChild", createItemByTrend(trend))
    }
}

func createItemByTrend(repo common.Repository) js.Value {
    item := createElement("div")
    class := item.Get("classList")
    class.Call("add", "item")

    itemLink := createElement("a")
    itemLink.Set("href", repo.URL)

    itemTitle := createElement("div")
    itemTitle.Set("textContent", fmt.Sprintf("%s/%s (☆ %d)", repo.User, repo.Name, repo.Star))
    class = itemTitle.Get("classList")
    class.Call("add", "item-title")

    itemDescription := createElement("div")
    itemDescription.Set("textContent", repo.Description)
    class = itemDescription.Get("classList")
    class.Call("add", "item-description")

    item.Call("appendChild", itemLink)
    itemLink.Call("appendChild", itemTitle)
    itemLink.Call("appendChild", itemDescription)
    return item
}

func loading(done chan bool) {
    chase := createElement("div")
    class := chase.Get("classList")
    class.Call("add", "sk-chase")
    app.Call("appendChild", chase)
    dots := []js.Value{}
    for i := 0; i < 6; i++ {
        chaseDot := createElement("div")
        class = chaseDot.Get("classList")
        class.Call("add", "sk-chase-dot")
        chase.Call("appendChild", chaseDot)
        dots = append(dots, chaseDot)
    }
    <-done
    style := chase.Get("style")
    style.Set("display", "none")

}

func createElement(elementName string) js.Value {
    return document.Call("createElement", elementName)
}

main処理

機能は、トレンドを取得する間ローディングを表示し、取得し終わるとトレンド一覧が出てきます。

func main() {
    done := make(chan bool)
    title()
    go loading(done)
    go fetch(done)
    select {}
}
  • title()は、サイトタイトルを表示するための関数です。中では、h1タグを生成しています。
  • fetch()はトレンドをサーバから取得して、一覧を表示します。中では、apiを叩き、domを生成していきます。
  • loading()はトレンドを取得中にローディングを出します。

doneについて、
Go特有のチャネルを使って、fetch()の内部で、トレンドを取得し終わるのを監視して、終わったことをloading()に伝えます。
select{}を用いて、main関数の処理が終了するのをブロックしています。

共通処理

先にこのコード内での共通処理を説明しておきます。

var document = js.Global().Get("document")
var app = js.Global().Get("document").Call("getElementById", "app")

func createElement(elementName string) js.Value {
    return document.Call("createElement", elementName)
}
  • documentはjsのdocumentと同じです。
  • appは、id=appのelementを取得しています。(getElementByIdはjsのメソッド名です)
  • createElement(name)はdomを生成します。
title()
func title() {
    t := createElement("h1")
    t.Set("textContent", "go-trending in github")
    app.Call("appendChild", t)
}

h1タグを生成し、中に"go-trending in github"を挿入し、appの子要素として挿入します。
書き方は違いますが、jsと同じメソッド名で処理をしています。

loading()
func loading(done chan bool) {
    chase := createElement("div")
    class := chase.Get("classList")
    class.Call("add", "sk-chase")
    app.Call("appendChild", chase)
    dots := []js.Value{}
    for i := 0; i < 6; i++ {
        chaseDot := createElement("div")
        class = chaseDot.Get("classList")
        class.Call("add", "sk-chase-dot")
        chase.Call("appendChild", chaseDot)
        dots = append(dots, chaseDot)
    }
    <-done
    style := chase.Get("style")
    style.Set("display", "none")

}

appの子要素に以下のようなelementを生成しています。

<div class="sk-chase">
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
    <div class="sk-chase-dot"></div>
</div>

特筆事項としては、

    <-done
    style := chase.Get("style")
    style.Set("display", "none")

doneチャネルに値が入ってくるまで<-doneでブロックし、値が入ってくると、style属性にdisplay:noneを追加し、見えないようにします・

fetch
func fetch(done chan bool) {
    trendListDiv := createElement("div")
    class := trendListDiv.Get("classList")
    class.Call("add", "trend")
    app.Call("appendChild", trendListDiv)

    trends, _ := getTrend()
    done <- true
    for _, trend := range trends {
        trendListDiv.Call("appendChild", createItemByTrend(trend))
    }

}

func getTrend() (common.Repositories, error) {
    r, err := http.Get("/trend")
    defer r.Body.Close()
    if err != nil {
        return common.Repositories{}, err
    }
    repos := common.Repositories{}
    err = json.NewDecoder(r.Body).Decode(&repos)
    return repos, err
}

getTrend()で、http通信でトレンドを取得し、構造体にマッピングしています。
取得したレポジトリ毎にdomを生成しています。
取得してから、doneチャネルにtrueという値を送信しています。
fetchでは、以下のようなElementを生成しています。

<div class="trend">
    <div class="item">
        <a href="">
            <div class="item-title"></div>
            <div class="item-description"></div>
        </a>
    </div>
    ...
</div>

実行

$ GOOS=js GOARCH=wasm go build -o static/main.wasm main.go
$ go run server.go

感想

共通処理が書ける

今回、common/trend.goをserverとwasm両方で利用しました。
そのためapiの通信の際、同じ構造体を利用できるので、マッピングが楽でした。
grpcと同じ感じがしました。

Vugu

https://github.com/vugu/vugu
という、wasmでReactやVue風にUIを書くことができるパッケージがあるらしいので、そっちを使うと管理が楽っぽいです

終わり

初めてwasmを触りましたが、wasmのファイルサイズが大きかったり、まだ課題はありそうですが、js以外にもウェブ制作の手段が広がったのは良いことだなと思いました。

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

Goの勉強のため自分のグローバルIPを確認するコマンドを作った

$ whatsmyip --version
             _               _     _                                 _             ___
 __      __ | |__     __ _  | |_  ( )  ___     _ __ ___    _   _    (_)  _ __     |__ \
 \ \ /\ / / | '_ \   / _` | | __| |/  / __|   | '_ ` _ \  | | | |   | | | '_ \      / /
  \ V  V /  | | | | | (_| | | |_      \__ \   | | | | | | | |_| |   | | | |_) |    |_|
   \_/\_/   |_| |_|  \__,_|  \__|     |___/   |_| |_| |_|  \__, |   |_| | .__/     (_)
                                                           |___/        |_|
whatsmyip version v0.1.0 (build with go1.10.4)

ときどき必要になるけど、控えておくのも面倒だし都度、「アクセス情報【使用中のIPアドレス確認】- cman.jp 」などのサイトを使って調べてたりしたけど、ちょうど良いので 先日書いたコードをベースにターミナルで直接実行可能なコマンドを作った。

インストール

go get github.com/snaka/whatsmyip

実行例

$ whatsmyip
113.43.xxx.xxx

作ってみた感想

Goはコンパイラが早いので軽量スクリプト言語に近い感覚で実装できるし、lintとかtestとかいろいろ便利なしくみが最初からそろっているの良い。

非同期処理の待ちをゴルーチンとチャンネルの組み合わせで書いてみたけど、こんな使い方で良いのかよくわからない。

参考

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

grpcurlを使ってGitHub Actions上でgRPCなAPIのテストを実行する

はじめに

grpcurlというgRPCサーバをコマンドラインで叩けるツールを利用し、それをGitHub Actions上で動かしてひとまず正常動作確認まで試せたのでここに記録を残します。

環境

go version go1.13.1 darwin/amd64
grpcurl version: v1.4.0

Main Contents

プロジェクト内のGo製ツールの管理はtools.goファイルを生やす

grpcurlはGo製のツールなのでgo installでコマンドを落とすことができます。

Go製のツールのバージョンもGo Moduleのgo.modにて管理するための方法を探しました。

すると公式のGo Modulesに関するこちらのリンクにて言及があり、tools.go(ファイル名は何でも良い)上でツールをimportすることが現状(2020年1月時点)のベストプラクティスとわかりました。

公式での具体例がこちら、GitHubのIssueでの議論がこちらになります。

例として、tools.goは以下のようになります。

tools.go
// +build tools

package tools

import (
    _ "github.com/fullstorydev/grpcurl/cmd/grpcurl"
    _ "github.com/golang/mock/mockgen"
    _ "github.com/golang/protobuf/protoc-gen-go"
    _ "github.com/google/wire/cmd/wire"
)

ポイントは+build toolsを付けることでプロダクションコードにはtools.goでimport記述されているライブラリコード分をコンパイル対象にさせないことです。

そして今回、こちらの記事を参考にMakefile上で上記ツールをインストールするコマンドを用意しました。

Makefileの一部
.PHONY: setup
setup: install-tools
    @true

.PHONY: install-tools
install-tools:
    $(GOMOD) download
    @cat tools/tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % 

make install-toolsとすることでツールコマンドがインストールされ、そのバージョンがgo.modで管理されるようになります。

grpcurlをローカルで試す

pgrpcurlのREADMEの例を見ながら検証しました。

デフォルトではgRPCのデバッグをするために利用するreflectionの機能が有効になっていることが想定されていますが、今回はprotoファイルを参照してクライアントコールする方法にしています。

$ grpcurl -v -d @ -plaintext -proto pb/service.proto localhost:8081 pb.service.Method < test.json

上記では

  • -d @test.jsonとしてgRPCクライアント側でテスト用に渡すJSONファイルを指定
  • -protoオプションでprotoファイルを参照するよう指定

しています。gRPCサーバを8081ポートで動作させていれば以下のような結果が得られます。

Resolved method descriptor:
// A server-to-client streaming RPC.
//
// Comment
rpc Method ( Args) returns ( stream .pb.ReturnType);

Request metadata to send:
(empty)

Response headers received:
content-type: application/grpc

Response contents:
{
  "userId": "ABCDE",
  ....
}

GitHub ActionsにてGo製ツールを使うためにGOPATHとPATH設定をする

GitHub Actionsの公式ランナーにてGOPATH設定しPATHを通すようにするには、現状(2020年1月時点)スマートな方法がまだないようです。

ここのIssueでの議論を参考に、以下のような設定をした上でgrpcurlコマンドでテストをしています。

GitHubActionsのworkflowsファイル一部
name: Test
on: push
jobs:
  integration:
    name: Integration test
    runs-on: ubuntu-latest
    steps:

    - name: Setup Go
      uses: actions/setup-go@v1
      with:
        go-version: 1.13.1

    - name: Setup GOPATH
      # temporary step to run grpcurl command in GOPATH
      # copied from https://github.com/actions/setup-go/issues/14#issuecomment-563273583
      run: |
        echo "::set-env name=GOPATH::$(go env GOPATH)"
        echo "::add-path::$(go env GOPATH)/bin"
      shell: bash

    - name: Checkout code
      uses: actions/checkout@v2

    - name: Install test tools
      run: make setup

    # gRPCサーバを立てる作業

    - name: Run gRPC API test
      run: grpcurl -v -d @ -plaintext -proto pb/service.proto localhost:8081 pb.service.Method < test.json

gRPCサーバに異常があれば正常終了しないのでひとまず正常系のテストができるようになりました。

gRPCにはHTTPステータスコードとの互換を想定したコード定義が以下リンクのように公式で定められているので、今後これに対応するようにサーバ側を更新しテストを充実させていく予定です。
https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto

参考

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