Taka blog

プログラミングとか

【Go言語】ginを用いたWebアプリケーション入門

htmlやjsonを返すGo言語のWebアプリの作り方をご紹介したいと思います。

WEBアプリのフレームワークは複数ありますが、代表的なものはginなどです。

フレームワーク スター数 リンク
gin 55.9k https://github.com/gin-gonic/gin
beego 27.8k https://github.com/beego/beego
gokit 22.4k https://github.com/go-kit/kit
eco 21.7k https://github.com/labstack/echo

今回はstarが多いginを利用します。

目次

  1. 最小の構成
  2. エンドポイントの増やし方
  3. 各種パラメータの利用
  4. レスポンスの設定
  5. 処理で共通の値を使う
  6. パッケージ構成を変更する

最小の構成

main.goとgo.modだけでサーバーを起動することができます。

内容 コマンド
go.mod作成 go mod init 任意のプロジェクト名
ginインストール go get github.com/gin-gonic/gin

起動コマンドはいつも通りのgo run main.goもしくはgo run .です。

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func main() {
    // Engineインスタンスを生成する
    r := gin.Default()

    // エンドポイントと処理を結び付ける
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "pong"})
    })

    // 起動時のポート番号を指定する
    r.Run(":8080")
}

動作確認

このファイルを追加し起動させている間にhttp://localhost:8080/pingにアクセスすると、{"message": "pong"}が返ってきます。
ブラウザのバーに上記のURLをコピペすれば、動作を確認できます。

エンドポイントの増やし方

先程はGETメソッドのpingパスの処理のみを定義しました。
この章では、エンドポイントの増やし方をご紹介します。

メソッドの種類

r.POST("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"message": "pong"})
})

どのメソッドであってもr.メソッド名で指定をします。

  • GET
  • POST
  • PUT
  • DELETE

パスの指定

/pingのようにスラッシュ始まりで記述します。

パスパラメータを利用する場合は、/ping/:id/:nameのように:パラメータ名形式で指定します。
この例の様に複数のパスパラメータを利用することもできます。
/ping/30/taroにアクセスしたとすると、idに"30"、nameに"taro"が入ります。

処理で各種パラメータを利用する

以下の3つのパラメータを取得する方法を説明いたします。

クエリパラメータ パスパラメータ フォームパラメータ

加えて、リクエスト情報全体の取得方法も書いています。

クエリパラメータ

r.GET("/ping", func(c *gin.Context) {
    id := c.Query("id")
    c.JSON(http.StatusOK, gin.H{"message": id})
})

パスが/ping?id=30&name=taroだとすると「?」以降がクエリパラメータです。
このパスにリクエストが来た場合、r.GETの第2引数に渡した関数が実行されます。
クエリパラメータの名前指定はパスパラメータとは違い、/pingのパス部分に含める必要はありません。

取得はQueryメソッドで行います。
このコード例で/ping?id=30にリクエストが来ると、idにstring型の"30"が入ります。

パスパラメータ

r.GET("/ping/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(http.StatusOK, gin.H{"message": id})
})

取得はParamメソッドで行います。
パスパラメータを利用する場合は、/:パラメータ名をエンドポイントのパスに含める必要があります。

フォームパラメータ

r.POST("/ping", func(c *gin.Context) {
    id := c.PostForm("id")
    c.JSON(http.StatusOK, gin.H{"message": id})
})

取得はPostFormメソッドで行います。
これはあまり使わないと思います。

リクエスト情報を取得する

r.POST("/ping", func(c *gin.Context) {
    request := c.Request()
    c.JSON(http.StatusOK, gin.H{"message": request.URL.String()})
})

gin.Context型の変数から、 http.Request型の変数を取得できます。
RequestからボディやURLなど全ての情報を持ってくることができます。
フォームパラメータを複数取得する場合などは、こちらを使いましょう。

request.Bodyとすればボディを取得できますが、これはstring型ではなくio.ReadCloser型なので注意が必要です。
string型にするには以下のように書きます。

// body は[]byte型
body, _ := io.Readall(c.Request().Body)
s := string(body)

レスポンスの設定

レスポンスを定義しているのは以下の部分です。

c.JSON(http.StatusOK, gin.H{"message": request.URL.String()})
})

c.メソッド名で記述していて、他にもメソッドがいくつかあります。
主に使いそうなのは下の3つです。

JSON String HTML

Stringはc.String(http.StatusOK, "hoge")のように書きます。

HTMLを利用する場合には、htmlファイルを作成しておき、事前にHTMLを読み込む関数を呼ぶ必要があります。

// templatesディレクトリ配下のhtmlを読み込む
r.LoadHTMLGlob("templates/*")
 
r.GET("/ping", func(c *gin.Context) {
    c.HTML(http.StatusOK, "ping.html", gin.H{
        "message": "pong",
    })
})

c.HTMLメソッドの第2引数では、htmlファイル名を渡します。読み込んだディレクトリパスに指定したファイルが存在しなければ、レスポンスでHTMLを返せません。

第3引数gin.H{~}では、htmlに埋め込む値を記述しています。なので埋め込む値が無ければ、gin.H{}内の中身は無くても大丈夫です。

各処理に共通の値を渡す

type Config struct{
    Message string
    // 色々入れる
}

func main() {
    r := gin.Default()

    // 実際はconst定数や環境変数に設定した値を入れる
    config := Config{}

    // 関数の外の変数configを、変数内で利用できる
    r.GET("/ping", func(c *gin.Context) {
        m := config.Message

        c.JSON(http.StatusOK, gin.H{"message": m})
    })

    router.Run(":8080")
}

パッケージ構成を変更する

業務で扱う場合は、MVCやクリーンアーキテクチャを用いることになると思います。

[Golang]クリーンアーキテクチャを用いたWebアプリ入門・パッケージ構成

GolangでWebアプリを作るとき、パッケージ構成をどうすればよいか悩むことありますよね。今回はパッケージ構成の1つの例として、クリーンアーキテクチャ(Clean Architecture)の構成をご紹介しようと思います。クリーンアーキテクチャを採用しない場合も、パッケージ構成のヒントになるのではと思います。

Webフレームワークにginやechoなど使う場合にも適用できます。

クリーンアーキテクチャ

f:id:masquerade0:20211229114201j:plain

クリーンアーキテクチャを用いた構成で気をつけることは、以下の2つです。

  1. 役割毎にパッケージを4層に分ける
  2. 依存の方向は外から内のみ




パッケージ構成

パッケージ名は円の外から順に、infrastructure、interfaces、usecase、domainの4つになります。

domain
 ├ product_repository.go // DB操作interfaceを定義
 └ product.go // モデルの種類毎にファイルを追加
infrastructure
 ├ router.go // リクエストにhandlerを割り当て
 └ product_repository.go // DB操作interfaceを実装
interfaces
 └ getproduct_handler.go // レスポンスを定義
usecase
 └ getproduct_usecase.go // 業務ロジックを記述
main.go


コード例

getproduct_handler.goの例です。

package interfaces

import{
// 省略
}

type GetProductHandler interface{
	Handle(c gin.Context) error
}

type getProductHandler struct {
	u usecase.GetProductUsecase
}

func  NewGetProductHandler(r domain.ProductRepository) GetProductHandler {
	return &getProductHandler{
		u : usecase.NewGetProductUsecase(r)
	}
}

func (h *getProductHandler) Handle(c gin.Context) error {
	id := c.Param("id")

	// handlerのフィールドにあるusecaseを利用する
	product := h.u.GetProduct(id)

	return c.JSON(http.StatusOK, product)
}

注意点

  • interfaceで定義したメソッドを、構造体が実装していること
  • メソッドのレシーバーがポインタなので、NewGetProductHandler関数はポインタを返す。Handleメソッドを実装しているのはgetProductHandlerのポインタ型であり、getProductHandlerの値型は実装していないため。戻り値の型をGetProductHandlerにしているので、値型だとコンパイルエラーになる。
  • NewGetProductHandler関数はinfrastructureパッケージから呼ばれる想定なので、既にinfrastructure→interfacesの参照方向がある。更に、infrastructure/product_repository.goで定義したNewProductRepository関数をこのファイルから呼んでしまうと、interfaces→infrastructureの参照方向ができて、循環参照(コンパイルエラー)になってしまう。
    なので、infrastructure/router.goでNewProductRepository関数を呼び、domain.ProductRepository型の戻り値をNewGetProductHandler関数の引数に渡す。




インターフェースを利用する

※分かりづらいですがパッケージの名称として「interfaces」、Goの仕様のインターフェースは区別してインターフェースと表記します。Goのコードでは「interface」と書くのですが、見分け辛いので。

クリーンアーキテクチャは依存を外から内に制限しています。また、Golangの仕様としても循環参照が禁止されています。usecaseからインターフェースなしでrepositoryを利用しようとすると、interfaces→usecaseとusecase→interfacesの参照が発生します。この場合、後者の依存の方向が内から外になってしまっているのに加え、参照が循環していることからますGoのコンパイルエラーとなります。このusecase→interfacesの参照を無くす(依存の方向を逆転させる)ために、インターフェースを用います。

インターフェースの使い方については、過去記事に書きました。

takaittakait.hatenablog.com




悩みどころ

Repositoryインターフェース

クリーンアーキテクチャで調べると、記事によって構成が違うことがあります。
Repositoryのインターフェースは今回はdomainに置いてますが、usecaseに置いている人もいます。

domainにmodelの定義とその振る舞いを書いて、usecaseは業務ロジックという分け方が綺麗な気はします。

[Git]複数のコミットを1つに纏める

無駄なコミットが残らないように、どちらかの方法でコミットを纏めます。

rebaseを用いる方法

コミット履歴を確認する

git log

纏めたいコミットより前のコミットのハッシュ値をコピーします。2つのコミットを纏めたい場合は、上から3つ目のコミットのハッシュ値です。

リベースする

git rebase -i コミットのハッシュ値

コマンドを打つと、対象より新しいコミットが、古い順に表示されます。2つ目以降の削除したいコミットの「pick」を「s」に書き換えると、スカッシュ後に削除されます。

エディタはナノエディタを想定しています。環境によって変わるのかもしれないですけれど、VScode&Git Bashではナノエディタでした。

sに書き換えたら、「ctrl + x」→「y」→「Enter」と入力すると保存されます。その後コミットメッセージの編集画面になります。表示したくないコミットのメッセージを消すことができます。保存方法は先程と同じ流れです。

プッシュする

rebase後は、オプション「-f」を付けてpushする必要があります。


resetを用いる方法

reset --softする

直近のcommitが削除され、その変更内容はcommit前の状態になります。

git reset --soft HEAD^

新たにcommitする

[vscode]ターミナルでGit Bashを利用する方法

いつの間にかGit Bashvscodeから使えなくなったことがあってムカついたので、設定方法を残しておきます。
vscodeは2021/09/13時点で最新のversionに合わせていて、settings.jsonでもうるさい警告は出ていません。ご紹介するのが恐らく必要最低限の設定だと思ってます。

vscodeのshell一覧にGit Bashを入れる

「設定」から「terminal」で検索すると、「settings.jsonで編集」からsettings.jsonを開けます。(他の検索ワード・方法でもたどり着けます。あくまで一例)

f:id:masquerade0:20210913224621j:plain
settings.json

terminal.integrated.profiles.windowsの項目に、使いたいshellを設定します。警告が出ていたり、「terminal.integrated.shell.windows」のようなshell関連っぽい他の項目は消して大丈夫です。

1つ注意しないといけないのが、shellの名前です。(デフォで入ってるCommand Promptはスペースが入ってる癖に、)「Git Bash」のようにスペースを入れてしまうと、認識されないケースがあるようです。ちゃんと認識されているかは、次の章で確認できます。

Git Bashを既定のshellに設定する

f:id:masquerade0:20210913225735j:plain
設定

前章でお見せした画像のようにsettings.jsonで直接定義するか、「設定」の「Terminal › Integrated › Default Profile: Windows」から指定します。設定からだと内部のshell一覧が表示されてそこから選ぶ形式なので、前章のsettings.jsonで定義したshellがきちんと反映されているか確認できます。

[AndroidStudio4]ポップアップ(ダイアログ)を表示する方法

以下の画像のように画面上にポップアップを表示するのは、PopupWindowという機能です。今回の記事では、PopupWindowを表示し操作に処理を割り当てる方法をご紹介します

f:id:masquerade0:20210829212816j:plain
popup

PopupWindowのレイアウトを定義する

res/drawable/popup_background.xml

ポップアップの枠内、枠のレイアウト設定をします。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@android:color/white" />
    <stroke
        android:width="2dp"
        android:color="@color/black" />
</shape>

res/layout/popup.xml

ポップアップ内に表紙するViewを定義します。background属性で上のレイアウトファイルを指定します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="160dp"
    android:layout_gravity="bottom"
    android:background="@drawable/popup_background"
    android:orientation="vertical"
    android:padding="8dp"
    android:layout_margin="16dp">
    <Button
        android:id="@+id/add_note"
        android:layout_width="wrap_content"
        android:layout_height="48dp"
        android:layout_gravity="center_horizontal"
        android:text="画面遷移"  />
</LinearLayout>

PopupWindowを表示させる

MainActivity

事前に、画面レイアウトにImageButtonを定義します。

<LinearLayout
    android:layout_alignParentBottom="true"
    android:layout_height="48dp"
    android:layout_width="fill_parent"
    android:orientation="vertical"
    android:background="@color/gray"
    android:layout_marginTop="4dp"
    android:layout_marginBottom="0dp"
    android:id="@+id/main"
    >
    <ImageButton
        android:id="@+id/add"
        android:src="@drawable/ic_baseline_add_circle_24"
        android:layout_gravity="center"
        android:scaleType="centerInside"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:background="#00000000"
        android:onClick="displayPopup"
        />
</LinearLayout>

onResumeメソッド内では、PopupWindowの表示サイズなどの設定を記述します。そしてImageButtonに対応するメソッド(例ではdisplayPopup)に、PopupWindowを表示するメソッドを呼び出します。このメソッド名は、xmlファイルのImageButtonのonClick属性と一致させないといけません。

PopupWindow popupWindow;
View popupView;

@Override
protected void onResume() {
    // 省略

    popupWindow = new PopupWindow((View)findViewById(R.id.add));

    // 使用するレイアウトを指定する
    popupView = getLayoutInflater().inflate(R.layout.popup, null);
    popupWindow.setContentView(popupView);

    // popup表示時に、他のButtonなどを無効にする
    popupWindow.setOutsideTouchable(true);
    popupWindow.setFocusable(true);

    // 表示サイズを設定
    popupWindow.setWidth(findViewById(R.id.main).getWidth()/10*9);
    popupWindow.setHeight(480);
}

// PopupWindow内のボタン押下時
public void displayPopup(View notused) {
    popupWindow.showAtLocation(popupView, Gravity.CENTER_HORIZONTAL, 0, 500);
}

PopupWindowに処理を定義する

onResumeメソッド内に、以下のコードを追加します。

    // PopupWindow内のボタンにクリック時の処理を定義する
    popupView.findViewById(R.id.add_note).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            popupWindow.dismiss();

            Intent intent = new Intent(MainActivity.this, SubActivity.class);
            startActivity(intent);
        }
    });


PopupWindowを非表示にする

上のコード例にあるdismissメソッドを呼ぶと、ポップアップが消えます。

[AndroidStudilo4]ListViewの使い方

リスト形式で表示するには、ListViewかRecyclerViewを用います。RecyclerViewはListviewをより柔軟にしたものなので、今回はListViewをご紹介します。

ListViewのレイアウト

リスト全体のレイアウトは、ListView内で定義できます。各アイテムのレイアウトは、別途定義するかデフォルトで用意されているものを使うことになります。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:background="@color/gray"
    >

    <ListView
        android:id="@+id/itemList"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:divider="@color/green"
        android:dividerHeight="4dp"
        >
    </ListView>

</LinearLayout>

LinearLayoutは、子要素を1列に並べるレイアウトです。これも各Layoutの中でシンプルな方です。

ListViewのdividerはアイテム間の色を、dividerHeightはアイテム間の高さを定義します。

ListViewにアイテムを表示する

MainActivity

@Override
protected void onResume() {
	super.onResume();

	// ListViewに表示したいデータ
	List<String> list = new ArrayList<String>();
	list.add("sample1");

	// データとレイアウト内のListviewを紐付け
	ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list);
	ListView listView = (ListView)findViewById(R.id.itemList);
	listView.setAdapter(adapter);
}

ListViewにデータを設定するためには。Adapterが必要です。Adapterにも種類があるのですが、ArrayAdapterの場合はList型のデータを渡します。

ArrayAdapterの生成時、第2引数はリスト表示されるアイテムのレイアウト定義です。例ではAndroidStudioにデフォで用意されているレイアウトを指定しています。

流れは、List型データを作成 → データを引数にしてAdapter生成 → ListViewを画面レイアウトから生成 → ListViewにAdapterを追加 です。

Listviewにクリック時の操作を追加する

MainActivity

@Override
protected void onResume() {
	super.onResume();

	// 省略
	
	// クリック時の処理
	listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
		@Override
		public void onItemClick(AdapterView<?> parent, View view, int positon, long id) {
			// 例として、画面遷移処理を定義
			Intent intent = new Intent(MainActivity.this, SubActivity.class);
			startActivity(intent);
		}
	});
}

onItemClickの第3引数であるpositionには、クリックしたアイテムのindexが入ります。これを使って、アイテムの持つデータを更新したり、リストからアイテムを削除したりできます。

ListViewに長押し時の処理を追加する

contextMenuとsetOnItemLongClickListenerの2通りの方法があると思いますが、contextMenuの方を使います。

長押し時にcontextで表示されるボタンと、処理を定義します。

res/menu/context.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/context_del"
        android:title="Delete" />

</menu>

MainActivity

@Override
protected void onResume() {
	super.onResume();

	// 省略

	// listViewにcontextを設定
	registerForContextMenu(listView); 
}

// 利用するcontextを指定する
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo info) {
	super.onCreateContextMenu(menu, view, info);
	// 上のxmlを指定する
	getMenuInflater().inflate(R.menu.context, menu);
}

// contextのボタン押下時、対応する処理を実行する
@Override
public boolean onContextItemSelected(MenuItem item) {
	// 長押ししたアイテムのindex(position)を取得する
	AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
	int position= info.position;
	
	switch(item.getItemId()) {
		// xmlで定義したボタンに処理を割り当てる
		case R.id.context_del:
			Toast.makeText(this, "test del", Toast.LENGTH_LONG).show();
			break;
	}
	return false;
}

サンプルの処理では長押ししたアイテムの位置を利用はしていませんが、取得処理を載せています。

ListViewから要素を追加する/削除する

List型データの要素を追加または削除し、それをArrayAdapter経由でListViewに反映させる、という流れになります。

まずは、onContextItemSelectedからList型データとArrayAdapterを参照できるように、クラス変数で定義するようにコードを変更します。

public class MainActivity extends Activity {
	ArrayAdapter adapter;
	List<String> list;

前章のonContextItemSelectedを、以下のように書き換えます。

// contextのボタン押下時、対応する処理を実行する
@Override
public boolean onContextItemSelected(MenuItem item) {
	// 長押ししたアイテムのindex(position)を取得する
	AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
	int position= info.position;

	switch(item.getItemId()) {
		// xmlで定義したボタンに処理を割り当てる
		case R.id.context_del:
			list.remove(position);
			list.add("sample2");
			adapter.notifyDataSetChanged();
			break;
	}
	return false;
}

[AndroidStudio4]画面を遷移させる方法、間でデータを受け渡しする方法

画面遷移には大別して2つの方法があります。今回は「Activityの切り替え」をご紹介します。ちなみに、もう1つは「Fragmentの生成」です。

新たなActivityに遷移する

起動して最初の画面について、内部処理はMainActivityに定義していました。画面とActivityは1対1で対応しています。画面を切り替えるには、現在のActivityから別のActivityを呼び出す必要があります。

MainActivity

public class MainActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.titlebar, menu);
		return true;
	}

	@Override
	public boolean onMenuItemSelected(int featureId, MenuItem item) {
		switch(item.getItemId()) {
			case R.id.button_add:
				// この2行
				Intent intent = new Intent(MainActivity.this, SubActivity.class);
				startActivity(intent);
		}
		return super.onMenuItemSelected(featureId, item);
	}
}

予め遷移先のActivityファイル(例ではSubActivity)を作成します。中身はMainActivityのデフォルトと同様(onCreateメソッドのみ実装した状態)で問題ありません。画面に対応するレイアウトも、MainActivityと同じファイルを使用しても問題ないです。

Intent生成の第1引数には遷移元のActivity、第2引数には遷移先のActivityを指定します。これらを同じActivityにして、同じActivity定義の別画面に飛ぶこともできます。

startActivityメソッドを呼ぶと、画面が切り替わります。

例の2行だけだとIntent生成をstartActivityの引数に入れて1行にできますが、データ渡しでintentに対してメソッドを呼ぶことになるので分けています。

遷移先にデータを渡す

MainActivity

Intent intent = new Intent(MainActivity.this, SubActivity.class);
intent.putExtra("test", "あ")
startActivity(intent);

遷移先にデータを渡す為に、渡す前にputExtraメソッドを使用します。第1引数にはデータの変数名、第2引数に渡すデータを指定します。

SubActivity

public class SubActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		setContentView(R.layout.activity_main);
	}

	String test;
	@Override
	protected void onResume() {
		super.onResume();

		Intent intent = getIntent();
		test = intent.getStringExtra("test");
	}
}

遷移先のActivityでは、データの変数名を指定し受け取る必要があります。getIntetで受け渡されたIntetを取得し、getStringExtraでデータを取得しています。

データ取得処理を実装しているonResumeメソッドは、画面の表示時に実行されるメソッドです。「画面の表示時」には、画面の作成時と、戻るアクションによる再表示時が含まれます。onCreateメソッドは画面の作成時に呼び出されるメソッドです。

遷移先にデータを渡す&遷移元に戻る際にデータを渡す

MainActivity

	@Override
	public boolean onMenuItemSelected(int featureId, MenuItem item) {
		switch(item.getItemId()) {
			case R.id.button_add:
				Intent intent = new Intent(MainActivity.this, SubActivity.class);
				intent.putExtra("test", "あ")
				startActivityForResult(intent, 1000);
		}
		return super.onMenuItemSelected(featureId, item);
	}

	Intent back;
	String test;
	protected void onActivityResult( int requestCode, int resultCode, Intent intent) {
		super.onActivityResult(requestCode, resultCode, intent);

		if(resultCode == RESULT_OK && requestCode == 1000 && intent != null) {
			back = intent;
			test = intent.getStringExtra("test");
		}
	}

まず値を遷移先から受け取れるようにするためには、startActivityの代わりにstartActivityForResultを使用しなければなりません。

第2引数はrequestCodeという値で、この値をonActivityResultのrequestCodeと合わせることで、遷移と戻ったときの処理が紐づけられます。

onActivityResultが、遷移先から戻ったときにIntentを受け取るメソッドです。resultCodeは、遷移先で同じ値を指定してIntentをセットする必要があります。

SubActivity

	@Override
	protected void onPause() {
		super.onPause();

		Intent intent = new Intent();
		intent.putExtra("test", "い");
		setResult(RESULT_OK, intent);
	}

戻るアクションや別アプリに移動した際に呼ばれるのが、onPauseメソッドです。

遷移先では、戻るときにIntentをSetするsetResultメソッドを呼ぶ必要があります。setResultの第1引数に、前述のresultCodeを指定します。

[golang]sliceの操作方法一覧

sliceの型はstringとします。

宣言

slice := make([]string, 0, 100)

第2引数に初期要素数、第3引数に容量を指定します。

初期要素数を1以上にすると、要素にはゼロ値が入ります。

宣言時に容量を確保するのは、そうしないと要素を追加して容量オーバーしたときに新しくメモリを確保する無駄が発生してしまうからです。




一部を取り出す

slice := []int{4, 2, 9, 7, 5, 1}
a := slice[2:4] // {9, 7}
b := slice[:1] // {4}
c := slice[4:] // {7, 5, 1}

slice[a:b]はslice[a]からslice[b-1]までをsliceにしたものを指します。slice[b]は含まれないので注意してください。

数字は両者とも省略することができます。1つ目の数字を省略した場合は0が、2つ目の数字を省略した場合はlen(slice)が入る扱いとなります。つまり前者を省略すれば先頭から、後者を省略すれば末尾までの要素が対象です。




要素を追加する

slice = append(slice, "a")
slice = append(slice, []string{"b", "c"}...)

第1引数には元のスライスを指定します。

スライスに1つの要素を追加する場合は、第2引数にスライスの基の型の変数を渡します。複数の要素を追加したい場合は、第2引数にスライスを記述し、その後ろに...を付けます。




スライスをコピーする

i := copy(slice[:3], slice[10:13])

copy()

  • 第1引数のスライスの要素に第2引数のスライスの要素を、先頭から順にコピペ
  • どちらかのスライスが全てコピーorペーストされるとそこで終了
  • コピペに成功した数がint型の戻り値




要素を削除する(並び順を保ちたい場合)

// 削除したいindex
n := 10

// 第2引数の方が少ないので、i は len(slice) - n - 1 と等しい
i := copy(slice[n:], slice[n + 1:])
// copy後、末尾の要素を消す
slice = slice[:len(slice) - 1]

// i + n は len(slice) - 1 と等しいので、上の2行を1文に纏められる
slice = slice[:n + copy(slice[n:], slice[n + 1:])]

// 連続する3つの要素を削除する場合
slice = slice[:n+copy(slice[n:], slice[n+3:])]

// 性能は劣るが分かり易い方法(要素数:1)
slice = append(slice[:n], slice[n+1:]...)

appendを利用すると、メモリアロケーションが発生する可能性があります。


削除されるスライスの要素がポインタ型や、ポインタ型のフィールドを含む構造体である場合、メモリリークが発生します。

これを防ぐには下の例のように、ゼロ値を代入します。

i := copy(slice[n:], slice[n + 1:])
slice[n + i] = nil // 構造体なら、User{}などゼロ値を代入
slice = slice[:n + i]


要素を削除する

slice[n] = slice[len(slice)-1]
slice = slice[:len(slice)-1]

消したい要素を末尾の要素で上書きし、末尾の要素を消しています。




全要素を削除する

slice = slice[0:0]

この操作でsliceは、要素数が0、容量はそのまま、のスライスになります。

[AndroidStudio4]タイトルバーにボタンを追加し、ボタンにアクションを設定する方法

デフォで表示される画面上部のバーは、タイトルバー、アプリバー、アクションバーなど名称が統一されていない感がありますが、このブログでは「タイトルバー」でいこうと思います。

タイトルバーを表示する

アプリの作り方の記事でご紹介しましたが、theme.xmlを編集してタイトルバーを表示する/非表示にすることができます。

タイトルバーにボタンを追加する

f:id:masquerade0:20210817223647j:plain
bar(button押下前)
f:id:masquerade0:20210817223717j:plain
bar(button押下時)

res/menuフォルダ配下にxmlファイルを作成します。android:titleの文字またはandroid:iconの画像がタイトルバーに表示されるようになります。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:id="@+id/button_add"
        android:orderInCategory="1"
        android:title="add"
        android:icon="@android:drawable/ic_menu_add"/>
</menu>

次にMainActivityで上記のxmlを適用させるための設定をします。

public class MainActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.titlebar, menu);
		return true;
	}
}

ここまでコーディングできたら、エミュレータ起動で動作確認してみてください。

タイトルバーのボタンにアクションを設定する

画面の操作に対して処理を割り当てる場合は、基本は○○Activity.javaに実装することになります。今回はMainActivityに以下の例をコピペしてください。

public boolean onMenuItemSelected(int featureId, MenuItem item) {
	switch(item.getItemId()) {
		case R.id.button_add:
			// button押下時の処理
			Toast.makeText(this, "selected", Toast.LENGTH_LONG).show();
			break;
		default:
			break;
	}
	return super.onMenuItemSelected(featureId, item);
}

タイトルバーのボタン押下時の処理は全てこのonMenuItemSelectedメソッドに追加していきます。caseにはタイトルバーのxml内で指定したidを選択します。こうすることで、xmlボタンと対応する処理を記述できます。

Toast.makeTextは、画面下部にトーストを表示するメソッドです。第2引数に表示したい文字列を指定します。

MainActivityですが、エラーが出る場合はextendsのAppCompatActivityをActivityに変更してください。

画面操作と処理 Androidアプリ開発時の進め方

AndroidStudioでアプリを開発していて、画面操作と処理の2つに整理すると、分かりやすいなと私は感じました。例えば今回の記事ではアプリバーのボタンを押下することが「画面操作」に当たります。これに対して、別画面への遷移や、ポップアップの表示、トーストの表示などが「処理」に該当します。

新しい機能を実装するときはそれを「画面操作」と「処理」に分類し、どちらかから順にコーディングしてエミュレータで確認しながら進めるのが良さそうです。

例として、あるボタンを長押ししたときに画面遷移させたいとします。そのときはまずボタン長押しという「画面操作」に対して、一旦トースト表示という簡単な「処理」を割り当てて動作確認する、という流れです。

まだまだ分からない部分は多いので、進め方について感じたことがあれば順次まとめたいなと思っています。

[golang(Go言語)]json入門

APIサーバーでは、jsonを用いてデータのやり取りをすることが多いと思います。今回はjsonの基本的な使い方について、ご紹介します。

構造体とjson

リクエストとして受け取ったjsonを取り扱うために、golangではjsonを構造体に変換します。

ここでは、json変換の為の構造体定義を紹介します。main.goは下のコードのまま変えず、構造体の記述の違いによって出力される値(構造体を変換したjson)がどう変化するか、見ていきます。

package main

import (
	"encoding/json"
	"fmt"
	"json/domain/model"
)

func main() {
	u := model.User{
		Name: "james",
	}

	s, _ := json.Marshal(&u)

	fmt.Print(string(s))
}

基本

package model

type User struct {
	Name string
	Age  int
}

まず構造体は、このままでもjsonに変換することは可能です。この構造体をjsonに変換した場合、json内でのフィールド名は構造体のフィールド名と同じになります。

$ go run main.go
{"Name":"james","Age":0}

jsonタグでフィールド名を指定する

type User struct {
	Name string `json:"name"`
	Age  int    `json:""`
}

jsonタグを用いて、自由にjsonフィールド名を指定できます。タグを付けつつフィールド名を指定しないという無意味なことも可能で、フィールド名はそのままです。

$ go run main.go
{"name":"james","Age":0}

omitemptyか-でフィールドを除外する

type User struct {
	Name string `json:"-"`
	Age  int    `json:"age,omitempty"`
}

jsonフィールド名を-にすると、フィールドの値がどうであろうと変換時に無視されます。

,omitemptyを付けると、フィールドの値がゼロ値の場合そのフィールドが構造体から除外されます。フィールド名を指定せず",omitempty"だけでも可です。

$ go run main.go
{}

Marhal 構造体→json

	u := model.User{
		Name: "james",
	}

	s, _ := json.Marshal(&u)

	fmt.Print(string(s))

構造体のポインタが引数、[]byteとerrorが戻り値です。なおMarshlには1つだけ注意事項があるので記載します。

	var u *model.User
	s, _ := json.Marshal(u)

このように記述してしまうと、コンパイルエラーにはなりませんが、panicが発生します。原因は、Marshlの引数にnilを渡してしまっているからです。ポインタ型をvarで宣言する際は気をつけてください。

Unmarshal json→構造体

	u := model.User{
		Name: "james",
	}

	s, _ := json.Marshal(&u)

	fmt.Println(string(s))

	n := model.User{}
	_ = json.Unmarshal(s, &n)

	fmt.Printf("%+v", n)

Unmarshalでも、構造体のポインタを引数にしています。

出力結果は、フィールドが何も指定されていないのでゼロ値になります。

$ go run main.go
{}
{Name: Age:0}