Corredor

ウェブ、プログラミングの勉強メモ。

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!")
}

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

  • package : このプログラムを外部に公開する際に使用する、パッケージ名を指定する
  • import : 外部ライブラリ等をインポートする。ココではコンソール出力に使う fmt をインポートしている
  • func main() : func で関数宣言。main() 関数は Java などと同じく、自動的に実行されるメイン関数
  • 行末のセミコロンは付けなくて良い。書いても問題ないので、イメージ的には改行がセミコロン代わりになる、JavaScript の言語仕様っぽい感じかな

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

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

$ 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)
}

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

  • importvarconst などは、カッコ () でまとめて宣言できる
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 := ""
  • ループは for のみ、while は存在しない
  • iffor の条件部分は、カッコ () で囲まない。ココは Java などの言語と大きく違う点。コードブロックはブレース {} で囲む必要がある
  • if !*omitNewline 部分のアスタリスク * は、ポインタを示すモノらしい。ポインタはサッパリ分からないので要勉強…
  • 何らかの条件によって異常終了させたい場合は、os.Exit(1) という風に書けば良い

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

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

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

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

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

  • ./mylib/myfile.go … サブディレクトリ ./mylib/ を作りそこに置く
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
}
  • ./read.go … 上の ./mylib/myfile.go を読み込んで利用している
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 型に変更した。コレもイマイチ分かってない。

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

  • type File struct : type で型定義ができる
  • typeconstfunc などの名前を大文字で始めると、その要素をパッケージ外部に公開できる。コレにより、import した側がその要素を参照できるようになる
    • Node.js でいうと、大文字始まりの定数や関数などが module.exports に自動追加される、みたいな言語仕様になっているようだ。コレは便利かも
    • 例えば func newFile() は小文字の n で始まっているので、read.go から参照することはできない。しかし、func Open() は大文字の O で始まっているのでエクスポートされており、read.gomyFile.Open() と利用できている
  • func newFile() はアンパサンド & やアスタリスク * が登場している。これらはポインタを示す何やららしいが、よく分かっていない。関数の処理的には以下のようにも書けるので、この方が Java の「DTO」っぽい記述の仕方で、慣れてる人の方が多いかも
// newFile() 関数を以下のように書いても同じ
func newFile(fd int, name string) *File {
  var myFile = new(File)
  myFile.fd = fd
  myFile.name = name
  return myFile
}
  • func Open() は、引数指定のあとに (file *File, err error) と書いてある。コレは正常終了時の戻り値と異常終了時の戻り値の2つをセットで宣言しているモノ。Go 言語では戻り値をこのように2つ指定できるようだ
  • func Open() 内の r, e := syscall.Open(name, mode, perm) 部分も同じことで、syscall.Open() 関数が2つの戻り値を返しているので、r (= result)e (= error) の2つを変数で受け取っているのだ
  • 自分が実装した他のファイルをインポートして利用したい場合は、read.go ファイルに書いたように、import "./mylib"ディレクトリパスを指定する
    • こうすると、そのディレクトリ配下にある .go ファイルを読み込めるようになる
  • ディレクトリごと読み込んだ後は、各 .go ファイルの package 宣言で指定した名前を使い、myFile.Open() といった要領で、関数や定数などを参照できる
  • インポートしたファイルたちの中に main() 関数があると、複数の main() 関数が存在するとして実行できないので注意
  • nullnil と表記。Ruby っぽい
  • if 文における else 句は、} else { と1行で書かないとエラーになる。個人的には Java や JS で普段このように1行では書かないので、慣れない…
if file == nil {
  // 処理
}
else {
  // ↑ このように「}」と「else {」を別の行に分けて書くとエラーになる
}

今日はココまで

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

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

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

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)