A Tour of Go - Exercise: Web Crawler をやってみた

冬休みの間にGoの概要を知るためにA Tour of Goをやってみた。Exercise: Web Crawlerにちょっと苦戦したので記録として残しておく。

V1:とりあえずCache機能作る

Cacheを作成するとこまでを目標に実装。Mutexを扱うことを想定して、結果とMutexを同じ場所で管理できるようにstruct Crawlerを作っておいた。CrawlもCrawlerのインスタンスメソッド化。

package main

import (
    "fmt"
    // "sync"
)

type Fetcher interface {
    Fetch(url string) (body string, urls []string, err error)
}

type Crawler struct {
    // m sync.Mutex
    result map[string]string
}

func (c *Crawler) Crawl(url string, depth int, fetcher Fetcher) {

    if depth <= 0 {
        return
    }
 
    if c.result[url] != "" {
        return
    }
 
    body, urls, err := fetcher.Fetch(url)

    if err != nil {
        c.result[url] = err.Error()
        fmt.Println(err)
        return
    }

    c.result[url] = body
    fmt.Printf("found: %s %q\n", url, body)

    for _, u := range urls {
        c.Crawl(u, depth-1, fetcher)
    }
    return
}

func main() {
    c := Crawler{ result: make(map[string]string) }
    c.Crawl("https://golang.org/", 4, fetcher)
}

V2: go routine + sync.Mutex で非同期化

非同期化したものMutexだけじゃ処理が終わるのを待ってくれない。いまいちな方法だがとりあえずtime.Sleepで処理が終わるのを待たせてみる。チュートリアルの流れ的にMutex使う方法かなと思っていたけどそう単純にはいかなかった。

package main

import (
    "fmt"
    "sync"
    "time"
)

type Fetcher interface {
    Fetch(url string) (body string, urls []string, err error)
}

type Crawler struct {
    m sync.Mutex
    result map[string]string
}

func (c *Crawler) Crawl(url string, depth int, fetcher Fetcher) {

    if depth <= 0 {
        return
    }
 
    if c.result[url] != "" {
        return
    }
 
    c.m.Lock()
    body, urls, err := fetcher.Fetch(url)
    c.m.Unlock()

    if err != nil {
        c.result[url] = err.Error()
        fmt.Println(err)
        return
    }

    c.result[url] = body
    fmt.Printf("found: %s %q\n", url, body)

    for _, u := range urls {
        go c.Crawl(u, depth-1, fetcher)
    }
    return
}

func main() {
    c := Crawler{ result: make(map[string]string) }
    go c.Crawl("https://golang.org/", 4, fetcher)
 
    time.Sleep(1000)
    c.m.Lock()
    fmt.Printf("Cache: %s\n", c.result)
    c.m.Unlock()
}

V3: time.Sleepを使わないように修正

別の方法としてchannelを使う、JavaScriptでいうPromise .allのようなものを探すなどがありそうか。channel使うとデータの受け渡しをする方向に修正が必要か。そう考えるとPromise .all的なものを探して使ってみた。(V2の後にfetcherをstruct Crawlerに入れる修正してた)

import (
    "fmt"
    "sync"
)

type Fetcher interface {
    Fetch(url string) (body string, urls []string, err error)
}

type Crawler struct {
    wg      *sync.WaitGroup
    result  map[string]string
    fetcher Fetcher
}

func (c *Crawler) Crawl(url string, depth int) {
    if depth <= 0 {
        return
    }

    if c.result[url] != "" {
        return
    }

    body, urls, err := c.fetcher.Fetch(url)

    if err != nil {
        c.result[url] = err.Error()
        fmt.Println(err)
        return
    }

    c.result[url] = body
    fmt.Printf("found: %s %q\n", url, body)

    c.wg.Add(len(urls))
    for _, u := range urls {
        go func(u string) {
            defer c.wg.Done()
            c.Crawl(u, depth-1)
        }(u)
    }
    return
}

func main() {
    c := Crawler{
        result:  make(map[string]string),
        wg:      &sync.WaitGroup{},
        fetcher: fetcher,
    }
    c.Crawl("https://golang.org/", 4)
    c.wg.Wait()
}

V3 おまけ

やっていく中で本題とは違うところで引っ掛かった。クロージャ + range + goroutineを使うと挙動が想定と違った。以下のように単純化してループを作成して挙動を確認してみる。

urls := [2]string{
    "https://golang.org/cmd/",
    "https://golang.org/pkg/",
}

1個目のコードでは range urls の初回の結果がバインドされ、ループ2回目も同じ値が入っている。なのでこのようなケースでは引数として値を渡す必要がある。

for _, u := range urls {
        go func() {
            fmt.Printf("url = %v\n", u)
        }()
    }
    time.Sleep(1000)
}

// url = https://golang.org/pkg/
// url = https://golang.org/pkg/
for _, u := range urls {
        go func(u string) {
            fmt.Printf("url = %v\n", u)
        }(u)
    }
    time.Sleep(1000)
}

// url = https://golang.org/cmd/
// url = https://golang.org/pkg/

goroutineを使わない場合はレキシカルな評価をしてくれる。

for _, u := range urls {
        func() {
            fmt.Printf("url = %v\n", u)
        }()
    }
}

// url = https://golang.org/cmd/
// url = https://golang.org/pkg/
for _, u := range urls {
        func(u string) {
            fmt.Printf("url = %v\n", u)
        }(u)
    }
}

// url = https://golang.org/cmd/
// url = https://golang.org/pkg/

こちらのサンプルコードが、自分の方針と同じ方針だったのは励みと参考になってよかった。

A Tour of Go の Exercise: Web Crawler を書いてみた - Qiita

syncパッケージは奥深そう。アプリケーションレベルではMap, Onceあたりが利用機会ありそうな予感がする。

sync

sync.Condはうまく使うとコードがシンプルにできるのは良さそうだな。

sync.Cond/コンディション変数についての解説

まとめ

冬休み中にA Tour of Goを完走できた!前半はサクサク進めれたのでGo簡単じゃんと思ってたけど並列処理周辺は比較すると理解に時間が必要だった。あくまで前半よりという話なので、シンプルにかつ使いやすい感じに作られているんだろうな。ちょうどいい難易度のExerciseを適宜挟んで理解を促進してくれたのも助かった。

この先は何をやろうかな。

2021年に買ってよかったもの

リモートワークを支えてくれた仲間たち

ネックバンドスピーカー

骨伝導派が多いけど音漏れに問題ない環境で使うならネックバンドスピーカーもいいぞ。会社では耳塞ぐタイプのやつが良いので使い分けるでしょ。

Bose使ってる。ビクターでも良かったけどメルカリでちょうどよく出品あった方を選んだ。軽さを求めるならビクターが良いかな。音楽を聴くならこの辺が選択肢かな。テレビ用でBT非対応なのもあるので選ぶときは注意。最近SONYが新製品出してたのも気になる。

amzn.to

amzn.to

自作キーボードキット

肩が丸まりにくいようにセパレートタイプ使おうぜ。HHKBに近い配列のこれを組んだ。

7sPro

初回は安定のため有線で組んだけど、やっぱ無線化したいのとロープロファイルほしいという欲望のためにこれとBT用部品を買ったけどまだ組んでない。。

【委託】7sKB(Choc)

7sKB - 自キ温泉街販売所 - BOOTH

iPad + Apple pencil

リモート会議でPCだけしかないの辛くね?Miro, Jamboard, Figma Jamあたりをみんなフル活用できる環境でディスカッションしたい。

安くていいからペンタブ(液晶じゃなくていい)を会社で補助出してほしい。これとかで良いのでは。

amzn.to

モニターアーム

amzn.to

机が狭いなら設置面積を下げればいいじゃない。机が白なので合わせて白を買った。溜まってたヨドバシポイントを注ぎ込んで買ったはず。ただ今から買うとしたら長身ポール買ってたかもなとは思う。

生活環境を改善してくれたやつら

クロスバイク

こいつを型落ちセールで買った。公共交通機関に頼らず行ける範囲が広がって良かった。

Verza Speed 50 | Felt公式日本語Web

しばらくバイクシェアのヘビーユーザーだったけどバッテリー切れやら状態悪いとかでストレス多かったので切り替え。一度ちゃんとした自転車に乗るとクオリティ下がるのは辛かった。そういえばバイクシェアの車体状態レポートWebサービス作ろうかと思って忘却してたの思い出した。車体番号と故障箇所を共有するだけのSNSみたいなの。

SwitchBot + 開閉センサー

amzn.to

BlackFridayで買ったばっかりだけど。アナログ洗濯機を無理やりスマート家電化した。 センサーだけじゃ動かないのでハブが必要なのは注意。

amzn.to

開けっぱなしタイマーが30分固定なのでもっと便利にしたいと思ってる。SwitchBotのiOSアプリがサイズ1GB超えてるのだけは気に入らない。

コーヒーメーカー

amzn.to

1日数回最寄りのローソン行ってたけど面倒になったし買っても元取れるんじゃねと思ったので買った。なぜか新品2万で売ってて怪しみながらも購入した。拍子抜けするほど問題なく使えてる。コーヒーオタクな人がミキサータイプのミルなぞ邪道と言ってたので臼式のやつが気になってた。

WPIプロテイン

乳糖不耐症気味なのでWPIプロテインに切り替えると体調良いことに今更気づいた。いわゆる普通に買うと大体WPCプロテイン。WPIプロテインはより乳糖はじめとしたタンパク質以外の成分を除去したもの。牛乳のめる人はWPCプロテイン買っておけば問題ない。

ビーレジェンドは水で飲める味が多いのでさらに自分にぴったりだった。豆乳ストックするの面倒だったし。ジムに粉だけ持ってって飲めるのも便利。波動拳味もおすすめ。なぜか私はお腹壊さないし駄菓子みたいな味も悪くない。水でも飲みやすい味だと乳糖が副次的に除去されてるとかありそう。

amzn.to amzn.to

水で飲みやすいシリーズだとザバスのグレープフルーツも良い。ほぼスポドリ

amzn.to

ラクターゼサプリ

乳糖不耐症改善シリーズ2。乳糖を分解してくれる酵素ラクターゼが入ってるので飲むと症状がマシになるらしい。実際WPIプロテインが切れた時に合わせて飲むとお腹壊さない気がする。

amzn.to

リュックサックの購入候補を考えてみた

カバンを使う目的が休日に出かけつつジムに行くことがメインになってるのでリュックサックを買い替えようと考えた。整理しつつ候補をあげるとこまでやったメも。

状況・事前の整理

悩み

だいたいコワーキングスペースなどでコード書く、勉強する、本読む。ちょっとした買い物する。最後にジム行く。という順番が多い。そうすると上の方に本や筆記具、買った物がくる。なのでジムに行く頃にはギアが底の方に来ていて、とり出すのに一苦労となってしまう。更には帰る時にはギア→本・筆記具・買った物にするため一度中身を空にしないといけないのでまたー苦労…

調査

その悩みを解決するにはと考えつつ軽く下調べした。スタートとしては、良い評判を耳にしていたHAGLOFS CORKER LARGEを調べてみた。

その中で検索に引っかかったところ以下のブログが参考になった。

【2021年版ビジネスリュック】アウトドアショップを8軒回り、買いました。 | IT営業マンだけど、投資やっています。

特に候補にも入れた Arc'teryx BLADE 28 BACKPACK との出会いが大きかった。荷室が区分けされているタイプに出会えたことで方針がクリアになった。スーツに合わせる必要はないので他の候補は自分で探すことにし、良さげなバッグを見繕いつつ必要な特徴をまとめた。

条件

これを満たしていれば買う!という条件をまとめてみた。

容量

ジムでやってることの都合である程度の容量が必要。Maxを考えると25Lは欲しいところ。容量は大きめがいいが取り回しに困るサイズにならないくらいのバランスは保ちたい。

セパレートできる

ジム用のギアとそれ以外が分けやすいこと。ギアが一番かさばるのに底に入れておきたいのが困るポイントなので、二気室あり着替えとそれ以外を別々にできればタイミングによって区分けできる。今はギアはバッグインバッグに入れてるが、底の方から取り出すのに手間がかかっている。重量バランスの面から身体側に本など、外側にギアを配置したい。本などは下の方に来ないとなおのこと良い。

重量

軽さ大事。荷物多くなりがちなので軽くはしておきたい。ただアウトドアブランド中心なので極端に重いということはなさそう。

アクセスの良さ

口が広く開いて荷室にアクセスできること。これは上2つの要素を満たすものはだいたいクリアしてそう。開き方がパターンあるくらいか。

候補

というわけで今の候補。追加・精査と個々の特徴・比較は追記か別記事で。

[カリマー] デイパック tribute 25 Black(ブラック)
 
[カリマー] デイパック tribute 40 Black(ブラック)
 
[ミレー] リュック EXP 35 Black-Noir

[ミレー] リュック EXP 35 Black-Noir

  • メディア: ウェア&シューズ
 

 





 

tmuxでpowerlineフォントがうまく表示されない時

Alacritty入れてみたけど、tmuxでpowerlineフォントが表示されなくて困ってた。

alacritty.ymlの設定で対応する方法で悩んでたけど、tmux側の問題だった。

github.com

全体でlocale的にUTF-8を使うようになっていない状態だったけど、tmuxなしの状態ではよしなに判定してくれてたけど、tmuxに入るとUTF-8使ってくれない状態だったようで。

rcファイルにきちんと LANG=ja_JP.UTF-8 LC_TYPE=ja_JP.UTF-8 を設定して解決した。

Jestで非公開関数をテストする方法がイマイチだった

前回の続き。

結論:const を上書きできないっぽいのでrewire以外の方法を探すのが良さそう…

(追記に上げたように、Jestで動かす時にできないという条件付き)

前提

検証した時のバージョン

{
  "dependencies": {
    "jest": "^23.6.0",
    "rewire": "^4.0.1"
  }
}

モック前

テスト対象

const fs = require("fs");

const read = cb => {
  return fs.readFile("./text.txt", "utf-8", cb);
};

module.exports = read;

テストコード

const read = require("./read.js");

test("read", done => {
  return read((err, text) => {
    expect(text).toMatch("foobar");
    done();
  });
});

モックしてみる(失敗)

fs.readFile の結果をモックしたいので、こう書いてみる

テストコード2

const rewire = require("rewire");
const read = rewire("./read.js");

test("read", done => {
  read.__set__("fs", {
    readFile: (path, enc, cb) => cb(null, "foobar")
  });

  return read((err, text) => {
    expect(text).toMatch("foobar");
    done();
  });
});

けれど結果はモック前と変わらない…

Issue当たってみると、同じ悩みを抱えてる人がいたよう。「Rewiring const is supported since 3.0.0」って言ってるけどReopenされてる。そしてReopenした人の「TypeError: Assignment to constant variable」とも違う状況なので不思議な気持ち。

github.com

モックしてみる(とりあえず成功)

Issueへの返信にもあった let 使ってみる方法を試す。テストコードは2の状態のまま。

テスト対象2

let fs = require("fs");

const read = cb => {
  return fs.readFile("./text.txt", "utf-8", cb);
};

module.exports = read;

これでテストはPassした。でもテストのために let にするのっておかしいし、そんな制限ある中でコード書きたくないのでrewireは使えないかな…


追記

どうやらJestとの組み合わせが良くないらしい。

github.com

リンク先ではMochaで試してるけどnodeでもMock成功した。

// run.js
const rewire = require("rewire");
const read = rewire("./read.js");

read.__set__("fs", {
  readFile: (path, enc, cb) => cb(null, "foobar")
});

return read((error, text) => {
  console.log({ error, text });
});

node run.js として呼び出したらモック成功した。まあJestとの組み合わせが良くないなら選択できないんだけど、ライブラリ自体は問題ないようだったので訂正。

Jestで非公開関数をテストしたい

rewire を使うと簡単にできた。

github.com

前提

Jest入ってる。Babel入れてない。

インストール

npm install --save-dev rewire

準備

通常なら require(path) とするところを rewire(path) にする

const rewire = require('rewire')
const myModule = rewire('./myModule')

テスト対象の関数を取り出してテストする

const myPrivateFunc = myModule.__get__('myPrivateFunc');
expect(myPrivateFunc()).toEqual({})

CSSのみでスクロールできそうな雰囲気を出す

HTMLでElement内でスクロールさせるUIが必要になったけれど、Macスマホだとスクロールバーは消えるスクロールできるの気づきにくい。という問題があったので、CSSのみでできる方法を考えていた。

解決方法そのものが出てきてしまったけど、すぐに理解するにはCSS力が足りなかったので、分解しつつ理解したメモです。

元ネタ

http://lea.verou.me/2012/04/background-attachment-local/

元ネタの人が本出してて評判良いので読んでみたい。翻訳版ないけど…

CSS Secrets: Better Solutions to Everyday Web Design Problems

CSS Secrets: Better Solutions to Everyday Web Design Problems

分解してみた

codepen.io

本来は背景色が白だけど、影を隠す要素を見やすくするために色付けしてる。

背景の設定

元のCSSの定義順に、背景1〜4とすると、

  • 背景3,4
    • 影っぽい見た目の背景
    • background-attachment: scroll で、Elementのスクロール位置ではなく、ページ内のElementの位置に追従する
  • 背景1, 2
    • 影を隠すための背景
      • background の重ねがけは、定義順で上位レイヤーになる仕組みを利用
    • background-attachment: local で、Elementのスクロールに追従する
    • 重なりと追従のおかげで、スクロール位置がトップかボトムにあると影が隠れる

background-attachment

について、知らなかったので読んでみた。

CSS Backgrounds and Borders Module Level 3

fixed

fixed with respect to the page box

Note that there is only one viewport per view

Viewportに対して固定されるので、もし背景3,4に設定してしまうと、Viewportのボトムにない限り影が見えないことになる

scroll

fixed with regard to the element itself and does not scroll with its contents

Elementの背景として表示されて、内包するElementがスクロール可能でも追従しない

local

the background scrolls with the element’s contents

Element内でのスクロール位置に追従する

スクロール範囲とBorderの関係についても触れられてるけど、今回は省略

感想

CSSすげー

CSSで色々作れる人すげー