sagantaf

機械学習やITインフラ、特にコンテナ関連の記事を書いているブログ。

redisとetcdをコンテナとして起動してPythonやGUIから使ってみる

はじめに

KVS(Key Value Store)として利用できるRedisとetcdのうち、どちらがより簡単に使い始められるか、シンプルな導入として適しているかを検証しました。

どちらもDockerコンテナの形式で起動し、Pythonからの操作と、GUIでの操作を試しています。



環境

  • Ubuntu 16.04
  • Docker version 18.06.0-ce
  • docker-compose version 1.24.1
  • etcd:3.4.3
  • redis:5.0.7


etcd

まずはetcdです。

コンテナとして構築

下記のDockerHubにあるdocker-compose.ymlをダウンロードします。

https://hub.docker.com/r/bitnami/etcd/

$ curl -LO https://raw.githubusercontent.com/bitnami/bitnami-docker-etcd/master/docker-compose.yml

ダウンロードしてきたら、中身を以下のように書き換えます。

version: '3.5'

services:
  etcd:
    image: bitnami/etcd:3.4.3
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379
    ports:
      - 2379:2379
      - 2380:2380
    networks:
      - etcdtest
  myapp: # etcd操作用のコンテナを追加
    image: python:3.6
    command: tail -f /dev/null
    networks:
      - etcdtest

networks: # コンテナ同士を通信させるためにネットワークを追加
  etcdtest:
    name: etcdtest
    driver: bridge

etcdコンテナに加えてpythonからetcdを操作するためにmyappコンテナも起動する設定にしています。commandで/dev/nullをtail -fさせておき、コンテナが起動し続けられるようにしています。

また、内部ネットワークとしてetcdtestを作成し、コンテナ同士が疎通できるようにしています。


では作成したdocker-compose.ymlをもとにコンテナを起動させます。

$ sudo docker-compose up -d
Creating network "etcdtest" with driver "bridge"
Creating etcdtest_etcd_1  ... done
Creating etcdtest_myapp_1 ... done

コンテナが二つ立ち上がればOKです。


pythonからetcdを操作

コンテナが二つ立ち上がったので、実際にPythonでetcdを操作してみます。

まずはmyappコンテナにログインしてetcdのpython sdkとなるetcd3をインストールします。

$ sudo docker exec -ti etcdtest_myapp_1 bash
root@83cbcbc628f8:/# pip install etcd3

続いて、そのままコンテナにログインした状態でpythonインタプリタを起動し、etcd3をimportします。

root@83cbcbc628f8:/# python
Python 3.6.9 (default, Oct 17 2019, 06:34:57)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import etcd3
>>>

実際にetcdに接続し、データの書き込みと読み込みをしてみます。

>>> client = etcd3.client(host='etcd' port=2379)
>>> client.put('lang', 'python')
header {
  cluster_id: 14841639068965178418
  member_id: 10276657743932975437
  revision: 4
  raft_term: 2
}

>>> client.get('lang')
(b'python', <etcd3.client.KVMetadata object at 0x7f1fc95afa90>)

>>> for i in client.get_all():
...   print(i)
...
(b'python', <etcd3.client.KVMetadata object at 0x7f1fcb3ba668>)

接続の方法はシンプルにhostとportを指定するだけでできました。また、keyとvalueの設定や取り出しもputgetで簡単にできました。

ただ、keyとvalueの一覧はget_all()を使ってイテレータを取得する形になるため、表示させるにはfor文とprintを使う必要があります。


GUIからの操作

続いてGUIでの操作を試すために、ETCD Managerというツールを使ってみます。(これしか見つからなかった)

GitHub - icellmobilsoft/etcdmanager: A cross-platform GUI for ETCD

アクセス元のクライアントにインストールして使うタイプのツールのため、今回はMac OS用のインストールファイルをダウンロードします。

githubのreadme.mdにインストールファイルのリンクがあります。

f:id:sagantaf:20200214004538p:plain

ダウンロードしてきたら、ダブルクリックして開きます。

よくあるインストールページとして下記の画面が開きますので、etcd-managerのアイコンをApplicationsにドラック&ドロップしてください。

f:id:sagantaf:20200214004626p:plain

ぱっと見、反応がないですが、Applicationsにちゃんと追加されていました。クリックして起動します。

初回起動時はMacのデフォルトの動作としてセキュリティに引っかかって起動させてくれません。「システム環境設定 > セキュリティとプライバシー」の画面を開いて、下記のように”このまま開く”をクリックします。

f:id:sagantaf:20200214004715p:plain

これでETCD Managerが起動するはずです。

f:id:sagantaf:20200214004742p:plain

起動したら、Settings > ETCD を押します。下記のように接続先を入力する画面になりますので、etcdコンテナが起動しているホストのIPアドレスとポート番号2379を入力し、SAVEしてください。

右上に緑のダイアログが表示されればOKです。

f:id:sagantaf:20200214004815p:plain

この状態で左のメニューから「Manage keys」に移動すると、先ほど入力したkeyとvalueが表示されるのが分かります。

f:id:sagantaf:20200214004841p:plain

GUIのツールインストールと操作は非常に簡単にできることがわかりました。見た目も分かりやすいし、サクサク動きます。

ただ、クライアント側にインストールするタイプのツールであるため、開発者ごとにインストールしないといけない点がイマイチです。


Redis

続いてredisを構築して操作してみます。

コンテナとして構築

まずはdocker-compose.ymlファイルを作成します。

version: '3.5'
services:
  redis:
    image: redis:5.0.7
    ports:
      - 6379:6379
    volumes:
      - ./data/reis:/data
      - ./redis.conf:/etc/redis/redis.conf
    command: redis-server /etc/redis/redis.conf
    networks:
      - redistest
  myapp:
    image: python:3.6
    command: tail -f /dev/null
    networks:
      - redistest
networks:
  redistest:
    name: redistest
    driver: bridge

etcdの時と同様、my appコンテナを用意しています。一つ異なる点は、Redisの設定ファイルである redis.confをボリュームマウント形式でコンテナに渡している部分です。

そのため、redis.confを下記のように作成しておく必要があります。

port 6379
bind 0.0.0.0

Redisコンテナにアクセスするためのホストとポートの設定をしているだけです。

では作成したdocker-compose.ymlをもとにコンテナを起動させます。

$ sudo docker-compose up -d
Creating network "redistest" with driver "bridge"
Creating redistest_redis_1 ... done
Creating redistest_myapp_1 ... done

コンテナが二つ立ち上がればOKです。


pythonからredisを操作

コンテナが二つ立ち上がったので、実際にPythonで操作してみます。

まずはmyappコンテナにログインしてredisのpython sdkとなるredisをインストールします。

$ sudo docker exec -ti redistest_myapp_1 bash
root@fe8d915cd26b:/# pip install redis

続いて、そのままコンテナにログインした状態でpythonインタプリタを起動し、接続、書き込み、読み込みなどの操作をしてみます。

>>> import redis
>>> r = redis.Redis(host='redis', port=6379, db=0)
>>> r.set('hoge',123)
True
>>> r.get('hoge')
b'123'
>>> r.keys()
[b'hoge']

接続やkey/valueの取り出しはetcdと同じく非常にシンプルにできました。また、etcdとは異なり、r.keys()の一文でvalueを取ってこれました。


GUIからの操作

続いてGUI操作のためのツールを使ってみます。

こちらはコンテナとしてサーバを起動してブラウザからアクセスできるタイプです。そのため、Macなどのローカル環境にインストールする必要はありません。

Installing RedisInsight on Docker | Redis Labs Documentation Center


シンプルにコマンドでコンテナを起動させます。

docker run -d -v redisinsight:/db -p 8001:8001 --name redisinsight redislabs/redisinsight

問題なく起動したら、ブラウザから <ホストのIPアドレス>:8001にアクセスしてみます。

最初は下記のようなシンプルな画面が表示されます。

f:id:sagantaf:20200214005033p:plain

「ADD REDIS DATABASE > ADD DATABASE」を選択して、redisコンテナに接続します。

Nameは何でもOKです。HostはホストのIPアドレスを入力し、Portには6379を入力します。

f:id:sagantaf:20200214005111p:plain

接続に成功したら、Redisコンテナにアクセスできるようになります。

実際に左のメニューにある「Browser」を確認すると、python sdkで追加したkeyとvalueを確認できます。

f:id:sagantaf:20200214005129p:plain

etcdと同じくGUIのツールインストールと操作は非常に簡単にできました。


おわりに

etcdはKubernetesで利用されているため、KVSとしてだけでも使っておいたら理解が深まり、一石二鳥かと思いましたが、Redisの方が使いやすく感じました。ただ、それぞれ得意な場面があり、用途に応じて使い分けられるのがベストだと思います。

今回の目的である、どちらがより簡単に使い始められるか、シンプルな導入として適しているか、という意味ではRedisが適している!と言えそうです。


参考

etcd3 · PyPI

PythonでRedisを操作しよう (基本操作編)

docker-composeでコンテナの内部IPアドレスを固定化してWebサイトを作る

はじめに

docker-composeでDockerコンテナの内部IPアドレスを固定化し、nginx + uwsgi + flask を使ってWebサイトの環境を構築します。

サイトの中身自体は hello world するだけの超簡単な内容にしてあるので、正直nginxを挟む必要は無いですが、コンテナが内部IPアドレスで通信できることを確かめるためにnginxを使っています。



環境

  • Ubuntu 16.04
  • docker 18.09.7
  • docker-compose 1.23.2

Dockerとdocker-composeのインストール方法は下記をご参考ください。

sagantaf.hatenablog.com

ちなみにDocker for Mac (macOS Catalina)でも変わらず作れます!


全体構成

nginxとuwsgiのコンテナを2つ作ります。 また、app_netをいう名前で内部ネットワークを作成します。

app_netのサブネットは172.30.0.0/24の範囲とし、nginxコンテナに172.30.0.2IPアドレスを、uwsgiコンテナに172.30.0.3IPアドレスを割り当てます。 172.30.0.1となっているbridgeは自動的に作成されます。

f:id:sagantaf:20200211000601p:plain


nginxコンテナ

nginxコンテナを構築するために、nginxの設定ファイルとDockerfileを作成します。

まずは好きなディレクトリで下記を実行します。

mkdir nginxuwsgi
cd nginxuwsgi/

nginx.conf

nginxディレクトリを作成し、その中にnginx.confを作成します。

(nginxは素人なので余計なパラメタ設定もあるかもしれません。。。)

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen 80;
        charset utf-8;
        location / {
            include uwsgi_params;
            uwsgi_pass 172.30.0.3:8765;
        }
    }
}

重要なのは、最後のservserの部分です。

    listen 80;
    charset utf-8;
    location / {
        include uwsgi_params;
        uwsgi_pass 172.30.0.3:8765;
   }

nginxコンテナの80ポートに来たアクセスを、172.30.0.3:8765(uwsgiコンテナ)に受け渡す設定となっています。

Dockerfile

続いては、同じnginxディレクトリの中にDockerfileを作成しますが、中身はとてもシンプルです。

FROM nginx

CMD ["nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]

nginxがあらかじめインストールされたベースイメージを使っています。

またCMDで、先ほど作成したnginx.confを指定してnginxサーバを起動しています。

/etc/nginx/を指定していますが、後ほどdocker-compose.ymlを作る時に、ホストのnginx.confをコンテナにボリュームマウントします。


ここまでで、下記のようになっているはずです。

nginxuswgi
└──  nginx
    ├── Dockerfile
    └── nginx.conf


uwsgiコンテナ

uwsgiコンテナを構築するために、flaskのソースコード(app.py)、uwsgiの設定ファイル(uwsgi.ini)、Dockerfileを作成します。

flask(app.py)

uwsgiディレクトリを作成し、hello worldを表示するためのソースコードをapp.pyとして作成します。

# -*- coding: utf-8 -*-
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello, World!"

if __name__ == "__main__":
    app.run()

uwsgi.ini

続いて同じuwsgiディレクトリの中にuwsgiの設定ファイルを作成します。

[uwsgi]
wsgi-file = /var/www/app.py
callable = app
master = true
processes = 1
socket = :8765
chmod-socket = 666
vacuum = true
die-on-term = true
py-autoreload = 1

wsgi-file = /var/www/app.py でwebサーバとして起動する元となるソースコードを指定しています。/var/wwwとしていますが、nginxの時と同様、docker-compose.ymlにてホストのapp.pyをボリュームマウントします。

socket = :8765でリクエストを受け付けるポート番号を指定しています。先ほどnginx.confで8765ポートを指定したので、同じポート番号にする必要があります。

Dockerfile

最後にDockerfileです。(uwsgiディレクトリの中で作成します。)

FROM python:3.6

RUN pip install uwsgi flask
RUN mkdir /var/www/
WORKDIR /var/www/

CMD ["uwsgi", "--ini", "/var/www/uwsgi.ini"]

ベースはpython3.6があらかじめインストールされているイメージにしています。

またCMDで、先ほど作成したuwsgi.iniを指定してuwsgiサーバを起動しています。


ここまでで、下記のようになっているはずです。

nginxuswgi
├── nginx
│   ├── Dockerfile
│   └── nginx.conf
└── uwsgi
    ├── Dockerfile
    ├── app.py
    └── uwsgi.ini


docker-compose

最後にdocker-compose.ymlを作成し、コンテナを起動します。

serviceでコンテナの設定を記述し、networksで内部ネットワークの設定を記述しています。

docker-compose.yml

version: "3.5"

services:
  nginx:
    build: ./nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "10080:80"
    networks:
      app_net:
        ipv4_address: 172.30.0.2

  uwsgi:
    build:
      context: ./uwsgi
    volumes:
      - ./uwsgi:/var/www/
    networks:
      app_net:
        ipv4_address: 172.30.0.3

networks:
  app_net:
    name: app_net
    driver: bridge
    ipam:
     driver: default
     config:
       - subnet: 172.30.0.0/24

networksでは、内部ネットワークのドライバとしてbridgeを指定し、サブネットとして172.30.0.0/24を指定しています。これで、172.30.0.2 ~ 172.30.0.254をコンテナの内部ネットワークとして利用できます。

serviceではnginxとuwsgiのコンテナの設定を記述しています。build:でそれぞれDockerfileの場所を指定することで、コンテナビルドごとdocker-composeで実施してしまっています。(事前にコンテナビルドしてイメージを作成しdocker-compose.ymlで指定する形式ではない。)

また、volumes:でそれぞれの設定ファイルをボリュームマウントさせてコンテナに渡しています。

networks.app_net.ipv4_addressの部分ではコンテナそれぞれのIPアドレスを指定しています。

コンテナの起動とアクセス確認

最終的な構成は以下のようになっているはずです。

nginxuswgi
├── docker-compose.yml
├── nginx
│   ├── Dockerfile
│   └── nginx.conf
└── uwsgi
    ├── Dockerfile
    ├── app.py
    └── uwsgi.ini

では、コンテナを起動してみます。docker-compose.ymlが配置されている場所で下記コマンドを実行します。-dはバックグラウンド起動のオプションです。

$ docker-compose up -d
Creating network "app_net" with driver "bridge"
Creating nginxuswgi_nginx_1 ... done
Creating nginxuswgi_uwsgi_1 ... done

errorにならずdoneが表示されればOKです。

ブラウザでhttp://<ホストマシンのIPアドレス>:10080にアクセスしてみてください。

画面にHello, World!と表示されれば、正常にサーバが稼働し疎通ができています。

もし、エラーが発生した場合は、docker logs nginxuswgi_nginx_1などのコマンドを実行しログから原因を探ってみてください。

ネットワーク構成を確認

コンテナが問題なく起動している状態で、ネットワークの構成を確認してみます。

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
2a94d4756996        app_net             bridge              local
f5e41246ea45        bridge              bridge              local
4920c3cc5a3e        host                host                local
6278bf6b8cc0        none                null                local

app_netが作成されていることがわかります。docker network inspect app_netでより詳細な情報を確認できます。サブネットや所属しているコンテナの情報まで表示されます。(出力が多いので省略)

また、ifconfigでブリッジが1つ作成されていることも確認できます。

$ ifconfig
br-2a94d4756996 Link encap:イーサネット  ハードウェアアドレス XX:XX:XX:XX:XX:XX
          inetアドレス:172.30.0.1  ブロードキャスト:172.30.0.255  マスク:255.255.255.0
          inet6アドレス: fe80::42:a6ff:XXXX:XXXX/64 範囲:リンク
          UP BROADCAST RUNNING MULTICAST  MTU:1500  メトリック:1
          RXパケット:0 エラー:0 損失:0 オーバラン:0 フレーム:0
          TXパケット:44 エラー:0 損失:0 オーバラン:0 キャリア:0
          衝突(Collisions):0 TXキュー長:0
          RXバイト:0 (0.0 B)  TXバイト:5372 (5.3 KB)

(省略)


最後に掃除

起動したコンテナや作成したネットワークは下記のコマンドで削除できます。

docker-compose down
Stopping nginxflask_uwsgi_1 ... done
Stopping nginxflask_nginx_1 ... done
Removing nginxflask_uwsgi_1 ... done
Removing nginxflask_nginx_1 ... done
Removing network app_net


おわりに

dockerのネットワークについての基本を知りたい方はこちらをどうぞ!

sagantaf.hatenablog.com

Go言語の開発環境をMacとVScodeで作りコードを動かしてみる

はじめに

A Tour of GoでGo言語の基本を学んだので、次は実際に開発するために開発環境を構築してみます!
また、Go Modulesを使ったモジュールの作成も簡単に試しています。


想定環境

Golangのインストール

まずはGo言語自体のインストールです。

bash-3.2$ brew install go

を実行します。

インストールが問題なく完了したら、次はパスを追加します。.bash_profileに以下を追加します。 (.bashrcはデフォルトで読み込んでくれないので.bash_profileを利用)

export GOPATH=$HOME/golang/go
export PATH=$GOPATH/bin:$PATH
export GO111MODULE=on

反映します。

bash-3.2$ source ~/.bash_profile
bash-3.2$ go version
go version go1.13.7 darwin/amd64

バージョンが表示されればOKです。

VSCodeのインストール

Virtual Stadio Codeを

https://code.visualstudio.com/

からダウンロードし、インストールします。

VSCodeをGo環境用にカスタマイズする

Go言語を開発しやすくするために、VSCodeのアドオンをインストールします。

Golang開発環境を作成する(module対応) - Ubuntu16.04/VSCode - Qiita

を参考に実施し、$GOPATH配下に色々インストールされていることを確認します。

基本的な開発の方法

さて、これだけで環境の構築自体は終わりです!
が、どういうディレクトリ構成でソースコードやモジュールを作成したら良いのか分からなかったので、実際に試してみました。

hello worldしてみる

$HOME/golangにて、sampleprojectディレクトリを作成し、その中にhello.goファイルを作成します。

ディレクトリ構成は以下のようになります。

$HOME/golang
├── go
│   └── 自動インストールされたいろんなモジュール
└──  sampleproject
    └── hello.go

hello.goの中身は下記のようにします。

package main

import "fmt"

func main() {
    fmt.Println("Hello, world.")
}

このファイルをVSCodeで作成したら、下のパネルのTERMINALにてビルドとランを実行します。

もし、下にパネルが表示されていない場合は、View > Appearance > Show Panelを選択するか、⌘+Jを押すことで表示させることができます。

f:id:sagantaf:20200208220412p:plain


ビルドとランを一気に実行するには、

bash-3.2$ go run hello.go
Hello, world.
bash-3.2$ 

とします。

ビルドとランを別々のコマンドで実行するには、

bash-3.2$ go build hello.go 
bash-3.2$ ls
hello           hello.go
bash-3.2$ ./hello 
Hello, world.
bash-3.2$ 

とします。この場合はバイナリファイル(hello.go)を残すことができます。

モジュールを作成してみる

次は自分でモジュールを作成して、別のコードからimportしてみます。Go Modulesという機能を使います。

Go Modulesについては下記で詳しく解説されていました。

Go Modules - Qiita

超簡単に要約すると、今まではGoのソースコードは$GOPATHに配置して扱う必要があったらしいですが、Go 1.12以降、Go Mudulesという機能が用意され、$GOPATH外にあっても扱えるようになったらしいです。

今回は、$GOPATHを $HOME/golang/goに設定していますが、sampleprojectディレクトリは$GOPATHの外にあるので、そのままsampleprojectディレクトリの中でモジュールを作成してみます。

まずは$GOPATH外のモジュールでも呼び出せるようにするためにgo modコマンドを使って初期化します。

go mod init <モジュールをimportするときの文言>として初期化することで、同階層にgo.modファイルが作成されます。これで$GOPATH外でもimportできるようになるようです。

実際にやってみます。

bash-3.2$ pwd
$HOME/golang/sampleproject
bash-3.2$ 
bash-3.2$ go mod init github.com/hoge-username/sampleproject
go: creating new go.mod: module github.com/hoge-username/sampleproject
bash-3.2$ ls
go.mod          hello           hello.go
bash-3.2$ cat go.mod 
module github.com/hoge-username/sampleproject

go 1.13
bash-3.2$ 

通例として自作モジュールの場合は リポジトリURL/アカウント名/リポジトリ名 を指定してinitした方が分かりやすくなるようです。

今回は、

go mod init github.com/hoge-username/sampleproject

としてinitしました。こうすることで、 github.com/hoge-username/sampleproject が自作moduleをimportするときに指定するPathとなります。

例えば、sampleproject/calc/add.goを作成しhello.goからimportする場合を考えてみます。

add.goおよびhello.goは下記のように作成します。

add.go

package calc

// Add arg1 and arg2
func Add(x, y int) int {
    return x + y
}

hello.go

package main

import (
    "fmt"
    "github.com/hoge-username/sampleproject/calc"
)

func main() {
    fmt.Println("Hello, world.")
    fmt.Println("2 + 3 = ", calc.Add(2, 3))
}

ディレクトリ構成は以下のようになっています。

sampleproject
├── calc
│   └── add.go
├── go.mod
└── hello.go

これでランすると正常に稼働します。

bash-3.2$ go run hello.go
Hello, world.
2 + 3 =  5
bash-3.2$ 

おわりに

これでGo言語を開発する環境を整え、開発の基本的な方法も理解できました。

go言語の文法基礎をサンプルとともに(part3)

はじめに

A Tour of GoでGo言語の基本を学んでみましたが、言葉や具体例がいまいち分かりづらい部分もあったため、自分なりにまとめました。 (今の自分のレベルに対する備忘録の側面が強いため、逆に分かりにくい部分もあるかもしれません…)

これはpart3です。

part1 -> go言語の文法基礎をサンプルとともに(part1) - sagantaf

part2 -> go言語の文法基礎をサンプルとともに(part2) - sagantaf


関数と関数型(Method)

複数の引数

可変長引数をとる場合の定義は以下のようになります。

func f(s string, params ...string) {
    fmt.Println(s)
    for _, p := range params {
       fmt.Println(p)
    }
}

これは引数を全てprintする関数となっています。

関数型

Go言語では、関数を変数に格納することができ、その場合、関数を関数型というデータ型で扱うことになります。

func multiply(a int, b int) int {
    return a * b
}
func main(){
    var own_func func(int, int)int // 引数を2つ取り、intをreturnする関数をown_funcという変数として定義
    own_func = multiply // own_funcにmultipyという関数を格納
    fmt.Println(own_func(3,4)) // 12
}

この場合、own_funcという変数の型が関数型となります。

変数だけでなく、引数や戻り値に関数を指定することもできます。

Closures(クロージャ)

関数を変数として定義し、その関数を呼び出すたびに処理を実行することができます。つまり、関数に値を渡して計算をさせ結果を取得することができ、これをクロージャと呼びます。

以下はカウンターを関数で作成した例です。

package main
import "fmt"

func createCounter() func() int {
        n := 0 // initialize
        return func() int { // createCounterが呼び出されるたびにこのreturnが実行される
                n += 1
                return n
        }
}

func main() {
        counter := createCounter() // 関数を変数として定義。initializeの部分のみ実行される
        fmt.Println(counter()) // 1
        fmt.Println(counter()) // 2
        fmt.Println(counter()) // 3
}

構造体のメソッド

go言語では、classがないためclassのメソッドを使う、といったことはできませんが、似たような定義として、構造体にメソッドを持たせることができます。

func (d UserStruct) greeting(greet_word string)  string {
    return greet_word + ", I'm " + d.name + "."
}
func main(){
    taro := UserStruct{"taro", 25, true}
    fmt.Println(taro.greeting("Good Morning"))
}

funcの後ろに構造体を指定します。上記の例だとd Userstructの部分です。これをレシーバ引数と呼びます。後の書き方は、普通の関数の定義と同じです。

ただし、レシーバは上記のように変数として渡すのではなく、ポインタとして渡した方が使いやすくなります。

変数として渡した場合、構造体のメソッドは新たなコピーをreturnします。

例えば、

type nums struct { a, b int }
func (n nums) Twice() nums {
    n.a = n.a * 2
    n.b = n.b * 2
    return n
}
func main(){
    n := nums{2, 5}
    fmt.Println(n.Twice()) // {4 10}
    fmt.Println(n) // {2 5}
}

とした場合は、nに対してTwice()メソッドを実行しても、新たなコピーが作成されるため、元のnは変わっていません。

しかし、classのメソッドのようにして使いたい場合は、元の構造体nが直接変更された方が、便利です。

そこでポインタをレシーバとして渡すことで、元の構造体nのアドレスを渡せるため、Twice()メソッド実行時に新たなコピーを作成することなく、元の構造体nが変化します。

type nums struct { a, b int }
func (n *nums) Twice() *nums {
    n.a = n.a * 2
    n.b = n.b * 2
    return n
}
func main(){
    n := nums{2, 5}
    fmt.Println(n.Twice()) // &{4 10}
    fmt.Println(n) // {2 5}
}

これにより、変数の新たなコピーを作ることもなくなるため、メモリ効率もよくなります。

なお、上記例の場合、レシーバをポインタとして定義しているため、Twice()を呼ぶときは、本当は

fmt.Println((&n).Twice(2))

として書く必要があるはずです。しかし、n.Twice()でも問題なく動いた理由は、レシーバの型を自動的に解釈してくれる機能があるためです。 逆であっても(変数が必要なところをポインタを渡したとしても)自動的に解釈してくれます。

defer と panic と recover

defer

deferは、defer 処理 と書き、この処理の実行を、呼び出し元の関数の終わり(returnする)まで遅延させます。つまり、main内でdeferを使った場合は、mainの処理を最後まで実行した後にdeferに渡した処理を実行させます。評価自体はすぐに実行されますが、処理の実行が最後になる形です。

func main() {
        defer fmt.Println("first")
        fmt.Println("second")
}

この例の場合、出力は

second first

となります。

defer へ渡した処理が複数ある場合、その呼び出しはstackされ、最後にLIFO(last-in-first-out) としてstackした順番とは逆に出力されます。

func main() {
        defer fmt.Println("あ")
        defer fmt.Println("い")
        defer fmt.Println("う")
        fmt.Println("か")
        fmt.Println("き")
        fmt.Println("く")
}

この例の場合、出力は

    か
    き
    く
    う
    い
    あ

となります。

deferの使いどころについては後述しています。

panic

Go言語では、panicという関数を利用して、エラー処理をすることができます。

関数の中でpanicを呼び出すと、その時点で処理が停止し、関数の呼び出し元に戻る仕組みとなっています。main関数まで戻るとスタックトレースを出力し、プログラムが終了します。

func main(){
    fmt.Println("Begin")
    panic("panic happen!")
    fmt.Println("End")
}

上記を実行すると、下記のように表示されます。

Begin
panic: panic happen!

goroutine 1 [running]:
main.main()
        /tmp/sandbox698225432/prog.go:8 +0xa0

deferとpanicの使いどころ

deferは、リソースの後始末をするために利用されることが多いです。

例えば、ファイルのReadをした後にエラーが発生し、ファイルをCloseしないまま終わってしまうことを回避できます。

例は下記のページに記載されています。

Defer, Panic, and Recover - The Go Blog

また、deferに加えてpanic/recoverの使い方やerrとの使い分けについては、下記に詳しく記載されています。

Golang の defer 文と panic/recover 機構について - CUBE SUGAR CONTAINER

インターフェース(Interface)

インターフェースの基本

メソッドの集まりを定義するためにインターフェースは使われます。

type インターフェース名 interface { メソッドA() メソッドB() }という形式で定義します。

インターフェースはその名前の通り、構造体のインターフェースとなるメソッドと考えるとイメージしやすくなります。

戦士という構造体があり、アタッカーというインターフェースがあるとすると、戦士アタッカーというインターフェースをアタッチすることで、戦士はアタッカーの行動が取れる、というイメージです。

より具体的に

戦士という構造体として、

  • 名前
  • レベル

が定義されているとします。

また、アタッカーというインターフェースがあり、下記のメソッドが用意されているとします。

メソッド:

  • 武器を手に取る
  • 狙いを定める
  • 武器で攻撃する

このとき、戦士という構造体に、アタッカーというインターフェースをアタッチすることで、「武器を手に取る」や「武器をで攻撃する」というメソッドを利用できるようになります。

実際に実装すると、以下のようになります。

// アタッカーというインターフェースの定義
type Attacker interface {
        haveWeapon()
        setAim()
        moveWeapon()
}

// 戦士という構造体の定義
type Warrior struct {
        name string
        level int
        weapon string
        aim string
}

// インターフェースのメソッドを定義
func (w *Warrior) haveWeapon() {
        w.weapon = "Sword"
}
func (w *Warrior) setAim() {
        w.aim = "monster"
}
func (w *Warrior) moveWeapon() {
        fmt.Printf("%s attack %s using %s.", w.name, w.aim, w.weapon)
}

func main() {
        w := Warrior{"Taro", 15, "none", "none"}
        var actor Attacker = &w // インターフェースactorと構造体Warriorを紐付け
        
        actor.haveWeapon()
        actor.setAim()
        actor.moveWeapon()
}

出力は、

Taro attack monster using Sword.

となります。

例えば、actor.haveWeapon()actor.setAim()コメントアウトして実行すると、よりインターフェースの動きが理解できると思います。

インターフェースの利点と注意点

戦士という構造体のメソッドとして直接定義した方が手っ取り早いと思うかもしれませんが、その場合、魔法使い盗賊といった構造体を新しく作りたいときに毎回専用のメソッドを定義しなくていけなくなります。

どんな構造体にも紐付けられるメソッドとしてインターフェースを定義しておくことで、魔法使い盗賊といった構造体に対しても同じメソッドをアタッチすることができるため、効率的に開発できます。(オブジェクト指向ポリモーフィズムと同じ)

注意点としては、インターフェースで定義されているメソッドを全て定義する必要があります。定義していない場合はimplementエラーが発生します。(例えば「武器を手に取る」「武器を当てる」のみを定義して、「狙いを定める」を定義しなかった場合)

構造体の中身がnilのままインターフェースをアタッチすることもできる

構造体の中身を定義せずともインターフェースをアタッチすることができます。そのため、例えば先に構造体とインターフェースの枠だけ用意しておき、後から中身を定義するということも可能です。

func main() {
        // w := Warrior{"Taro", 15, "none", "none"}
        var w Warrior
        var actor Attacker = &w // インターフェースactorと構造体Warriorを紐付け

        w.name = "Taro"
        actor.haveWeapon()
        actor.setAim()
        actor.moveWeapon()
}

空インターフェースと型アサーションと型switch

空インターフェース

インターフェースは空っぽの状態でも定義できます。これを空インターフェースと呼び、上述のインターフェースとはまた別の使い方ができます。

空インターフェースは下記のように定義し、どんな型のデータでも格納ができる特徴があります。

var blank_if interface{}
blank_if = 123
blank_if = []int{1, 2, 3}
blank_if = func hello(_ string) string { return "Hello world" }

アサーション

この特徴を活かして、型アサーションすることができます。アサーションというと分かりにくいかもしれませんが、要するに、型のチェックができるということです。

下記のように使います。

var blank_if interface{}
blank_if = 123

inputdata, ok := blank_if.(int)

このようにvalue, ok := <変数>.(type)とすることで、変数とtypeが合っていれば、valueに変数が、okにtrueが格納されます。上記の例でいうと、inputdata=123, ok=trueになります。

変数とtypeが合っていない場合は、valueにはtypeのゼロ値が、okにfalseが格納されます。

inputdata, ok := blank_if.(string) // inputdata= , ok=falseになる
inputdata, ok := blank_if.([]string) // inputdata=[], ok=falseになる
inputdata, ok := blank_if.(func()) // inputdata=<nil> , ok=falseになる

では、この型アサーションができて何が嬉しいかというと、例えば関数に渡されたデータの型によって処理を変えたりすることができます。

具体例を見てみます。

func printItems(items interface{}) {
        var item_list []string

        if value, ok := items.([]string); ok { //string型のsliceの場合
                item_list = value
        } else if value, ok := items.(string); ok { // string型の場合
                item_list = []string{value}
        } else {
                fmt.Printf("you cannot use %T type", items)
                return
        }
        for _, v := range item_list {
                fmt.Println(v)
        }
}

func main() {
        printItems([]string{"hoge", "hogehoge"})
        printItems("string")
        printItems(3)
}

if文の条件として型アサーションを実行し、okにtrueが入れば処理が実行されるように実装しています。これにより、printItems関数はstring型のスライスを入力されても、string型を入力されても問題ないため、柔軟性も持たせられます。

この出力は、

hoge
hogehoge
string
you cannot use int type

になります。

型switch

型switchは、上記の型アサーションのif文をswitch文で行うことです。 上記の例を型switch文で書き換えます。

func printItems(items interface{}) {
        var item_list []string
        
        switch value := items.(type) {
        case []string:
                item_list = value
        case string:
                item_list = []string{value}
        default:
                fmt.Printf("you cannot use %T type", items)
                return
        }
        
        for _, v := range item_list {
                fmt.Println(v)
        }
}

func main() {
        printItems([]string{"hoge", "hogehoge"})
        printItems("string")
        printItems(3)
}

よりシンプルに書けることがわかるかと思います。

並列処理

ゴルーチン (goroutin)

ゴルーチンは並行処理を実現するための仕組みです。 go文で関数を呼び出すことで、実行したプログラムとは別で処理が動き出します。

package main
import (
        "fmt"
        "time"
)

func main(){
        go hello() // hello()関数を別処理として実行
        fmt.Println("main1")   
        time.Sleep(time.Millisecond * 10)
        fmt.Println("main2")
}

func hello(){
        time.Sleep(time.Millisecond * 10)
        fmt.Println("sub1")
}

こうすると、出力は

main1
sub1
main2

となります。

sub1がmain2の前に表示されているため、hello()が並行して稼働していることがわかります。

ただし、main()はhello()の終了を待ってくれないため、main()が終わるとhello()の処理はまだ動いていたとしても終了してしまいます。

2行追加して確認してみます。

func hello(){
        time.Sleep(time.Millisecond * 10)
        fmt.Println("sub1")
        time.Sleep(time.Millisecond * 10) //追加
        fmt.Println("sub2") //追加
}

これを実行しても、sub2が表示されないことが分かるかと思います。これはsub2を表示する前にmain()が最後まで到達してしまったためです。

ゴルーチンを終了してからmain()が終了させるためにはチャネルという仕組みを使います。

チャネル

チャネルはゴルーチン間で値を送受信するための仕組みです。

make関数で生成し、矢印(<-, ->)で通信方向を定義できます。

func add(x int, y int, c chan int) {
        c <- x + y // channelに値を送信
}

func main() {
        c := make(chan int) // channelの作成
        go add(2,3,c) // goroutineの実行
        result := <- c // channelから値を受信
        fmt.Println(result)
}

make時にチャネルの型を明記し、その型でデータを送受信する必要があります。

また、チャネルはバッファを設定することができ、送受信のデータ量を制御できます。

c := make(chan int, 3)

とすると、3回まで送受信ができます。

例えば3回 c <- i という形で送信すると、4回目はブロックされます。

func main() {
        c := make(chan int, 3)
        c <- 1
        c <- 2
        c <- 3
        c <- 4 // ここでバッファを超える
}

上記を実行するとdeadlockというエラーが発生します。

fatal error: all goroutines are asleep - deadlock!

逆にチャネルに何も無い状態で受信したとしても同様のエラーがは発生します。

func main() {
        c := make(chan int, 3)
        fmt.Println(<- c)
}

ゴルーチンの同期

同期の方法は主に3つあります。

ただチャネルの受信を待つパターン

<- doneを書くだけで、ただチャネルが送られてくるのを待つだけの処理になります。

通常は以下を実行すると、worldのみ出力されます。

func wait(done chan bool){
        fmt.Println("hello ")
}

func main(){
        done := make(chan bool)
        go wait(done)
        fmt.Println("world")
}

しかし、以下のように2行追加することで、ゴルーチンの終了を待つようになるため、hello worldと表示されるようになります。

func wait(done chan bool){
        fmt.Println("hello ")
        done <- true  // 追加
}
func main(){
        done := make(chan bool)
        go wait(done)
        <- done // 追加
        fmt.Println("world")
}

closeでチャネルを閉じるパターン

closeを使うことで、チャネルを閉じたことを呼び出し元に伝えられます。

func printnum(n int, c chan int) {
        x := 0
        for i := 0; i < n; i++ {
                c <- x
                x += 1
        }
        close(c)
}

func main() {
        c := make(chan int)
        go printnum(5, c)
        for i := range c {
                fmt.Println(i)
        }
}

チャネルをcloseすることで、rangeループを終了させています。

closeしたことを明示的に受け取って判断するパターン

チャネルを受信するときに、2つ目のreturnにはチャネルのcloseされているかどうかが格納されています。

num, ok <- c

とすることで、okにはチャネルの状況によってtrueもしくはfalseが格納されます。

func wait(c chan int){
        c <- 1
        c <- 2
        close(c)
}

func main(){
        c := make(chan int)
        go wait(c)
        num, ok := <- c
        fmt.Println(num, ok)
        num, ok = <- c
        fmt.Println(num, ok)
        num, ok = <- c
        fmt.Println(num, ok)
}

上記の出力結果は、

1 true
2 true
0 false

となります。

チャネルがcloseされた時には、okにfalseが格納されていることに加えて、numには型のゼロ値(この場合はintなので0)が格納されていることも確認できます。

排他制御(sync.Mutex)

並列処理で同じリソースを扱う場合、どうしても排他制御が必要になってくる時があります。

go言語では、排他制御sync.Mutexによるリソースのロック、アンロックで実現できます。

例えば下記のように、Add()Mul()をゴルーチンで処理させたい場合を考えます。

package main
import (
        "fmt"
        "time"
)

type Counter struct {
        num int
}

func (c *Counter) Add(x int) {
        c.num = c.num + x
}

func (c *Counter) Mul(x int){
        time.Sleep(time.Second) //わざとsleep
        c.num = c.num * x
}

func main() {
        c := Counter{num: 1}
        go c.Add(2)
        go c.Mul(2)
        go c.Add(2)
        go c.Mul(2)

        time.Sleep(time.Second*3)
        fmt.Println(c.num)
}

このプログラムでは、main()にてnum: 1に対して、Add(2)Mul(2)を2回ずつ実行しています。

この計算は、

Add(2) → Mul(2) → Add(2) → Mul(2)

という順番になってほしいところです。

つまり、

(num:1 + 2 * 2) + 2 * 2 = 16

となってほしいですが、実際にはMul()にsleepが入っているため、先にAdd()が2回処理されてから、Add()が2回処理される動きとなってしまいます。

((num:1 + 2) + 2 ) * 2 * 2 = 20

そこで、sync.Mutexを使ってリソースをロックしておくことで、どんなに Mul(2)に時間がかかっても想定した通りの順番で処理を稼働させられます。

package main
import (
        "fmt"
        "sync" //追加
        "time"
)

type Counter struct {
        num int
        mux sync.Mutex//追加
}

func (c *Counter) Add(x int) {
        c.mux.Lock()//追加
        c.num = c.num + x
        c.mux.Unlock()//追加
}

func (c *Counter) Mul(x int){
        c.mux.Lock()//追加
        time.Sleep(time.Second)
        c.num = c.num * x
        c.mux.Unlock()//追加
}

func main() {
        c := Counter{num: 1}
        go c.Add(2)
        go c.Mul(2)
        go c.Add(2)
        go c.Mul(2)

        time.Sleep(time.Second*3)
        fmt.Println(c.num)
}

方法としては、リソースに対する処理を実行するときに、c.mux.Lock()でロックし、処理が終わった後にx.mux.Unlockでアンロックするだけです。

これで想定どおり、20が出力されます。

なお、ロック中にエラーが発生して、アンロックが処理されずに終わるとバグの原因になるため、deferでアンロックを処理する方が安全です。

func (c *Counter) Mul(x int){
        c.mux.Lock()
        defer c.mux.Unlock()
        time.Sleep(time.Second)
        c.num = c.num * x
}

参考

go言語の文法基礎をサンプルとともに(part2)

はじめに

A Tour of GoでGo言語の基本を学んでみましたが、言葉や具体例がいまいち分かりづらい部分もあったため、自分なりにまとめました。 (今の自分のレベルに対する備忘録の側面が強いため、逆に分かりにくい部分もあるかもしれません…)

これはpart2です。

part1は go言語の文法基礎をサンプルとともに(part1) - sagantaf です。


型について

型変換

float64(10)uint(2)のようにすることで、型変換できます。

i := 42
f := float64(i)
u := uint(f)

型宣言

型宣言、として型に別名をつけて新しい型を作成できます。type 新しい型 元の型の形式で作成します。

type SelfInt int // 型宣言
var num1 SelfInt = 1
var num2 SelfInt = 2
var num3 int = 3

func main() {
    fmt.Println(num1 + num2) //3
    //fmt.Println(num2 + num3) //型が異なるためエラーになる

構造体の基本

go言語にはclassは存在しません。その代わり、複数の変数をまとめて1つの型として扱うことができる構造体を利用できます。 型宣言と同様typeを使って宣言します。

type UserStruct struct {
    name string
    age int
    over20 bool
}
    
func main(){
    var taro UserStruct
    taro.name = "taro"
    taro.age = 25
    taro.over20 = true
    fmt.Println(taro.name, "は", taro.age, "歳です。")
}

構造体に構造体を埋め込むこともできます。その場合は型名のみ記載します。

type DevMember struct {
    UserStruct // 別の構造体を利用している
    lang string
    office string
}

func main(){
    var taro DevMember
    taro.name = "taro" // UserStructのフィールドをそのまま利用できる
    taro.lang = "Japanese"
    taro.office = "Tokyo"
    fmt.Println(taro.name, "は", taro.office, "で働いています。")
}


ポインタと構造体

Goにおけるポインタの扱い

Goはメモリアドレスを指すポインタを使えます。 var p *intのように宣言し、ゼロ値はnilになります。 変数(オペランド)に&をつけることで、そのポインタを得ることができます。

func main() {
        i := 42
        p := &i // pにはiのメモリアドレスが格納される
        fmt.Println(p)
        fmt.Println(*p) // *をつけることで中身を取り出せる
}

出力は下記。

0x414020
42

C言語とは異なり、ポインタ演算はありません。

構造体のフィールドにポインタを使ってアクセス

ポインタpを使ってフィールドにアクセスには、(*p).Xp.Xという書き方が可能です。

type Vertex struct {
        X int
        Y int
}

func main() {
        v := Vertex{1, 2}
        p := &v // vのメモリアドレスを取得
        p.X = 100000 //ポインタにドットを使ってフィールドにアクセス
        fmt.Println(v)
}

出力は下記になります。

{100000 2}

また、Structsに&をつけて、ポインタ型の構造体にすることもできます。

ポインタ型の構造体の場合は、*をつけることで中身を得られ、&をつけることでメモリアドレスを得られます。何もつけずにprintするとと&が付いた表記になります。

var (
        p  = &Vertex{1, 2} // pは*Vertexとしてポインタ型の変数になる
)

func main() {
        fmt.Println(p, p.Y) //&が付いた表記になる。フィールドアクセスも可能
        fmt.Println(*p, &p) // *で中身を、&でメモリアドレスを得られる
}

出力は下記になります。

&{1 2} 2
{1 2} 0x1cd044


配列とスライス(ArrayとSlice)

配列

同じ型を持つ要素をまとめたもので、固定長なため、一度作成したら要素数を変えられません。

下記のように配列名、サイズ、型を宣言して利用します。それぞれの要素には、a[x]のようにアクセスできます。

var a[3] int
a[0] = 1

var a[3] int = [3]int{2,4,6} // 初期値を指定して宣言できる
a := [...]int{2,4,6} //初期値を指定するのであれば、要素数を省略することも可能


スライス

スライスの基本

スライスは配列から一部を切り取って扱える可変な配列のようなものです。

func main() {
        nums := [...]int{1, 3, 5, 7, 9}
        var s []int = nums[1:4]
        fmt.Println(s) // [3 5 7]
}

上記のようにa[low : high]と指定し、highの値は含まれません。

スライスには実際に値が格納されるわけではなく、配列へのポインタが格納されるだけになります。そのため、スライスの要素を変更すると、その元となる配列の対応する要素も変更されます。

下記のコードでその挙動が確認できます。

func main() {
    countries := [...]string{"Japan", "Korea", "China"}
    fmt.Println("元の配列:", countries)
    a := countries[:2] // 先頭の0は省略可能
    b := countries[1:] // 末尾の値も省略可能([1:3]にしてもOK)
    fmt.Println("変更前のスライス:",a, b)

    a[0] = "Tokyo"
    fmt.Println("変更後のスライス:", a, b)
    fmt.Println("変更後の配列:", countries)
}

スライスaを編集しただけですが、bもcountriesも変わってしまいます。

元の配列: [Japan Korea China ]
変更前のスライス: [Japan Korea] [Korea China]
変更後のスライス: [Tokyo Korea] [Korea China]
変更後の配列: [Tokyo Korea China ]

Structを使ってSliceを作成することもできます。

func main() {
    s := []struct {
            i int
            b bool
    }{
            {2, true},
            {3, false},
            {5, true},
            {7, true},
            {11, false},
            {13, true},
    }
    fmt.Println(s[1]) // {3 false}
}

また、何も要素を書かずにスライスだけ定義すると、nilスライスになります。nilスライスは 長さ0と容量0で、何の元となる配列も持っていない状態です。

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s)) // [] 0 0
    if s == nil {
            fmt.Println("nil!")
    }
}

s == nilで判定できます。

SliceのLengthとCapacity

スライスには長さ( length )と容量( capacity )という属性を持っています。

lengthは、スライスに含まれる要素の数を表し、len(s)で算出できます。

capacityは、スライスの最初の要素から数えた、元となる配列の要素数です。cap(s)で算出できます。

下記のようなイメージです。

f:id:sagantaf:20200120000341p:plain

スライスはこのcapacityを持ったまま定義されているので、再スライスして長さを変えることができます。下記がその例です。

func main() {
        s := []int{1, 2, 3, 4, 5} // len=5, cap=5
        s = s[:0] // len=0, cap=5
        s = s[:4] //上記でスライスを一度0個にしているがcapは6のままなので再スライスできる
        printSlice(s) // len=4, cap=5
        s = s[2:6] //先頭を変えるとcapは減る
        printSlice(s) // len=4, cap=4
}
func printSlice(s []int) {
        fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

Sliceへの新しい要素の追加

スライスへ新しい要素を追加するには append 関数を使います。ただし、既存のスライスに追加するのではなく、新しいスライスがreturnされます。

基本的には

new_slice = append(old_slice, 追加する値, 追加する値, ...)

として使います。

追加していくことでcapは自動的に増えていきます。

func main() {
        var s []int
        printSlice(s) // len=0 cap=0 []
        s = append(s, 0, 1, 2)
        printSlice(s) // len=3 cap=4 [0 1 2]
}
func printSlice(s []int) {
        fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

make関数でSliceを作成できる

make 関数を利用してスライスを作成することもできます。

下記のように宣言することで、0で初期化されたスライスが生成されます。なお、capacity部分は省略可能です。

a := make([]type, length, capacity)

例えば以下のように使います。

a := make([]int, 5) // len=5 cap=5 [0 0 0 0 0]
b := make([]int, 0, 5) // len=0 cap=5 []
// -> とりあえずcapだけ決めておいてlenは必要な時に決めることができる
c := b[:2] // len=2 cap=5 [0 0]


RangeとMaps

Range

反復処理を実装するためにrangeが利用できます。

例えばスライスの要素を1つずつ取り出して処理をさせたい時には、rangeが使えます。イテレーションごとにindexとvalue(要素値のコピー)を返します。

var num_slice = []int{10, 100, 1000, 10000}
func main() {
        for i, v := range num_slice {
                fmt.Println(i, v)
        }
}

下記のようにprintされます。

0 10
1 100
2 1000
3 10000

indexかvalueどちらかだけ利用することも可能です。

for i := range num_slice { fmt.Println(i) } // vを省略
for _, v := range num_slice { fmt.Println(v) } // iを省略

Maps

mapの基本

mapはkeyとvalueをセットで保存する辞書のようなものです。

var 変数名 = map[keyの型]valueの型{key1:value1, key2:value2, …}

例えば、下記のように作成します。

var m = map[string]int{"first":1, "second":2, "third": 3}

また、makeを使っても作成できます。

make(map[keyの型]valueの型, capacityの初期値)
make(map[keyの型]valueの型)// capacityの初期値は省略可

var m = make(map[string]int, 3) // map[]となる
m["first"] = 1
m["second"] = 2
fmt.Println(m) // map[first:1 second:2]

mapの操作

追加や削除は以下のように実行します。

m["Answer"] = 42 //要素の追加
m["Answer"] = 48 //要素の変更
delete(m, "Answer") //要素の削除

キーの存在チェックもできるため、キーがあれば処理を実行する、なければしない、などの操作が可能になります。

v, ok := m["Answer"] 

上記の例だと、keyが存在すればokにtrueが入り、なければfalseが入ります。falseの場合はvには型のゼロ値が入ります。(intだと0, stringだと空白、など)

参考