Go 言語を触ってみる

最近 GitHub で見かけるコマンドラインツールの多くで、PythonGo 言語が採用されているのを見かける。最終的にシングルバイナリにビルドできるっぽくて、OS 間の差異も上手く吸収できそうなので、少し触ってみることにした。

今回はネットで見つけた入門系記事を試してみただけだが、その中でも少しつまづくポイントがあったので、記事にした次第。

目次

Go 言語の開発環境・実行環境の準備

MacOS Mojave の場合は、Homebrew を使ってインストールすると楽。

$ brew install go

Windows の場合は未検証だが、Chocolatey で同じようにインストールできる様子。

> choco install golang

インストールができたら、PATH を通すため ~/.bash_profile 辺りに以下のように追記する。

export GOPATH="${HOME}/go"
export PATH="${GOPATH}/bin:${PATH}"

ターミナル上で $ go version コマンドが通るようになっていれば OK。

# MacOS での例
$ go version
go version go1.12.5 darwin/amd64

コレで Go 言語のファイルをビルドしたり実行したりする基盤はできた。

今回は特別な IDE 等は用意せず、VSCode を使う。特に拡張機能も使わないので、CotEditor や NotePad++ など、簡単なエディタとターミナルツールだけあれば良い。

簡単なコードを書いてみる

まずは最も簡単な、Hello World を出力するだけのプログラムを書いてみる。以下のような内容の hello.go ファイルを作る。

package main

import "fmt"

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

ココでの留意点は以下のとおり。

ということで、コレを実行してみる。

ビルドしてバイナリファイルを作り、それを実行する場合は以下のとおり。

$ go build ./hello.go

コレで 2MB 程度の hello というバイナリファイルがコンパイルされる。

$ ./hello
hello, world!

あとは上のように実行すれば良い。

他にも、.go ファイルをコンパイルせず直接実行する方法もある。import 等の制約があるので、依存関係があるファイルでは上手くいかないこともあるようだが、今回のレベルであれば問題なし。

$ go run ./hello.go
hello, world!

echo コマンド的なモノを作ってみる

続いて、echo コマンドのように、引数を受け取ってコンソール出力する、というプログラムを作ってみる。

package main

import (
  "os"
  "flag"  // コマンドラインオプションのパーサ
)

// -n オプションを用意する。指定した場合は最後に改行を含めないで出力する
var omitNewline = flag.Bool("n", false, "don't print final newline")

// 定数宣言
const (
  Space   = " "
  Newline = "\n"
)

// メイン関数
func main() {
  // パラメータリストを調べて flag に設定する
  flag.Parse()
  
  // 変数宣言
  var s string = ""
  
  // 引数を順に処理する
  for i := 0; i < flag.NArg(); i++ {
    if i > 0 {
      s += Space
    }
    s += flag.Arg(i)
  }
  
  // -n オプションによるフラグがなければ改行を付与する
  if !*omitNewline {
    s += Newline
  }
  
  // コンソール出力する
  os.Stdout.WriteString(s)
}

ココでの留意点は以下のとおり。

import (
  "os";    // ← セミコロンはこの位置に打っても、打たなくても良い
  "flag";
)
const (
  Space   = " "
  Newline = "\n"
)

// 以下のように書いたのと同じ
import "os"
import "flag"
const Space   = " "
const Newline = "\n"
var s string = ""

// コレでも代入している値から string 型と分かる
var s = ""

// var 宣言は以下のように省略して書ける (コード中では for 文で使用している)
s := ""

このコードを myecho.go として保存したら、以下のように実行できる。

$ go run ./myecho.go HOGE FUGA
HOGE FUGA

ファイルを読み込んでみる

今回の最後は、ファイルを読み込むサンプルコード。ココでかなりつまづいた。

先に動作する正解のコードを載せておく。

package myFile  // パッケージ名
import "syscall"

// 型定義
type File struct {
  fd   int     // ファイル記述子番号
  name string  // ファイルを開く時の名前
}

// インスタンスを生成するファクトリ関数
func newFile(fd int, name string) *File {
  if fd < 0 {
    return nil
  }
  return &File{fd, name}
}

// ファイルを開き File 型のインスタンスとして返す
func Open(name string, mode int, perm uint32) (file *File, err error) {
  r, e := syscall.Open(name, mode, perm)
  err = e
  return newFile(r, name), err
}
package main
import (
  "./mylib"  // ディレクトリを読み込む
  "fmt"
  "os"
)

func main() {
  // 開きたいファイルのフルパスを指定する
  var path = "/does/not/exist"
  
  // パッケージ名 myFile を指定する
  file, err := myFile.Open(path, 0, 0)
  
  if file == nil {
    fmt.Printf("can't open file; err=%s\n", err)
    os.Exit(1)
  } else {
    fmt.Printf("OK")
  }
}

コードとしてはこんな感じ。

$ go run ./read.go

と実行すると、変数 path で指定したファイルを読み込み、「OK」だったり「can't open file」エラーだったりを出力する、というモノ。

このコードのベースにしたのは、以下の記事。

この記事では、os パッケージを import し、Open() 関数のエラーを os.Error 型で返そうとしていたが、どうも最近の Go 言語では os.Error がなくなっているらしく、このコードはエラーが出て動かなかったので、なんとなくで直した。元はこんなコードが含まれていた。

// 上の動作するコードでは「err = e」とした部分は、代わりにこうなっていた
if e != 0 {
  err = os.Errno(e)
}

err = e と直したら動いたが、コレでいいのかはよく分かっていない。

あと Open() 関数の引数 perm は、int 型では cannot use perm (type int) as type uint32 in argument to syscall.Open とかいうエラーが出てしまったので、unit32 型に変更した。コレもイマイチ分かってない。

大きな変更点はこのくらい。あとは言語仕様について学んだこと。

// newFile() 関数を以下のように書いても同じ
func newFile(fd int, name string) *File {
  var myFile = new(File)
  myFile.fd = fd
  myFile.name = name
  return myFile
}
if file == nil {
  // 処理
}
else {
  // ↑ このように「}」と「else {」を別の行に分けて書くとエラーになる
}

今日はココまで

今回はこの辺りにしておこう。

Go 言語は実行環境のインストールも簡単で、言語仕様も色々と理にかなった作りになっていてとっつきやすそうだ。

ネット上の文献は古今混在しているようで、最後の「ファイル読み込み」の例のように、現在では動かなくなっているコードもあるので、情報の鮮度を見極めて学習していきたい。