Goで特定の環境向けのビルドのときだけ取り込むソースを指定する

goはビルド時の環境変数の設定で異なる環境向けのバイナリを生成できるのだが、開発環境ではWindowsLinuxを使っていて、実行環境はLinuxということがあり、Linux向けのバイナリだけsyslogを使いたくなった。 そんなときはbuild constraintという機能を使うと解決できる。

ファイルの冒頭(package宣言より前)に

//go:build linux

とかくとそのファイルはLinux向けのビルドのときにしか取り込まれなくなる。 windows向けだったらwindowsmac向けだったらdarwinなどと書く。 アーキテクチャの指定も可能。

ginのログ出力先をsyslogにする

こんなかんじ

package main

import "github.com/gin-gonic/gin"
import "log/syslog"

func main() {
        logger, err := syslog.New(
                syslog.LOG_NOTICE|syslog.LOG_USER,
                "gin-test")
        if err != nil {
                panic(err)
        }
        gin.SetMode(gin.ReleaseMode)
        gin.DisableConsoleColor()
        gin.DefaultWriter = logger
        app := gin.Default()
        grp := app.Group("/api")
        grp.GET("/", func(ctx *gin.Context) {
                ctx.JSON(200, gin.H{"message": "ok"})
        })
        app.Run("localhost:3000")
}

まぁでもsupervisorとか使うだろうからログも標準出力にだして、あとはsupervisorに任せればよいでしょう。(※と、当初書いたけれど、systemdでやればいいんじゃないのって言われたが、とりあえず記事はそのまま残しておく。)

RHEL8系の場合、

sudo dnf install epel-release

ってやって(すでにやってあれば当然不要)

sudo yum -y install supervisor

とかやるとインストールされ、設定ファイルは /etc/supervisord.conf になっているので、そのなかに設定を追記するなり、設定ファイルをよく見るとわかるように /etc/supervisord.d/*.iniが読み込まれるようになっているのでそれにマッチする個別の設定ファイルを置くなりして自分のプログラムがsupervisordに起動されるようにすればよい。

例えば自分のプログラムがapiという名前で識別されるようにしたかったら、/etc/supervisord.d/api.ini などといった名前のファイルを作って、その中に

[program:api]
command=/usr/local/bin/api
autostart=true
autorestart=true
stderr_logfile=/var/log/api.err.log
stderr_logfile=/var/log/api.out.log

などと書いてやればよいだろう。

ってsupervisorの話のほうが長くなった。

ginでAPIサーバを作る

ディレクトリをほって初期化する。 モジュール名のところはリポジトリ名にするんだよ、とする情報が多くて、正しいのだけれど、公開しないものであれば別にそのフォルダ名とかでも特に支障はない(たぶん)。

mkdir XXXX
cd XXXX
go mod init モジュール名

DBを立ち上げるdocker-compose.ymlをかく

services:
  db:
    image: postgres:latest
    container_name: postgresql
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - ./pgdata:/var/lib/postgresql/data
      - ./pginit:/docker-entrypoint-initdb.d

初期化して初期データをつっこむSQLを書く

DROP TABLE IF EXISTS Users;

CREATE TABLE Users (
    user_id SERIAL,
    username VARCHAR(128),
    account VARCHAR(64),
    password VARCHAR(64)
);

INSERT INTO Users(username, account, password) VALUES('', 'user01', 'user01');
INSERT INTO Users(username, account, password) VALUES('', 'user02', 'user02');
INSERT INTO Users(username, account, password) VALUES('', 'user03', 'user03');
INSERT INTO Users(username, account, password) VALUES('', 'user04', 'user04');
INSERT INTO Users(username, account, password) VALUES('', 'user05', 'user05');

勢いで書く。

package main

import (
    "database/sql"
    "fmt"
    "log"
    "strconv"
    "strings"
    "time"

    _ "github.com/lib/pq"

    "github.com/gin-contrib/cors"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt"
)

const SECRETKEY = "secretkey"


func main() {
    // ログにファイル名と行番号を出す設定
    log.SetFlags(log.Lshortfile)
    app := gin.Default()
    // グループの作成
    apiRouter := app.Group("/api/v0")
    // CORS設定、どこからでもアクセスOKにしてみた
    apiRouter.Use(cors.New(cors.Config{
        AllowOrigins: []string{"*"},
        AllowMethods: []string{"POST", "GET", "OPTIONS"},
        AllowHeaders: []string{"Authorization", "Content-Type"}}))
    // このグループ共通のミドル、ログインしていたらユーザ情報を取得する
    apiRouter.Use(middleware)
    // ログイン
    apiRouter.POST("/login", Login)
    // ログインしているユーザの情報の取得
    apiRouter.GET("/me", Me)
    app.Run()
}

//
// Authorizationヘッダに正しいトークンが乗っていたらコンテキストにそのユーザの情報を乗せる
//
func middleware(c *gin.Context) {
    // Authorizationヘッダからトークンを取り出す
    authorizationHeader := c.Request.Header.Get("Authorization")
    if authorizationHeader != "" {
        ary := strings.Split(authorizationHeader, " ")
        if len(ary) == 2 {
            if ary[0] == "Bearer" {
                // ここまででトークンと思われるものを取り出せたので、解析する
                // ここのロジックは公式サイト参照
                // https://pkg.go.dev/github.com/golang-jwt/jwt#example-Parse-Hmac
                token, err := jwt.Parse(ary[1], func(token *jwt.Token) (interface{}, error) {
                    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                        return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
                    }
                    return []byte(SECRETKEY), nil
                })
                if err == nil {
                    // 解析に成功したら、ユーザIDを取り出す
                    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
                        userIDStr := claims["sub"].(string)
                        userID, err := strconv.Atoi(userIDStr)
                        if err == nil {
                            // ユーザIDを使って新しいトークンを取り出す(有効期限切れ対策)
                            newToken, err := generateToken(userID)
                            if err == nil {
                                // ユーザの情報を取り出す
                                conn, err := connectDB()
                                if err == nil {
                                    defer conn.Close()
                                    var username string
                                    err := conn.QueryRow(`
                                      SELECT username FROM Users WHERE user_id = $1`,
                                        userID).Scan(&username)
                                    if err == nil {
                                        // 新しいトークンとユーザの情報をコンテキストに保存
                                        c.Set("token", newToken)
                                        c.Set("username", username)
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    c.Next()
}

//
// 投入されたログイン情報を格納するための構造体
//
type LoginInfo struct {
    Account  string `json:"account" binding:"required"`
    Password string `json:"password" binding:"required"`
}

//
// ログイン処理
//   正しいアカウントとパスワードが渡されたらトークンを生成して返す
//
func Login(c *gin.Context) {
    // 投げつけられた情報を取り出す
    var loginInfo LoginInfo
    err := c.ShouldBindJSON(&loginInfo)
    if err != nil {
        log.Println(err)
        c.JSON(400, gin.H{"message": "bad request"})
        return
    }
    // DBに接続
    conn, err := connectDB()
    if err != nil {
        log.Println(err)
        c.JSON(500, gin.H{"message": "server internal error"})
        return
    }
    defer conn.Close()
    // 渡された情報に該当するユーザを探す。
    // 本当だったらDBに生パスワードは保存しないだろうからこの前にハッシュ化などが必要
    var userID int
    rows, err := conn.Query(`
      SELECT user_id FROM Users WHERE account = $1 AND password = $2`,
        loginInfo.Account, loginInfo.Password)
    for rows.Next() {
        err := rows.Scan(&userID)
        if err != nil {
            log.Println(err)
            c.JSON(500, gin.H{"message": "server internal error"})
            return
        }
    }
    // トークン生成
    token, err := generateToken(userID)
    if err != nil {
        c.JSON(500, gin.H{"message": "server error"})
        return
    }
    c.JSON(200, gin.H{"token": token})
}

//
// ログインしているユーザの自分自身の情報取得
//
func Me(c *gin.Context) {
    // 自作ミドルウェアがコンテキストに保存した情報を取り出す
    username, exist := c.Get("username")
    if !exist {
        c.JSON(403, gin.H{"message": "need to login"})
        return
    }
    token, exist := c.Get("token")
    if !exist {
        c.JSON(403, gin.H{"message": "need to login"})
        return
    }
    c.JSON(200, gin.H{"username": username, "token": token})
}

//
// DBへの接続
//
func connectDB() (*sql.DB, error) {
    db, err := sql.Open("postgres", `
      host=localhost
      port=5432
      dbname=postgres
      user=postgres
      password=postgres
      sslmode=disable`)
    if err != nil {
        log.Println(err)
        return nil, err
    }
    return db, nil

}

//
// トークン生成
//
func generateToken(userID int) (string, error) {
    now := time.Now()
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub": fmt.Sprintf("%d", userID),
        "iat": now.Unix(),
        "exp": now.Add(60 * time.Minute).Unix(), // 一時間で無効になる
    })
    return token.SignedString([]byte(SECRETKEY))
}

TypeScriptでexpressを使ってAPIサーバを作る

初期設定

とりえあえず、TypeScriptとexpressとexpressの型定義をインストールする。

npm i express
npm i -D typescript
npm i -D @types/express

はじめの一歩

expressを用いたWebアプリでは、expressのオブジェクトを作り、そのオブジェクトにどのメソッドでどのURLに対してどんなリクエストが来たらどんなレスポンスを返すのか、をひたすら実装していく。

import express from "express";
const app = express();
const port: number = 3000;
app.get("/", (Req: express.Request, res: express.Response) => {
  res.json({ message: "ok" });
  // res.status(500).json({ message: "error"}) // エラーレスポンスはこんな感じ
});
app.listen(port, () => console.log("ok, port =", port));

パスパラメータ

パスを指定する場所で、たとえば:user_idのように、コロンに続けて変数名を指定する。例として、ユーザIDを受け取るような状況を考える。

import express from "express";
const app = express();
const port: number = 3000;
app.get("/:user_id", (req: express.Request, res: express.Response) => {
  res.json({ message: "ok", user_id: req.params.user_id });
});
app.listen(port, () => console.log("ok, port =", port));

このように、req.paramsに続けて受け取った変数名を書くことで、パスを用いて渡された変数の値を取得することができるので、例えば~/1などにアクセスすることができるようになる。

POSTされたJSONの受け取り方

const port: number = 3000;
import express from "express";
const app = express();
import bodyParser from "body-parser";
app.use(
  bodyParser.urlencoded({
    extended: true,
  })
);
app.use(bodyParser.json());
app.post("/:user_id", (req: express.Request, res: express.Response) => {
  res.json({ message: "ok", user_id: req.params.user_id, data: req.body });
});
app.listen(port, () => console.log("ok, port =", port));

ルート

ルートを使うとあるパス配下の処理をまとめて扱うことができる。またルート単位に処理を割り当てて、そのルート配下であればリクエストがくるたびに割り当てた処理を実行させることができる。 例えば、/api/v0云々というパスに対するリクエストに来たら同じ処理をさせる場合は、以下のようなかたちになる。

import express from "express";
const app = express();
const port: number = 3000;

const apiRouter = express.Router();  // ルータを作る
app.use("/api/v0", apiRouter);  // ルータをパスに割り当てる
apiRouter.use(  // このルータで使うミドルウェアを設定する。
  async (
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
  ) => {
    // 例えばログインしている場合はユーザ情報を取得するとかその手のことをやる。
    next(); // これを呼ぶと次の関数が実行される、呼ばないと実行されない。
  }
);
apiRouter.get("/", (req: express.Request, res: express.Response) => {
  res.json({ message: "ok" });
});
app.listen(port, () => console.log("ok, port =", port));

なお、このように割り当てる処理のことをミドルウェアと呼ぶ。

JWT

APIサーバでの認証方法のひとつとしてトークンを用いるという方式があり、トークンの仕様のひとつとしてIETFによりRFC7519で定められたJWTがある。

https://datatracker.ietf.org/doc/html/rfc7519

これを使えばどうにかできるだろう。

https://github.com/auth0/node-jsonwebtoken

CORS

APIサーバなので、別のサーバが配信したHTMLからたたかれることを想定しなくてはならないこともあるだろう。

import express from "express";
import cors from "cors";
const app = express();
app.use(cors());

入力値のバリデーション

express-validatorを使えばよさそう。

https://express-validator.github.io/docs/

しめのあいさつ

なにか書き足したほうがよさそうなことを思いついたら順次追記していきます。

TypeScriptの基本

TypeScriptとは

JavaScriptの世界に静的型付けを持ち込んだイメージ。JavaScriptのゆるふわ感を緩和してくれる。TypeScriptでかいたソースコードコンパイラ(トランスパイラ)で変換するとJavaScriptのコードが生成されるので、そのソースコードをnode.jsなどで実行する。なお、Denoを使うとTypeScriptのソースを直接実行することができる。

以下、node.jsで触ってみる。

準備

適当なディレクトリを作ってからそのディレクトリ内に移動して、初期化。

npm init -y

-yをつけないとインタラクティブにいろいろ聞かれるが、-yをつけるとすべてデフォルト値で一気に設定ファイル(package.json)を生成してくれる。

TypeScriptのインストール

このプロジェクトにTypeScriptをインストールする。

npmでのパッケージのインストールには、

npm install パッケージ名

と入力する。installはiと一文字だけ入力することでも同じ結果になる。つまりこんな感じ。

npm i パッケージ名

また開発専用のパッケージをインストールする場合は、オプションで--save-devあるいは-Dオプションを付与する。-Dのほうを使うなら、こんな感じ。

npm i -D パッケージ名

TypeScriptは開発中しか使わないので、このオプションを付与してインストールする。

npm i -D typescript

これにより、./node_modules/.bin/tscという実行ファイルがインストールされる。たぶんTypeScript Compilerの略。

TypeScriptの設定ファイルを作る

TypeScriptの設定値はやまほどあるが、以下のコマンドで丁寧な説明がコメントで記載されている設定ファイルが生成されるので、これを編集すればよい。

./node_modules/.bin/tsc --init

コマンドを実行したディレクトリに設定ファイルであるtsconfig.jsonが生成されているはず。

例えば、こんな行がある。

    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */

コメントに、出力先のフォルダを指定するんだよ的なことが書いてあるので、コメントアウトを外してからJavaScriptのコードを出力するフォルダ名を書く。

    // "outDir": "./dist",

型定義の書き方

こんな感じ。変数名のうしろにコロンを書いて、さらにその後ろに型の名前を書く。

let n: number;
let s: string;
let a1: number[]; // 配列
let t: [number, string, string]; // これはタプル

オブジェクトの場合、オブジェクトリテラルを使う。

let c: {d: number} ={
  d: 1234;
}

オブジェクトリテラルにおいて、読み取り専用の変数にはreadonly、あってもなくてもよい変数には?を付与する。

let e: {
  readonly f: string;
  g?: number;
}

エイリアスを使うとオブジェクトを生成する都度オブジェクトリテラルを書く必要がなくなり簡潔なコードにすることができる。

type A = {
  h: string;
  i: number;
}

関数での型指定

関数の引数と戻り値ならこう

function sonomamakaesu(a : number) : number {
  return a;
}

型定義同様、?を使って省略できることを示すことができる。

function f(a: number, b?: number) {
  console.log(a);
  console.log(b); // 省略された場合はundefinedと出力される
}

値を書くことで省略時のデフォルト値を指定することができる。

function f(s = "default parameter") {
  console.log(s);
}

レストパラメータを使えば可変長引数も実現できる。

function f(...nums: number[]) {
  nums.map((x: number) => {
    console.log(x);
  });
}

関数自身がどんな型なのかを指定することもできる。

function f0(a: number): number {
  return a * a;
}

function f1(f: (x: number) => number): void {  // ここの引数に注目
  console.log(f(10));
}

f1(f0);

コーディング時に型を特定できない場合はジェネリック型を使うことができる。 始まりのカッコの直前に<と>でくくって型を指し示す文字列を書いておいて、関数定義内ではその文字列を型を記すべき箇所に書いていく。

function f<T>(x: T) {
  console.log(x);
}

f(1); // 省略することもできるし、
f<string>("asdf");  // 明示することもできる。

はじめてのJSX

ReactではHTMLを組み立てる上でJSXというJavaScriptの拡張構文を用いる。

関数コンポーネントを使う場合、そのコンポーネントを表す関数でJSXをreturnする。

例えばこんな感じ

export const Abc = () => {
    return <p>abac</p>;
}

JSXのなかでは{と}でくくることでJavaScriptの式を書くことができる。

条件分岐は3項演算子を使う。

import { useState } from "react";
export const Bbs = () => {
  const [flag] = useState<boolean>(true);

  return <div>{flag ? <></> : <></>}</div>;
};

ループにはmapを使う。returnを忘れないように。

return (<ul> {
  ary.map((item: string, key: number)=>{
     return <li key={key}>{ item }</li>
   });}</ul>);