20190430のGoに関する記事は6件です。

7言語に同時に入門する。

入門するのは以下の言語!

  • Nim
  • Elixir
  • Go
  • Lua
  • Python
  • Scala
  • Dart

今回は環境構築してHello Worldするところまでです。

では早速環境構築していきます。

ちなみに、今回インストールするマシンはWindows機(Surface)となっております。

環境構築編

Nim

https://trap.jp/post/452/
ここを参考にしつつ進めていきます。

Nimのインストールページ(windows用)

https://nim-lang.org/install_windows.html

NimはC言語に一回変換してから実行形式にするトランスパイラ的なやつなのでC言語のコンパイラが必要です。

公式ページでMingWというのがおすすめされてたので今回はこれを入れます。
上記のページの下の方にNotes about compiler dependenciesという部分があるのでそこにあるリンクからDLしました。

7zipとかで解凍して出てきたファイルを今回はC:devに入れます。
そして環境変数に追加。環境変数はコントロール パネル>システムとセキュリティ>システム>システムの詳細設定>環境変数でいじれます。
システム環境変数のPathのところに追加します。
今解凍したMingWの中のbinの階層をしていします。今回だとC:\dev\mingw64\binになります。

そのあとPowerShellで

gcc -v

で動作確認します。
なんか出てきたらOKです(だと適当過ぎるので一番最後にバージョンが表示されていること程度は確認します)。

さて、本命のNim自体のインストールです。

Mac、Linuxだとchoosenimというのが使えるらしいです。
一応Windowsでも使えそうだったけれど、公式ページでUnixの方でしか紹介されていなかったのでやめておきました(マイナーな言語でマイナーなバグを踏んではかなわないので)。
バージョン管理できないので辛さはありますが、まぁ所詮趣味なので良いでしょう。

https://nim-lang.org/install_windows.html
NimのダウンロードページからDLしてきます。
ダウンロードしてきたら解凍します。

解凍したファイルを今回はC:\devに入れます。
ついでになんとなく解答したフォルダからバージョン名を消して単なるnimにしました(ようはC:\dev\nimの中にbinとかfinish.exeが入っている状態)。

でnimくんも環境変数に追加します。

今回もシステム環境変数のPathにnimのbinを追加します(今回の例ではC:\dev\nim\bin)。

で、PowerShellで

nim --version

で動作確認します。一行目にバージョンが出てくるはずです。
無事に出てきたらnimの環境構築はおしまいです。お疲れ様でした。

ちなみになんですが、finish.exeを実行して適当に環境変数を設定してもらおうと思ったらうまくできなくて手動で設定しました。

Elixir

Elixirくんもインストーラーから
https://elixir-lang.org/install.html

と、思わせつつ、Chocolatyが使えるのでこっちでインストールします。
ChocolatyというのはUbuntuとかのapt-get的な存在です。つまりアップデートがコマンドでできる!
ちなみにnimでもChocolatyが使えはするんですけど、nimの最新バージョンが0.19.4に対してChocolatyで入れようとするとバージョンが0.11.2だったのでやめました。アップデートプリーズ(やり方がわからなくてできない)。
Elixirは最新が1.8.1に対してChocolatyで入れたときのバージョンが1.8.0で微妙に遅れてはいますが、まぁ許容範囲かな、って感じです。ElixirのインストールページでもChocolatyが載っているのでそのうち更新されるのでは、と思っています。

いや、インストーラーからやるわって人はインストーラーをDLしてNextを押し続ければインストールされるのではないかな。公式に

Click next, next, …, finish

って書いてあるし。

さて、Chocolatyを使うにはChocolatyをインストールしないといけないわけですが、今回のマシンには諸事情によりすでにインストールしていたため、詳細なやり方をかけません。

一応参考URLをいくつか載せておくのでそちらを参考にどうぞ……。

https://chocolatey.org/docs/installation
https://qiita.com/konta220/items/95b40b4647a737cb51aa

さて、今回はすでにインストールされているChocolatyを使うので念の為Chocolaty自体のアップデートを確認しておきます。

choco upgrade chocolatey

Chocolatey v0.10.11
Upgrading the following packages:
chocolatey
By upgrading you accept licenses for the packages.

You have chocolatey v0.10.11 installed. Version 0.10.13 is available based on your source(s).
Progress: Downloading chocolatey 0.10.13... 100%

chocolatey v0.10.13 [Approved]
chocolatey package files upgrade completed. Performing other installation steps.
The package chocolatey wants to run 'chocolateyInstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[N]o/[P]rint):

どうやらアップデートがあったみたいなのでyでアップデートします。

そのあとPowerShellを再起動しろ、みたいなメッセージが流れるので一回閉じてもう一度PowerShellを起動します。

そしたら以下のコマンドを実行します。

cinst elixir

Chocolatey v0.10.13
Installing the following packages:
elixir
By installing you accept licenses for the packages.
Progress: Downloading erlang 21.2... 100%
Progress: Downloading Elixir 1.8.0... 100%

erlang v21.2 [Approved]
erlang package files install completed. Performing other installation steps.
The package erlang wants to run 'chocolateyInstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[N]o/[P]rint):

こんな感じで出てくるのでyを押してエンターします。
あと、これを見る感じこのコマンドだけでElixirだけではなく、Erlangもインストールしてくれるみたいですね。
ElixirはErlangという言語の仮想マシン上で動くのでErlangのインストールも必要となっております。JavaにおけるScalaみたいなものです。

Erlangのインストールが終わったらElixirのインストールについても聞かれます。

Elixir v1.8.0 [Approved]
elixir package files install completed. Performing other installation steps.
The package Elixir wants to run 'chocolateyInstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[N]o/[P]rint):

yを押してインストールしていきます。

一応PowerShellを閉じてから開き直してElixirの動作確認をします。

elixir --version

でバージョン確認をします。
一行目にErlangのバージョンが表示されて次にElixirのバージョンが表示されました。
これにてElixirの環境構築は終了です。

Go

一応GoのDLページ
https://golang.org/dl/

ここを見つつ進めます。
https://qiita.com/yoskeoka/items/0dcc62a07bf5eb48dc4b

どうもChocolateyが使えそうなので一応最新バージョンとChocolateyの方のバージョンを確認します。
最新は1.12.1に対してChocolateyが1.12で良い感じなのでChocolateyを使ってインストールしていきます。

choco install golang

これを打つといつもの通り

Chocolatey v0.10.13
Installing the following packages:
golang
By installing you accept licenses for the packages.
Progress: Downloading golang 1.12... 100%

golang v1.12 [Approved]
golang package files install completed. Performing other installation steps.
The package golang wants to run 'chocolateyinstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[N]o/[P]rint):

こんな感じで聞かれるのでいつも通りyを入力します。
終わったらPowerShellを閉じて開き直します。

そしたら

go version

でバージョン確認をします。

シンプルに一行でバージョンが表示されました。
バージョンが表示されたのでこれでGoの環境構築はおしまいです。

ちなみに、参考に貼ったQiitaの記事では環境変数を設定していますが、私の場合確認したら環境変数が設定されていました。

Lua

ここを参考にしつつ進めます。
http://yohshiy.blog.fc2.com/blog-entry-291.html

てきとうにzipを落としてきます。

私は何も考えずに1つ目のDLリンク(http://luabinaries.sourceforge.net/download.html)から落としてきました。
参考にしたページでは1つ目の方には最新版がない、となっていましたが、私が見に行ったときには最新版(5.3.5)があったのでとりあえずはどっちでもいいと思います。

さて、落としてきたZipを解凍したところ以下のファイルが出てきました。

lua53.dll
lua53.exe
luac53.exe
wlua53.exe

参考にしているページではwlua53.exeについて書かれていないのでおとなしく同じ方でDLすれば良かったと少し後悔していますが、必要なファイルはあるっぽいので気にせず行きます。

どうも、解凍したところに環境変数を設定するみたいなので、解答したファイルを移しておきます。

C:\dev\lua

の中に上記の4ファイルがある状態にしました。
ここを環境変数に追加します。これまでと同じようにシステム環境変数のpathに追加していきます。
追加したら

lua -v

でバージョンを確認します。

1つ目のDLリンクから落としてきたらうまく動かなかったので参考にしたページ通り2つ目のDLリンクから落としてきます。

どうも、解凍したところに環境変数を設定するみたいなので、解凍したファイルを移しておきます。

C:\dev\lua

この中に3ファイルがある状態にしました。
ここを環境変数に追加します。これまでと同じようにシステム環境変数のpathに追加していきます。
追加したら

lua -v

でバージョンを確認します。
無事に出てきたのでこれでLuaの環境構築は終了です。

Python

Chocolateyくんが使える(最新3.7.2でChocolateyも3.7.2だった)ので使っていきます。
すこし調べた感じAnacondaが一式揃っていて便利な感じもありましたが、機械学習が主目的ではないので普通に入れます。
いまいち理解しきってないですが、Pythonはデフォルトでvenvというコマンドが使えて仮想環境が作れるらしいです。要はデフォルトでバージョン管理ができるようなものなのだろうか。

ともかくChocolateyでPythonを入れていきます。

choco install python

Chocolatey v0.10.13
Installing the following packages:
python
By installing you accept licenses for the packages.
Progress: Downloading python3 3.7.2... 100%
Progress: Downloading chocolatey-core.extension 1.3.3... 100%
Progress: Downloading python 3.7.2... 100%

chocolatey-core.extension v1.3.3 [Approved]
chocolatey-core.extension package files install completed. Performing other installation steps.
 Installed/updated chocolatey-core extensions.
 The install of chocolatey-core.extension was successful.
  Software installed to 'C:\ProgramData\chocolatey\extensions\chocolatey-core'

python3 v3.7.2 [Approved]
python3 package files install completed. Performing other installation steps.
The package python3 wants to run 'chocolateyInstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[N]o/[P]rint):

いつもどおりこんな感じで聞かれるのでyを押します。

で、pipも一緒に入れます。
pipはパッケージ管理ができるやつらしいです。どうせなので一緒に入れます。

choco install pip

Chocolatey v0.10.13
Installing the following packages:
pip
By installing you accept licenses for the packages.
Progress: Downloading easy.install 0.6.11.4... 100%
Progress: Downloading pip 1.2.0... 100%

easy.install v0.6.11.4 [Approved]
easy.install package files install completed. Performing other installation steps.
The package easy.install wants to run 'chocolateyInstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[N]o/[P]rint):

こちらもいつもどおりyを押していきます。

そのあとなんか途中でエラーが出た気がしますがyを押していきます。
多分pythonがインストールされてないよ的なことを言われたのでおそらくPythonを入れたあとにPowerShellを開き直さないでやったのが悪いのだと思います。

PowerShellを開き直してPythonのバージョンを確認します。

python --version

でバージョンが出てきたのでpythonのインストール自体は大丈夫そうです。
pipの確認もします。

pip --version

こちらもバージョンが出てきたので無事pipの方も大丈夫そうです。

というわけでこれにてPython完了です。

Scala

次はScalaです。

と、言いたいところですが、すでにこのPCには構築してしまっていたので参考リンクだけ貼ります。

https://dwango.github.io/scala_text/sbt-install.html

ScalaのバージョンとJDKのバージョンには注意しましょう。私はこれで少しハマりました。

ちなみに、Chocolateyがすでに入っていた諸事情はこれです。

Dart

最後にDartです。

これもChocolateyが使える(最新2.2.0でChocolateyも2.2.0)ので使っていきます。

というか公式のインストールページ( https://www.dartlang.org/tools/sdk )にChocolateyの事書いてありますね。

といことでインストールしていきます。

choco install dart-sdk

後ろに--preをつけるとdev release(要は開発中の不安定版)を入れられるみたいですが、今回は安定版を入れます。
このコマンドを入れると

Chocolatey v0.10.13
Installing the following packages:
dart-sdk
By installing you accept licenses for the packages.
Progress: Downloading dart-sdk 2.2.0... 100%

dart-sdk v2.2.0 [Approved]
dart-sdk package files install completed. Performing other installation steps.
The package dart-sdk wants to run 'chocolateyInstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[N]o/[P]rint):

いつものyを入力するやつが出てくるのでyを入力します。

終わったらPowerShellを開き直してバージョン確認します。

dart --version

これでバージョン確認ができたのでDartの環境構築は終了です。
お疲れ様でした。

Hello World編

無事環境構築が終わったので各言語でHello Worldしていきます。

Nim

ではNimに入門していきましょう。
https://qiita.com/nunulk/items/e3679ce256b8071e5255

適当な階層でhelloWorld.nimファイルを作ります
そして中身に

helloWorld.nim
echo "Hello World"

と書きます。

コンパイルは

nim c -r helloWorld.nim

こんな感じです。

これでhelloWorld.nimと同じ階層にhelloWorld.exeができます。
-rで自動的に実行までしてくれるのでHintがいろいろでたあとにHello Worldが出力されます。
ちなみに一回目のコンパイルはそこそこ時間がかかりますが、二回目以降はキャッシュされるため、短くなります。確か二回目以降は差分だけがコンパイルされると聞いた記憶があります。

Elixir

Elixirに入門します。
https://elixir-lang.jp/getting-started/introduction.html

helloWorld.exs
というファイルを作ります。

中身は

helloWorld.exs
IO.puts "Hello world from Elixir"

elixir simple.exs

実行できます。

コンパイルして実行したかったのですが、コンパイルするにはちゃんとプロジェクトを作らないとできないっぽかったのでここでは一旦諦めます。

Go

Goに入門していきます。
https://golang.org/doc/install

公式にご丁寧にHelloWorldが書いてあったのでコピペしました。
適当なところにhelloWorld.goファイルを作って以下をコピペ。

helloWorld.go
package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}

コンパイルは

helloWorld.goの階層で

go build

でコンパイルできました。
これ、ファイルを指定していないから、同じ階層にmainを持つファイルが2つあったらどうなるんだろうか……。
コンパイルしたらHelloWorld.exeができるので実行するとHelloWorldがでてきます。

ちなみに

go run helloWorld.go

みたいに書くとビルドして実行までしてくれます。

Lua

Luaに入門しましょう。
https://dotinstall.com/lessons/basic_lua

適当なところにhelloWorld.luaを作ります。

中に

helloWorld.lua
print("Hello World")

を書いて

コマンドで

lua helloWorld.lua

で実行してくれます。
スクリプト言語なのでコンパイルは特にないです。

Python

Pythonに入門します。
https://www.python-izm.com/introduction/execution/

今回入れたのはPython3系なのでhelloWorld.pyというファイルを作って以下のように入れます。

helloWorld.py
print('Hello World')

そしたら次のコマンドで実行します。

py helloWorld.py

うまく行かなかったら

C:\Python37\Scripts

みたいなところに環境変数を通したりすると動くような気がします。
私はVSCodeのターミナルでやっていて、最初うまく動かなかったんですけど、lintがどうのこうの言われて入れたら動いたのでそんな感じです(適当で申し訳ない)。

Scala

Scalaに入門していきます
https://dwango.github.io/scala_text/sbt-compile-execute.html

ここを見る感じプログラム本体だけではなく、設定ファイル的なものも必要っぽいので一緒に作ります。

適当に階層を切って

helloWorld.scala
build.sbt

を作ります。
それぞれの中身は

helloWorld.scala
object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello World")
  }
}
build.sbt
scalaVersion := "2.12.8"

scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlint")

こんな感じで作ったら

sbtを起動させてrunさせるとコードが実行されます。

sbt
(sbtが起動し終わるのをまつ)
run
(コンパイルを待つ)
Hello World

みたいな感じです。()のところにはログが出ます。ログが出てる間にいろいろファイルとかフォルダが生成されます。
sbtの起動が長いのは一回起動させたら起動させっぱなしで毎回ではないので問題ないと思います。
コンパイルも2回目以降は早いので最初だけ待ちます。

Dart

最後にDartに入門します。

https://www.dartlang.org/samples

適当にhelloWorld.dartファイルを作って中身を以下のようにします。

helloWorld.dart
void main() {
  print('Hello World');
}

あとは実行しておしまいです。

dart helloWorld.dart

スクリプト言語なのでコンパイルとかなくそのまま実行されて結果が表示されます。

終わり

同時に7言語もやると環境構築はともかく、HelloWorldだけでも時間がかかるので他の人はやめておこうね。

次回はFizzBuzzの予定。

他にも良い題材があれば教えてもらえるとありがたいです。

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

bitbucket-pipelines テストサマリを表示

概要

bitbucket pipelinesではjunitの形式のxmlからテストのサマリを表示することができる。
pipelinesで取得できたサマリーはpipelinesのページに表示される

環境

手順

  • bitbucket pipelinesを使える状態にする

参考
bitbucket pipelines セットアップはこちら

  • golangでjunitの形式でテストのレポートを作成する場合は go-junit-report を使用するのが便利
$ go get -u github.com/jstemmer/go-junit-report 
  • テスト時に特定のディレクトリにレポートを出力することでpipelinesで表示してくれるようになる。以下がpipelinesファイルの例
bitbucket-pipelines.yml
# This is a sample build configuration for Go.
# Check our guides at https://confluence.atlassian.com/x/5Q4SMw for more examples.
# Only use spaces to indent your .yml configuration.
# -----
# You can specify a custom docker image from Docker Hub as your build environment.
image: golang:1.7

pipelines:
  default:
    - step:
        script: # Modify the commands below to build your repository.
          - PACKAGE_PATH="${GOPATH}/src/bitbucket.org/${BITBUCKET_REPO_FULL_NAME}"
          - mkdir -pv "${PACKAGE_PATH}"
          - tar -cO --exclude-vcs --exclude=bitbucket-pipelines.yml . | tar -xv -C "${PACKAGE_PATH}"
          - echo "${GOPATH}"
          - cd "${PACKAGE_PATH}"
          - go get -v
          - go build -v
          - go get -u github.com/jstemmer/go-junit-report
          - mkdir -p "/opt/atlassian/pipelines/agent/build/test-reports/"
          - go test -v 2>&1 | go-junit-report -set-exit-code=1 > /opt/atlassian/pipelines/agent/build/test-reports/junit.xml
  • mkdir -p "/opt/atlassian/pipelines/agent/build/test-reports/" で出力先のディレクトリを作成
  • go test -v 2>&1 | go-junit-report -set-exit-code=1 > /opt/atlassian/pipelines/agent/build/test-reports/junit.xml でjunit形式のレポートを作成したディレクトリに出力する
  • -set-exit-code=1 をgo-junit-reportの実行オプションに指定することで、go test の実行でFAILがあったときに、中断することができる
  • 上記のpipelinesファイルでpipelineが実行された場合、テストの失敗時にビルドが失敗する。(成功時にもレポートに失敗した情報が入っている場合はサマリが表示されるが、コマンドが成功した場合は後段のコマンドが実行されてしまう。)
  • 失敗したテストケースがない場合はサマリは表示されない

成功時

image.png

失敗時

image.png

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

A Tour of Go(Exercise: Web Crawler)をツアーで学習したことだけで解答

目的

A Tour of Goは、Goを始める人なら誰でも通る道かと思うが、Exerciseは初見では結構頭を捻った。
仮に解けなくても、多くの解答がWebにあるので、理解して進めることができる。
ただし、最後の問題だけは、探してもツアーの中で学んだものだけで解かれたものがなかった。# 探し方が悪いだけかもしれないが。。。
今回、ツアーで学んだものだけで解いたので、(スマートではないだろうが)残しておく。

問題内容

A Tour of Go | Exercise: Web Crawler

同じURLを2度取ってくることなく並列してURLを取ってくるように、 Crawl 関数を修正してみてください(注1)。

補足: 工夫すれば Crawl 関数のみの修正で実装できますが、無理に Crawl 関数内部に収める必要はありません。

ひとこと: mapにフェッチしたURLのキャッシュを保持できますが、mapだけでは並行実行時の安全性はありません!

問題となるコード
exercise-web-crawler.go
package main

import (
    "fmt"
)

type Fetcher interface {
    // Fetch returns the body of URL and
    // a slice of URLs found on that page.
    Fetch(url string) (body string, urls []string, err error)
}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        Crawl(u, depth-1, fetcher)
    }
    return
}

func main() {
    Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
    body string
    urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
    if res, ok := f[url]; ok {
        return res.body, res.urls, nil
    }
    return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
    "https://golang.org/": &fakeResult{
        "The Go Programming Language",
        []string{
            "https://golang.org/pkg/",
            "https://golang.org/cmd/",
        },
    },
    "https://golang.org/pkg/": &fakeResult{
        "Packages",
        []string{
            "https://golang.org/",
            "https://golang.org/cmd/",
            "https://golang.org/pkg/fmt/",
            "https://golang.org/pkg/os/",
        },
    },
    "https://golang.org/pkg/fmt/": &fakeResult{
        "Package fmt",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
    "https://golang.org/pkg/os/": &fakeResult{
        "Package os",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
}

このコードは何をやっているのか

まずmail()では、Crawl("https://golang.org/", 4, fetcher)を呼んでいる。
また、fetcher.Fetch(url)で取得した複数のURLに対して、Crawl(u, depth-1, fetcher)を呼んでいることがわかる。
Crawl()関数を見てみると、depth <= 0returnしており、Crawl()関数を呼ぶ度にdepth-1している。
つまり、各URLに対して最大で3回Crawl()関数が呼ばれてる。

次に、fetcherの形と値を照らし合わせて見てみる。

39-44L
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
    body string
    urls []string
}
54-61L
var fetcher = fakeFetcher{
    "https://golang.org/": &fakeResult{
        "The Go Programming Language",
        []string{
            "https://golang.org/pkg/",
            "https://golang.org/cmd/",
        },
    },

1つ目を見るとfetcherは、map[key]{ body, urls[XXX, YYY] }の形をしてることがわかる。

fetcher["https://golang.org/"].bodyを呼ぶと、The Go Programming Language
fetcher["https://golang.org/"].urlsを呼ぶと、[https://golang.org/pkg/ https://golang.org/cmd/]
が出力される。
ここまで分かると、fetcher.Fetch(url)はつまるところ、上記を返しているに過ぎないことがわかる。

そして、urlsの各々に対して、同じことを最大3回、もしくはmapのkey(url)が存在しない(=not found)と分かるまで繰り返している。

解答までの道のり

問題内容から、やることとしては大きく2つ

  1. URL取得の並列化
  2. URLを1度しか呼ばない

1.に関しては、goroutineを使えば可能。また、同期を取るためにchanelを使えばよさそう。
2.に関しては、ひとことに書いてあるとおり、mapを使えばできそう。
ただし、並列実行するので排他制御mutex(Lock(),Unlock())する必要がありそう。

まずはmain()で呼ばれているCrawl()を見てみる。

func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        Crawl(u, depth-1, fetcher)
    }
    return
}

再帰呼び出しとなっているが、chanelを使うとなると、もう1つ関数を中で作ればよさそう。

元の処理をまるっとcrawlChild()に入れてみた。

func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    var crawlChild func(string, int)
    crawlChild = func(url string, depth int) {
        if depth <= 0 {
            return
        }
        body, urls, err := fetcher.Fetch(url)
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Printf("found: %s %q\n", url, body)
        for _, u := range urls {
            go crawlChild(u, depth-1)
        }
    }
    crawlChild(url, depth)
    return
}

これで実行すると、1つしか出力されない。他のgoroutineを待たずして終わってしまうから。

found: https://golang.org/ "The Go Programming Language"

chanelを使うように修正してみる。

func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    // 追加
    ch := make(chan string, depth*depth)
    var prev int

    var crawlChild func(string, int)
    crawlChild = func(url string, depth int) {
        if depth <= 0 {
            return
        }
        // 追加
        ch <- url
        body, urls, err := fetcher.Fetch(url)
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Printf("found: %s %q\n", url, body)
        for _, u := range urls {
            go crawlChild(u, depth-1)
        }
    }
    crawlChild(url, depth)
    // 追加
    for {
        if len(ch) != prev {
            prev = len(ch)
        } else {
            break
        }
        time.Sleep(time.Millisecond)
    }
    return
}

make()でサイズを指定しないと、deadlockとなるので指定してますが、適したサイズがわからないので、depth*depthとしてみた。
そして、return前に、本来の使い方ではないけど、chanel数がこれ以上増えないと分かるまで無限ループで待つようにする。

この状態で実行すると、ようやっと最初と同じ結果が得られた。
あとは、URLを1度しか呼ばないようにするだけ。URLをkeyとした、map[string]boolで、URLを呼ぶ前にT/F判定させればよさそう。

func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    // 追加
    ch := make(chan string, depth*depth)
    var prev int
    // 追加
    cache := make(map[string]bool)
    var mutex sync.Mutex

    var crawlChild func(string, int)
    crawlChild = func(url string, depth int) {
        if depth <= 0 {
            return
        }
        // 追加
        if cache[url] == true {
            return
        }
        mutex.Lock()
        defer mutex.Unlock()
        cache[url] = true

        // 追加
        ch <- url
        body, urls, err := fetcher.Fetch(url)
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Printf("found: %s %q\n", url, body)
        for _, u := range urls {
            go crawlChild(u, depth-1)
        }
    }
    crawlChild(url, depth)
    // 追加
    for {
        if len(ch) != prev {
            prev = len(ch)
        } else {
            break
        }
        time.Sleep(time.Millisecond)
    }
    return
}

実行すると、見事に1度しか呼ばれないようになった。

found: https://golang.org/ "The Go Programming Language"
found: https://golang.org/pkg/ "Packages"
found: https://golang.org/pkg/os/ "Package os"
not found: https://golang.org/cmd/
found: https://golang.org/pkg/fmt/ "Package fmt"

Program exited.

全体のコードは、下記にあります。
a-tour-of-go/exercise-web-crawler.go at master · hirano00o/a-tour-of-go

最後に

A Tour of Goは、中々に長い道のりですが、一通り基本は身につくので、やったほうが良いかと思います。
これが終わったら、実際になにか作るか、go Wiki(英語)、CodeReviewCommentsを見てくかですかね。

CodeReviewCommentに関しては、和訳してくれた方がいるので、そちらを見ればいいのかなと思います。

参考

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

Dropbox SDK for Goを使ったファイルのアップロードやダウンロード

概要

Dropbox SDK for Goという非公式のパッケージを使ってドロップボックスにファイルのアップロードやダウンロードを行う方法を紹介します。

go version go1.12.3 darwin/amd64

パッケージのインストール

go get github.com/dropbox/dropbox-sdk-go-unofficial/dropbox

インスタンスの生成

import "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox"
import "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/files"

config := dropbox.Config{
    Token: "DROPBOX_API_TOKEN",
}
client := files.New(config)

ファイル一覧の取得

arg := files.NewListFolderArg("path")

res, err := client.ListFolder(arg)
if err != nil {
    panic(err)
}

for _, e := res.Entries {
    f, ok := e.(*files.FileMetadata)
    if ok {
        fmt.Println(f.Name)
    }
}

ファイルのダウンロード

arg := files.NewDownloadArg("filepath")

_, err := client.Download(arg)
if err != nil {
    panic(err)
}

ファイルのアップロード

新規ファイルのアップロード

arg := files.NewCommitInfo("filepath")

// io.Reader型ならなんでも良い
file, _ := os.Create("sample.txt")

_, err := client.Upload(arg, file)
if err := nil {
    panic(err)
}

既存ファイルへのアップロード(上書き)

arg := files.NewGetMetadataArg("filepath")

res, err := client.GetMetadata(arg)
if err != nil {
    panic(err)
}

var rev string
m, ok := res.(*files.FileMetadata)
if ok {
    rev = m.Rev
}

arg = files.NewCommitInfo("filepath")
args.Mode = &files.WriteMode{Tagged: dropbox.Tagged{"update"}, Update: rev}

file, _ := os.Create("sample.txt")

_, err := client.Upload(arg, file)
if err != nil {
    panic(err)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語テストメモ

学習履歴

◼️はじめに

Go 言語の学習をはじめた。

Go 言語でのテストの書き方をメモしておく。

◼️テスト実行コマンド

# カレントパッケージ内の全てのテストコードを実行
$ go test .

上記のコマンドを実行すると

  • カレントパッケージ内の全てのテストファイルをコンパイル
  • テストコードからバイナリファイルを生成
  • テストコード内の関数を実行

という、処理が流れる。

◼️テストファイル名

デフォルトだと、*_test.go のファイル名を探す。

◼️テスト設置場所

Go のテストファイルは、テスト対象のファイルと同じディレクトリに格納する。

例えば、abc パッケージ内に、hoge.go というファイルがあった場合、
test_hoge.go の格納場所は以下のようになる。

abc
 L hoge.go
 L hoge_test.go

◼️Go テスト関数

Go テスト関数のシグネチャは、以下の通り。

func Test<Name>(*testing.T)

<Name> には、任意の名前をつける事ができるが、大抵は、テスト対象の処理と関連性がある名前にする。

◼️パッケージ構成

Go ファイルは、Package に所属する必要がある。

そのため、以下の構成にする。

testTutorial
├── main.go
└── math
    ├── calc.go
    └── calc_test.go

◼️テスト対象コードを準備

calc.go に平均値を求めるコードを記述する。

calc.go
package math

// 平均値を求める関数
func Average(s []int) int {
    total := 0
    for _, i := range s {
        total += i
    }
    return int(total / len(s))
}

上記は、スライスを引数として受け取り、平均値を計算するプログラム。

main.go は動作確認用に用意した。

main.go
package main

import (
    "fmt"

    "github.com/hoge/testTutorial/math"
)

func main() {
    s := []int{1, 2, 3, 4, 5} // Go だと、ローカル変数は、一文字にするのがよしとされているらしい(slice -> s)
    fmt.Println(math.Average(s))
}

VS Code だと、Ctrl + F5 で、簡単にプログラムを実行できるので、おすすめ。

// 実行結果
3

◼️テストコード

Package testing にテストの作法が乗っている。

calc.test.go
package math

import "testing"

func TestAverage(t *testing.T) {
    v := Average([]int{1, 2, 3, 4, 5})
    if v != 3 {
        t.Error("3が返却されるはず。", v)
    }
}

t * testing.T が、python で言う所の unittest だと思う。

◼️ テストコードの実行

VS Code だと、テストファイル上に、「run test」 が表示される。

スクリーンショット 2019-04-30 7.33.15.png

  • Go というエクステンションを入れる必要があったと思う

run test を押下すると、テストが実行される。

Running tool: /usr/local/go/bin/go test -timeout 30s github.com/hogehoge/testTutorial/math -run ^(TestAverage)$ -v

=== RUN   TestAverage
--- PASS: TestAverage (0.00s)
PASS
ok      github.com/hogehoge/testTutorial/math   0.047s
Success: Tests passed

なお、上記の表示結果は、-v オプションをつけているので、詳細な情報が表示されている。

参考

コマンドライン上だと、以下のように実行する。

# テストコードのディレクトリに移動(testTutorial 上でも実行できる)
$ pwd
/Users/hoge/go/src/github.com/hogehoge/testTutorial/math

# 全てのテストを実行する(... -> 全てのテストを実行)
$ go test ./...
ok      github.com/hogehoge/testTutorial/math       0.049s

$ go test ./... -v
=== RUN   TestAverage
--- PASS: TestAverage (0.00s)
PASS
ok      github.com/hogehoge/testTutorial/math       0.034s

◼️テストを失敗させる

テストを失敗させてみる。

calc_test.go
package math

import "testing"

func TestAverage(t *testing.T) {
    v := Average([]int{10, 11, 12, 13, 14})
    if v != 3 {
        t.Error("3が返却されるはず。", v)
    }
}
Running tool: /usr/local/go/bin/go test -timeout 30s github.com/hogehoge/testTutorial/math -run ^(TestAverage)$ -v

=== RUN   TestAverage
--- FAIL: TestAverage (0.00s)
    /Users/hoge/go/src/github.com/hogehoge/testTutorial/math/calc_test.go:8: 3が返却されるはず。 12
FAIL
FAIL    github.com/hogehoge/testTutorial/math   0.014s
Error: Tests failed.

◼️テストパッケージ

3rd-party のテストパッケージで、いつか使ってみたい。

◼️まとめ

Go では最初からテストの仕組みが組み込まれているから、ツールの統一ができる。

また、テストコード自体は書きやすいのではないかという印象を受けた。

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

Goで動的にioストリームの複製

備忘録的なアレ
io.ReadFullを使わないとゼロでパディングされて
動画などは正しく復号できなくなる
ライブ配信で使えそうなやつ
たまたまhttpのレスポンスを複製して別のhttpリクエストにコピーしてるだけで
基本どのioにも使える
別にchannelでパイプする必要もないけど、処理を分けたいから

package main

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

func writeStream(pipe chan *[]byte, writer map[*io.Writer]chan bool) {
    for buf := range pipe {
        for w, c := range writer {
            _, err := (*w).Write(*buf)
            if err != nil {
                close(c)
                delete(writer, w)
            }
        }
    }
}

func readStream(pipe chan *[]byte, writer map[*io.Writer]chan bool) {
    url := ""

    resp, err := http.Get(url)
    if err != nil {
        return
    }
    defer resp.Body.Close()

    size := 1024

    for {
        if len(writer) == 0 {
            close(pipe)
            return
        }
        buf := make([]byte, size)
        _, err := io.ReadFull(resp.Body, buf)
        if err != nil {
            return
        }
        pipe <- &buf
    }
}

func main() {
    writer := make(map[*io.Writer]chan bool)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        var wb io.Writer = w
        writer[&wb] = make(chan bool)
        if len(writer) == 1 {
            pipe := make(chan *[]byte, 188*10000)
            go writeStream(pipe, writer)
            go readStream(pipe, writer)
        }
        <-writer[&wb]
        fmt.Println("done")
    })

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

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