2013/06/29


先日、Go言語開発チームはリポジトリ内にあった殆どのMakefileを削除した。私(訳者)は混乱したし不安にもなった。しかしそれは私がこれまでの習慣と異なる場面に遭遇した事による物だと気付いた。その事を色濃く書かれたいる記事があったので紹介したい。Go言語を知らない人でも面白く読めると思います。
Are You Fuckign Coding Me!? - The go tool
http://areyoufuckingcoding.me/2012/02/14/the-go-tool/
本訳を許諾してくれたnu7hatchに感謝したい。(Thanks to nu7hatch)

THE GO TOOL

毎週リリースされる最新版に新しいgoコマンドが導入され話題になっていたので、ちょっとこれについて書くことにした。 私は初め、このgo toolを統一しようというアイデアを聞いた時、少し懐疑的になり不安でいっぱいになった事を認めざるを得ない。 それが他の言語固有のパッケージマネージャの様にめちゃくちゃになるのではないかと心配した。 それらパッケージマネージャのほとんどは私の知る限り車輪の再発明であり、そのオペレーティングシステム上のパッケージマネージャと衝突していてシステム管理者を苦しませているのだ。 さらに言うと、私はmakefileが好きだった。本当に好きだった。単純明快だったしうまく動いた。 幸運にも新しいのgo toolが私の恐怖の全てを拭い去った!

繰り返すな...

新しいgo toolに関する情報の多くはgo nutsメーリングリストに流れている。 公式のgoのドキュメントにも現在、go toolを使ってのコードの書き方が短めの記事となって幾らか含まれている。

現時点ではドキュメントとのギャップがかなりあると思うので、新しいgo toolの採用と幾らかの便利なトリックを逸早く書くことが合理的だと思う。

設定よりも取り決め

私の最大の恐怖の源、その理由はRuby on Railsの経験によるものだ。 Railsをよく理解しる開発者は、ちょっとしたハックや、言ってしまえばルールに沿わないちょっとしたトリッキーな何かをしようとする場合、その全ての時間を費やす事に同意しなければならない。

IMPOSSIBRU!

しかし良い練習について説明して行こう。まず第一にgo toolの各々は一つの物事だけを行う。そしてそれは正しい。あるべき物はそこにある:

  • go build - パッケージのビルド
  • go get - 依存の解決とインストール
  • go test - テストスーツの実行とベンチマーク
  • go install - パッケージのインストール
  • go doc - ドキュメントの生成
  • go fmt - コードの整形
  • go run - アプリケーションのビルドと実行
  • go tool - その他ツールの呼び出し
  • その他...

Goのパッケージはビルドの設定といった物を全く持っていない。 makefileもない。その他、依存性の記述も無い。 じゃぁどの様になるのか。全てはソースコードから検索される。 マジックを起こすには、最初に行わなければならない事が一つある。 goのスタッフがどこにあるのかを明示する必要がある。 GOPATH環境変数でgoのツリーへのパスを定義する。 例えば、~/.bashrcの中での以下の様に:

GOPATH="/home/nu7/gocode"

...はgoのツリーが指定した場所に存在する事をgo toolに教えているのだ。 goのツリーって実際なに?と聞くかもしれない。簡単に言うと 君のソース、パッケージ、コマンドの全てが格納される場所だと答えておこう。 まぁ見なさい: ls /home/nu7/gocode/
bin   pkg   src

全てのソースはsrcフォルダーの中に位置するであろう。これはアプリケーション、パッケージ、そして依存している物、全てのソースという意味だ。 pkgフォルダーはコンパイルされてインストールされたパッケージが含まれ、cmdにはコマンドがインストールされる。

GOPATH変数はPATHに非常に似ており、必要なだけ多数のgoパスを設定出来る。 君はそれらの内、最初の一つをメインである覚えておかなけいけない。なぜならgo installでインストールされる全てはそこに入るからだ。

依存の解決

依存を明示する設定ファイルは無い... じゃぁどうやってgo toolはそれをどこからダウンロードして、どこにインストールしているのかを知り得るんだよ! リポジトリだろって思った?ノー、そんな物は無い。 Goはimportpathと呼ばれる物を導入している。 まぁ見なさい:

import "github.com/nu7hatch/gouuid"

import pathはツーインワンだ。 リポジトリURLでもあり、パッケージがローカルにインストールされる場所へのパスでもある。 go getツールはどこにフェッチすべき依存物があるかをimport pathを見て知る。 またgo buildはローカルにあるそれらをインポートする場所を知る。

システムに依存している物をインストールするには次のようにgo getツールを使う必要がある:

$ go get package-name

待った待った、ちょっと待った... このパッケージ名って何? これはインストールしたい依存物のパッケージ名だ。 例えばgoのソースとしてfooという名前のパッケージがあった場合、go get fooを呼び出す事で全ての依存物がインストールされる。 パッケージから直接このツールを使う事も出来る:

$ cd ~/gocode/src/foo
$ go get .

その他の全てのgo toolは同じ様に動作し、パッケージ、あるいはそのimport pathの指定から直接呼び出せる。 また ... ワイルドカード(3つのドット)を使ってネストされたパッケージのグループにおいても使う事ができ、fooパッケージがいくつかの入れ子のパッケージを含んでいる場合、それら依存する全てはこれをやるだけで同時にインストールすることが出来る:

$ go get ./...

goのツリーに指定した依存物が既にインストールされている場合、明示的に要求しない限り更新されない。 依存しているパッケージを更新するにはgo get-uフラグを付ける:

$ go get -u package-name

簡単でしょ?

依存地獄!

go toolを好きであると同時に恐れている、もう一つの取り決めがある。 Go toolはリポジトリのHEADバージョンをチェックして依存を解決する。 これはパッケージメンテナに後方互換性を維持する事を強要する。

GREEN MASTER MOTHERFUCKER! DO YOU HAVE IT?

グリーンマスターポリシーは私が仕事で常に主張していた事だった。 デフォルトブランチは人々がまず最初にチェックする物であり、したがってそれはグリーン(テストが全てパスした状態)であるべきでそれを動作させる為には少なくとも最新でなければならない! 一度公けに公開した、もしくは十分開発した段階ならば、後方互換性を持つべきだ。 我々は何かを廃止したり、パッチやマイナーバージョンである中でAPIの変更をする事は出来ない。

でも我々は練習の中でそれがどの様になるのかの全てを得て知る。 多くの人々は後方互換性なんて糞の様な物は提供しないし、彼らは遊び場としてデフォルトブランチを使う。 彼らの為に、そして新しいgo toolで平穏に暮らしたい開発者の為に、この取り決めのセットを

君がプログラマ人生を送る中で守るべきバカバカしいルール:

  • マスターブランチを常にグリーンにしとけよ!バカ
  • 新機能は別ブランチでやれ!ボケ
  • 一度コード公開しといて誰かが使ってるのにAPI変えるなクソがぁ!
  • API変えたいとか、どうして変える必要があるならメジャーバージョンで変えろよ。んでもってオリジナルブランチから派生ブランチに分けて作業しろよ!カス
  • もしどうしても、どうしても特別なタグやブランチ、もしくは依存としてコミットする必要があるなら、デフォルトに指定したリポジトリを自分でforkしてコミットに使えよ!このウンコが!
  • おいおい...簡単にしとけよ マザコン!

エルサレムの本を思い出しちゃうから何度も言わせないでくれ...

ビルドとインストール

Ok。goコマンドに話を戻そう。go buildコマンドはパッケージをコンパイルするのに使われる。 パッケージをビルドするだけでインストールはしない。 重要なのはパッケージはローカルソースツリーの中でチェックされなければならないという事。 代わりにリモートパッケージのインストールにはgo getが使われる:

$ go get github.com/nu7hatch/gouuid

ローカルのパッケージをインストールするためにはgo installツールが使われる。 これは最初にパッケージをビルドし(必要な場合)、$GOPATH/pkg、あるいは$GOPATH/cmd配下の物をインストールする。

go toolはフラグや特別な設定をしない限り特定のファイルをビルドから除外する事も出来る。 無視する為にやるべき事は、名前の先頭にアンダースコアを付けるだけ:

$ ls
_bar.go foo.go
$ go build .

上の例の_bar.goのはビルドからは無視される。

ふむ。この通り。これに関して言う事は何もない。先に進もう。

CGOによるC言語拡張

cgoコマンドによるC言語拡張の作成はかなり素晴らしい。 実際にC言語アプリケーションの殆どをビルドするのにcgoについてもっと知っておく必要は無く、go buildツールで十分なのだ。

素直に言うとcgoに関してはあまり言うことは無く、それらの殆どはだいたいドキュメント、そしてgo users wikiにある記事の中に記述されている。

まず言っておくべき事として私がそれが好きではない。オフィシャルの殆どの例で示される様にコメントの中にC言語のソースコードをそのまま配置している。 マジで本当に好きになれない。 主としてコードの量を最小化し、かつ単一のファイルで各例を示すために、サンプルがこの方法で提供されている事を知っておくべきだ。 現実のアプリケーションではC言語のコードはコメントブロックに配置すべきじゃないgo buildツールはパッケージ内の.h.cといったファイルをちゃんとスマートに扱ってくれるんだ。

例がいる? stdio.hにある関数printfを使って引数を画面に表示する単純なechoコマンドを書こう。 wikiページで言及されている様に、goはC言語の可変長引数の関数を呼び出す事は許していない。 よってprintf関数の小さなラッパを書かなければならない。 コードはgithubにある物の様になるだろう:

echo.h:

#ifndef _ECHO_H_
#define _ECHO_H_

#include <stdio.h>

void echo(char*);

#endif /* _ECHO_H_ */

echo.c:

#include "echo.h"

void echo(char* s)
{
    printf("%s\n", s);
}

echo.go:

package main

/*
#include <stdlib.h>
#include "echo.h"
*/
import "C"

import (
    "flag"
    "unsafe"
    "strings"
)

func main() {
    flag.Parse()
    cs := C.CString(strings.Join(flag.Args(), " "))
    C.echo(cs)
    C.free(unsafe.Pointer(cs))
}

これで君もgo buildツールでシームレスにビルド出来る様になる。 パッケージにある全てのC言語ファイルを認識し、コンパイルする。 つまりはこれだけでいいんだ!

NOT BAD

プラットフォーム固有のビルド

もう一つ素晴らしく興味深い事にgo buildはプラットフォーム固有のファイルについてのコンパイルをハンドリング出来る事だ。ファイルを名前から解析している。(こんな感じ: file_GOOS_GOARCH.go もしくは file_GOARCH.go):

foo_darwin_amd64.go
foo_386.go
foo.go

この機能はC言語のファイルでもちゃんと動く:

foo_amd64.c
foo_386.c
foo.h
foo.go

ドキュメントの中で言われている通り、これらの機能は必要ない。しかし go toolが単純なわりに如何に柔軟なのかを色濃く表している事を言及しておきたい。

Ok。でも君はきっとこう聞くだろう。もしコンパイラのフラグや幾つかの設定など、何かトリッキーな事が必要な場合は?と

救うべきMakefile!

そう、makefileを使う事を恐れてはいけない! これは拡張設定による取り決めや、先行条件等に対処する為の、最も簡単で全く便利な方法だ。 Makefileは、C言語の拡張にだけ役立つ物ではなく、色んなパッケージにおいても同様に適用出来る(例えばwebrocketのトップレベルでは作業を楽にする為にmakefileを使っている)。

より明示的な例... それをベースとしているコアパッケージやコマンドラインツー ルを含んでいるアプリケーションを想像してみて欲しい。 このechoの例だとモジュールが多くてもやり遂げられるだろう:

echo/
pkg/
echo/
echo.c
echo.h
echo.go
cmd/
echo/
echo.go

pkg/echoパッケージはC言語のprintf関数を再利用出 来る様にラップし、そのソースは前述の例と殆ど同じであるとしよう。 cmd/echoコマンドはコアパッケージを使って画面に何かを出力する実行モジュールとしよう。 cmd/echoコマンドはこんな感じになるだろう:

package main

import (
    "github.com/nu7hatch/cgoecho2/pkg/echo"
    "flag"
)

func main() {
    flag.Parse()
    echo.Echo(flag.Args()...)
}

Note: slice... の意味を知らない人々のために言うと、引数の数に相当する変数へsliceをマップする物だ。Rubyで言うと*argsの様な物だ。

話を戻そう。Makefileが必要なパッケージの為に私達が用意しなければならない簡単な物、それは以下の様な物だ: all: echo-pkg echo-cmd

echo-pkg:
    go build ./pkg/echo

echo-cmd:
    go build ./cmd/echo

これによりmakeを呼び出すだけで両方を瞬時にコンパイルでき、もっと重要な事を言うとリモートからそれらをインストールするのにもgoコマンドを使う事が出来るんだ。:

$ go get github.com/nu7hatch/cgoecho2/cmd/echo

もちろんこれは非常に単純な例だ。ソースの全てを俊敏にビルドするにはワイルドカード使う事も出来る。慌てずに:

$ go build ./...

しかし、多くのパッケージおよび(または)コマンドを含む巨大なアプリケーションを配布するのはトリッキーになりがちだ。 その際makefileやシェルスクリプト、君の好きな他のビルドツールを使うのは妥当な事なのだ。

まとめ

ハッキリと言おう。私は新しいgo toolをマジで愛してやまない。! 初め、それを触って遊んでいる間に山の様に問題に遭遇した。 しかしほとんどの問題は、他のパッケージマネージャ/ツールを使用する間に、私が得てきた幾つかの悪い習慣によって引き起こされた物だった。 はぁ...最近、go-nutsのIRCチャネルで愚かな質問を沢山してしまったよ。 そして私が得る答えは馬鹿らしく明白で単純だった...

以前使っていたeasy_installrubygemsbuilderといったこれまでのツールを担う物と考えていている。 私の心の中に一枚だけ写真がある... あぁ、でも嫌いになられそうなので公開しない方がよさそうだ。 :) 代わりといってはなんだが、新しいgo toolを私がどの様に感じているかをお見せしよう...

...

私はgoが正しい向に向かっている事を見る事が出来てとてもうれしい。今日はこの辺にしておく。ごきげんよう。Gophers!

Posted at by



2013/06/27


先日twitterで「C++でデバッグする時、よくやるよね」って言ったら結構知らない人がいたのでここでも紹介してみる。
既存のコードでcout/cerrを使ったデバッグ文がわんさかあって、これログファイルとして出力したいな...って場合ありますよね。
そんな場合
#include <iostream>
#include <fstream>

using namespace std;

int main() {
    // こんなの
    ofstream ofs("debug.log");
    cout.rdbuf(ofs.rdbuf());
    // いれとく

    cout << "debug string" << endl;
}

こうしておくと、その後のcoutへの出力が全てdebug.logというファイルへ出力される。
なおrdbufを元に戻すには #include <iostream>
#include <fstream>

using namespace std;

int main() {
    ofstream ofs("debug.log");
    streambuf* oldrdbuf = cout.rdbuf(ofs.rdbuf());

    cout << "output to file" << endl;

    cout.rdbuf(oldrdbuf);

    cout << "output to console" << endl;
}
こうやる。
ちなみに、昔のC言語使いのオッサン達はこうやる。
freopen("file.txt""w"stdout);
これをもう少し発展させて、coutへの出力をログファイルぽくする物を先日書いたので紹介してみる。
/* Copyright 2011 by Yasuhiro Matsumoto
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#include <streambuf>
#include <sstream>
#include <ctime>

template <class CharT, class Traits = std::char_traits<CharT> >
class basic_clogger : public std::basic_streambuf<CharT, Traits> {
public:
  typedef std::basic_streambuf<CharT, Traits> streambuf_type;
  typedef Traits traits_t;
  typedef CharT char_t;
  typedef typename traits_t::int_type int_t;
  typedef std::streamsize streamsize_t;
  typedef std::basic_stringstream<char_t, traits_t> sstream_t;

private:
  streambuf_type* sbuf_;
  char_t* cbuf_;
  static int const BUFFER_SIZE = 256;

public:

  basic_clogger(streambuf_type* buf) :sbuf_(buf),
      cbuf_(new char_t[BUFFER_SIZE]) {
    this->setp(cbuf_, cbuf_ + BUFFER_SIZE);
  }

  ~basic_clogger() {
    this->pubsync();
    delete[] cbuf_;
  }

protected:

  int_t
  overflow(int_t c = traits_t::eof()) {
    streamsize_t n = static_cast<streamsize_t>(this->pptr() - this->pbase());
    time_t ct;
    int year, month, day, hour, minute, second;
    time(&ct);
    struct tm* d = localtime(&ct);
    std::basic_stringstream<char_t, traits_t> ss;

    for (streamsize_t i = 0; i < n; i++)
      if (cbuf_[i] == '\n') { n = i; break; }
    if (n) {
#define CLOGGER_PAD4(o,d) if(d<10)o<<'0'<<'0'<<d; else if (d<100)o<<'0'<<d; else o<<d;
#define CLOGGER_PAD2(o,d) if(d<10)o<<'0'<<d; else o<<d;
      CLOGGER_PAD4(ss, d->tm_year + 1900);
      ss << '/';
      CLOGGER_PAD2(ss, d->tm_mon + 1)
      ss << '/';
      CLOGGER_PAD2(ss, d->tm_mday + 1)
      ss << ' ';
      CLOGGER_PAD2(ss, d->tm_hour)
      ss << ':';
      CLOGGER_PAD2(ss, d->tm_min)
      ss << ':';
      CLOGGER_PAD2(ss, d->tm_sec)
#undef CLOGGER_PAD4
#undef CLOGGER_PAD2
      ss << '\t';
      ss << std::basic_string<char_t>(cbuf_, n) << std::endl;
      std::basic_string<char_t> s = ss.str();
      if (sbuf_->sputn(s.c_str(), s.size()) != s.size())
        return traits_t::eof();
      this->setp(cbuf_, cbuf_ + BUFFER_SIZE);
    } 

    if (!traits_t::eq_int_type(c, traits_t::eof())) {
      traits_t::assign(*this->pptr(), traits_t::to_char_type(c));
      this->pbump(1);
    } 
    return traits_t::not_eof(c);
  }

  int
  sync() {
    if (traits_t::eq_int_type(overflow(traits_t::eof()), traits_t::eof())) {
      return -1;
    }
    return sbuf_->pubsync() == -1 ? -1 : 0;
  }
};

typedef basic_clogger<char> clogger;
ちょっと長ったらしいけど...。これを以下の様にして使う。
#include <iostream>
#include <fstream>
#ifdef _WIN32
#include <windows.h>
#define sleep(x) Sleep(x*1000)
#else
#include <unistd.h>
#endif

int
main() {
  std::ofstream file("debug.log", std::ios::out | std::ios::app);
  clogger logger(file.rdbuf());
  std::streambuf* oldrdbuf = std::cout.rdbuf(&logger);

  std::cout << "Yes" << std::endl;
  sleep(1);
  std::cout << "高洲" << std::endl;
  sleep(1);
  std::cout << "クリニック" << std::endl;

  std::cout.rdbuf(oldrdbuf);
}
するとdebug.logというファイルに 2011/04/03 01:26:09 Yes
2011/04/03 01:26:10 高洲
2011/04/03 01:26:11 クリニック
こう出力される。

Yes 高須クリニック!


良いC++デバッグライフをお送り下さい。
Posted at by



2013/06/20


元ネタ: 誰もが一度は陥る日付処理。各種プログラミング言語におけるDateTime型/TimeStamp型の変換方法のまとめ
Go言語が無かったので書いてみた。

現在時刻の取得

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println(time.Now())
}
2013-06-19 21:46:14.186298 +0900 +0900

Time => Unix時刻変換

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println(time.Now().Unix())
}
1371646123

Unix時刻 => Time変換

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println(time.Unix(13716461230))
}
ここからが面白くて、通常日付のフォーマットやパースは殆どの言語では %Y%m%d といった表記を使います。 しかしGo言語では、ある固定の数値を用いて表現する事で、いかにも日付らしく表現出来る手法を取っています。 package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
    t, err := time.Parse(
        "2006-01-02 15:04:05 -0700",    // スキャンフォーマット
        "2013-06-19 21:54:23 +0900")    // パースしたい文字列
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(t)
}
Formatも同じく package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println(time.Now().Format("2006/01/02 15:04:05 MST"))
}
2013/06/19 22:01:01 +0900 この数値とタイムゾーン文字列で構成された文字列を、定数として提供しています。 const (
    ANSIC       = "Mon Jan _2 15:04:05 2006"
    UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
    RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
    RFC822      = "02 Jan 06 15:04 MST"
    RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
    RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
    RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
    RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
    RFC3339     = "2006-01-02T15:04:05Z07:00"
    RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
    Kitchen     = "3:04PM"
    // Handy time stamps.
    Stamp      = "Jan _2 15:04:05"
    StampMilli = "Jan _2 15:04:05.000"
    StampMicro = "Jan _2 15:04:05.000000"
    StampNano  = "Jan _2 15:04:05.000000000"
)
この手法を使う事で、ちょっとした日付フォーマットの差異も自分のプログラムで吸収する事が出来る様になっています。
初めてこの実装を見た時は戸惑いましたが、慣れると非常に心地よくなります。

おまけでもう少し。

Go言語では、timeパッケージを使って経過時間(Duration)も表す事が出来ます。 time.Sleep(1 * time.Second) // 1秒
time.Sleep(2 * time.Minute) // 2分
time.Sleep(3 * time.Hour)   // 3時間
1時間と61秒は、61分 (1 * time.Hour + 61 * time.Second).Minutes()
日付の加算も出来る。閏年も完璧。 package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println(time.Date(20122281213140, time.UTC).AddDate(001))
    // 2012-02-29 12:13:14 +0000 UTC
    fmt.Println(time.Date(20122291213140, time.UTC).AddDate(001))
    // 2012-03-01 12:13:14 +0000 UTC
}
このDurationを使って、数秒後に発動するイベント(channel)も作れます。例えば、3秒以内に CTRL-C を押すかのイベントループ package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    s := make(chan os.Signal)
    signal.Notify(s, syscall.SIGINT)

    select {
    case <- time.After(3 * time.Second):
        fmt.Println("Timeout")
        break
    case <- s:
        fmt.Println("Pressed CTRL-C")
        break
    }
}
3秒後にコールバック package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    time.AfterFunc(3 * time.Second, func() {
        fmt.Println("Timeout")
        os.Exit(0)
    })

    select {}
}
とまぁ、とても便利になっています。Go言語触った事ないとか、かなりヤバいです。

「えーマジGo言語童貞!?」
「Go言語童貞が許されるのは小学生までだよね」
「キモーイ」
「キャハハハハハハ」
Posted at by