Go言語標準パッケージの深堀調査 - io.Copy()

Go言語標準パッケージへの理解を深めるために、ioパッケージの深堀調査をおこないました。今回はio.Copy関数を調査したので、処理の流れを記事にまとめておきます。

io.Copy()で出来ること

io.Copy関数により、ファイルや標準入力等から読み込んだデータを、標準出力やファイルへ書き出すことが可能です。io.Copy関数は引数としてio.Writerとio.Readerを受け取ります。関数の出力として、コピーされたデータサイズとエラー情報が返されます。

func Copy(dst io.Writer, src io.Reader) (written int64, err error)

io.Copy()のサンプルプログラム

Go言語の公式ドキュメントで紹介されているプログラムを動作確認してみます。このプログラムでは、与えられた文字列をReaderで読み込み、データをos.Stdoutへコピーしています。os.Stdoutは標準出力へデータを書き出すWriterです。

package main

import (
	"io"
	"log"
	"os"
	"strings"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")

	if _, err := io.Copy(os.Stdout, r); err != nil {
		log.Fatal(err)
	}
}
動作結果(標準出力)
some io.Reader stream to be read

io.Copy()の中身

io.Copy関数では、copyBuffer関数が呼ばれていました。この関数はバッファを用いてコピーを行う関数のようです。第三引数でバッファを指定できるのですが、Copy関数ではnilを指定しています。

func Copy(dst Writer, src Reader) (written int64, err error) {
	return copyBuffer(dst, src, nil)
}

さらにcopyBuffer関数の中身を見てみます。

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// If the reader has a WriteTo method, use it to do the copy.
	// Avoids an allocation and a copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}
	if buf == nil {
		size := 32 * 1024
		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
			if l.N < 1 {
				size = 1
			} else {
				size = int(l.N)
			}
		}
		buf = make([]byte, size)
	}
	for {
		nr, er := src.Read(buf)
		if nr > 0 {
			nw, ew := dst.Write(buf[0:nr])
			if nw > 0 {
				written += int64(nw)
			}
			if ew != nil {
				err = ew
				break
			}
			if nr != nw {
				err = ErrShortWrite
				break
			}
		}
		if er != nil {
			if er != EOF {
				err = er
			}
			break
		}
	}
	return written, err
}

序盤のプログラムでは、srcのWriteToメソッド、dstのReadFromメソッドが使えないかチェックしています。もし使える場合、それらのメソッドによってデータのコピーを行っています。
今回のサンプルプログラムでは、src(strings.Reader)にWriteToメソッドが実装されていました。そのため、wt.WriteTo(dst)によってデータコピーが行われました。
後半のプログラムでは、Read・Writeメソッドを使用しつつデータコピーを行っています。src.Read(buf)によりバッファにデータを読み込み、dst.Write(buf[0:nr])によりバッファの中身を読み込んだ分だけ書き出しています。

感想

io.Copy関数のコードを確認することで、データコピーがどのように実行されるのかを整理することができました。io.Writerとio.Readerの実装に関して、パッケージごとに読み書きの処理が異なるので、他のパッケージでの実装も参照していきたいと思います。
今回が初めてのコードリーディングで、やり方が分からず情報整理に苦戦しました。他の方々のブログを参照し、自分に合ったリーディング方法を見つけたいと思います。

AngularアプリをHerokuにデプロイする

AngularアプリをHerokuにどうやってデプロイするのか気になったので、調査・検証しました。"ng new"で作成されたAngularプロジェクトをGithub経由でデプロイすることができたので、手順を記録しておきます。

作業環境

  • Angular CLI : 11.2.11
  • Node : 14.15.5
  • Git : 2.19.0
  • OS : Windows10
  • Github, Herokuのアカウントを作成しておく

手順

  1. "ng new <アプリ名>"でAngularプロジェクトを作成する
  2. Angularプロジェクトをデプロイ用に修正
  3. AngularプロジェクトをGithubリポジトリに登録する
  4. Herokuにて"Create new app"をクリック。アプリ名とデプロイ方法を設定(Github連携を指定)
  5. Githubリポジトリの更新が起きるたびに、自動ビルド・デプロイが行われる。(手動でのビルドも可能)

各手順の内容を書いていきます。

Angularプロジェクトの作成

任意のフォルダにて、"ng new <アプリ名>"を実行します。(本記事では"angular-heroku-app"という名前でプロジェクトを作成しました。)f:id:kurobuchimeganex:20210505161535p:plain
作成されたプロジェクトの構成は以下の通りになります。
f:id:kurobuchimeganex:20210505161949p:plain
cdコマンドでプロジェクト内に移動して、"ng serve"を実行します。すると、Angularアプリをローカルで起動することができます。ブラウザ上で"http://localhost:4200"にアクセスすることで、起動中のAngularアプリを利用できます。

f:id:kurobuchimeganex:20210505162440p:plain
ng serveの実行結果
f:id:kurobuchimeganex:20210505163033p:plain
ブラウザ上でAngularアプリにアクセス(ローカル)

Angularプロジェクトをデプロイ用に修正

AngularアプリをHerokuでも使えるようにするために、プロジェクトを修正します。以下の手順を実施しました。

  • npmでexpressパッケージをインストール
  • "ng build --prod"を実行。(やらなくても良い)
  • server.jsを作成し、ローカルでアクセスできるか試す。(やらなくても良い)
  • package.jsonにて"start", "build"項目を修正

まず、プロジェクトルート内で"npm install express --save"を実行し、expressパッケージをインストールします。
次に、"ng build --prod"を実行し、プロジェクトをローカルでビルドします。ビルドがうまくいくと、プロジェクトフォルダ内にで"dist/アプリ名"のフォルダが生成されます。

f:id:kurobuchimeganex:20210505180443p:plain
ビルド後にdistフォルダが生成される

ビルドが上手くいったら、プロジェクトロート直下にserver.jsを作成し、Webサーバーを実装します。

const express = require('express');
const path = require('path');

const app = express();

// 'dist/アプリ名'を指定
app.use(express.static(path.join(__dirname, 'dist/angular-heroku-app')));

// 'dist/アプリ名/index.html'を指定
app.get('/*', (req, res, next) => {
    res.sendFile(path.join(__dirname, 'dist/angular-heroku-app/index.html'));
});

app.listen(process.env.PORT || 8080);

プロジェクトルートにて、"node server.js"を実行してみます。ブラウザで"http://localhost:8080"にアクセスし、Angularアプリが上手く動作するか確かめます。
最後に、package.jsonにて、start項目とbuild項目を修正します。start項目は"node server.js"を設定し、build項目は"ng build --prod"を設定します。

f:id:kurobuchimeganex:20210505174637p:plain
package.jsonの編集(start項目とbuild項目)

AngularプロジェクトをGithubリポジトリに登録

作成したAngularプロジェクトをGithubリポジトリに登録します。まずは、Githubにてリポジトリを作成します。Githubにログインして、"new repository"をクリックします。

f:id:kurobuchimeganex:20210505163538p:plain
リポジトリの作成

f:id:kurobuchimeganex:20210505164138p:plain
Githubリポジトリを作成したら、ローカルのAngularプロジェクトをGithubリポジトリに登録します。この時、ブランチ名を"master"から"main"に変更しています。

f:id:kurobuchimeganex:20210505165340p:plain
GithubリポジトリへAngularプロジェクトを登録

Herokuでアプリを作成

Herokuにログインし、画面右上の"Create new app"をクリックします。すると、アプリ名を入力する画面になります。この時、他のデプロイされているアプリの名前と被らないようにする必要があります。

f:id:kurobuchimeganex:20210505165906p:plain
"Create new app"をクリック
f:id:kurobuchimeganex:20210505170144p:plain
アプリ名の入力

"Create App"ボタンをクリックすると、デプロイ方法を選択する画面に移ります。今回Githubを使ったデプロイのため、Deployment method項目にてGithubを選択します。すると、Githubリポジトリを入力する項目が出てくるため、デプロイするリポジトリ名を入力します。リポジトリ名を入力が"Search"ボタンを押すと、"Connect"ボタンが出てきます。Connectをクリックすることで、次の設定へ進めます。

f:id:kurobuchimeganex:20210505170952p:plain
デプロイするリポジトリ名を入力
f:id:kurobuchimeganex:20210505171118p:plain
"connect"ボタンをクリックした結果

"connect"ボタンを押下後、"Automatic deploys"項目と"Manual deploy"項目が出てきます。"Automatic deploys"項目では、Githubリポジトリを更新するたびに、自動ビルド&デプロイを実行するかを設定できます(CIの設定ができる)。"Enable Automatic Deploys"ボタンをクリックして、自動デプロイを有効化します。

f:id:kurobuchimeganex:20210505171645p:plain
自動デプロイ設定と手動デプロイ実行

Manual deployでは手動デプロイを実行できます。"Deploy Branch"ボタンをクリックすることで、GithubリポジトリをHerokuにデプロイします。

デプロイ後

自動デプロイもしくは手動デプロイが上手くいくと、"https://アプリ名.herokuapp.com"でアプリを利用できるようになります。

f:id:kurobuchimeganex:20210505175938p:plain
HerokuにAngularアプリをデプロイできた^^

感想

デプロイ方法が思いのほかシンプルで扱いやすかったです。さらに、Githubリポジトリの更新をトリガーに、Herokuで自動ビルドが行われるのが非常に便利に感じました。
Angularに関して、まだまだ知識が浅いため、引き続き学習に努めたいと思います^^

参考になったサイト

betterprogramming.pub
dev.to

GoとpostgreSQLで簡単なAPIサーバを実装する

概要

Go言語の文法を一通り学習し終えたため、アプリ開発に挑戦しました。今回は、GoのプログラムとpostgreSQLで、簡単なAPIを実装しました。処理の流れとしては、以下のようになります。

f:id:kurobuchimeganex:20210502155005p:plain
実装した処理の流れ

サンプルデータ

postgreSQLには、簡単に学生情報データ(student_name, email, tel)を作成しています。そして、Goの方には全学生の情報を取得するプログラムと、学生の名前を指定して情報を取得するプログラムの2つを用意しています。

f:id:kurobuchimeganex:20210502155931p:plain
サンプルデータ

実装

"net/http"パッケージを使用して、APIサーバーを実装しています。ただ、このパッケージだけだと"/users/ユーザー名"のようなユーザー指定のリクエストに対してうまく処理できないことが分かりました。そこで、"github.com/gorilla/mux"パッケージを使用し、リクエストパスを柔軟に処理できるようにしました。

package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"

	_ "github.com/lib/pq"
)

// 学生情報
type Student struct {
	Name  string `json:"name"`
	Email string `json:"email"`
	Tel   string `json:"tel"`
}

// エラー情報
type ErrorInfo struct {
	Message string `json:"error_message"`
}

// 学生情報を全取得
func AllStudentInfo(w http.ResponseWriter, r *http.Request) {
	db, err := sql.Open("postgres", "host=db port=5432 user=postgres password=postgres dbname=postgresdb sslmode=disable")

	// クエリの作成・実行
	rows, err := db.Query("SELECT * FROM student")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	// 実行結果をStudent型に変換
	students := []Student{}
	for rows.Next() {
		var studentname string
		var email string
		var tel string
		if err := rows.Scan(&studentname, &email, &tel); err != nil {
			log.Fatal(err)
		}
		tmpstudent := Student{studentname, email, tel}
		students = append(students, tmpstudent)
	}

	// json形式のデータに変換
	jsonstr, err := json.Marshal(students)
	if err != nil {
		log.Fatal(err)
	}

	// jsonデータを出力
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, string(jsonstr))
}

// 指定の学生情報を取得
func StudentInfo(w http.ResponseWriter, r *http.Request) {
	db, err := sql.Open("postgres", "host=db port=5432 user=postgres password=postgres dbname=postgresdb sslmode=disable")

	// URLからユーザ名を取得
	vars := mux.Vars(r)
	name := vars["name"]

	// クエリの作成・実行
	row := db.QueryRow("SELECT * FROM student WHERE student_name = $1", name)

	// 実行結果をStudent型に変換
	student := Student{}
	if err := row.Scan(&student.Name, &student.Email, &student.Tel); err != nil {
		jsonstr, err := json.Marshal(ErrorInfo{err.Error()})
		if err != nil {
			log.Fatal(err)
		}
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, string(jsonstr))
		return
	}

	// json形式のデータに変換
	jsonstr, err := json.Marshal(student)
	if err != nil {
		log.Fatal(err)
	}

	// jsonデータを出力
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, string(jsonstr))

}

func main() {
	// リスナの登録
	r := mux.NewRouter()
	r.HandleFunc("/users/{name}", StudentInfo)
	r.HandleFunc("/", AllStudentInfo)
	http.Handle("/", r)
	http.ListenAndServe(":80", nil)
}

実行結果

実行結果は以下のようになりました。"/"でアクセスした場合、全学生の情報をJSON形式で返しています。"/users/ユーザー名"でアクセスした場合、特定の学生情報のみをJSON形式で返しています。

f:id:kurobuchimeganex:20210502162557p:plain
全学生情報を取得
f:id:kurobuchimeganex:20210502162809p:plain
指定の学生情報を取得

感想

Go言語を学習し始めて間もないですが、文法がシンプルだったり、パッケージが豊富で分かり易かったため、実装しやすかったです。APIサーバーの実装方法を整理できたので、次はフロントエンドの開発に挑戦したいと思います。