string型の変数をfor文で回す
競プロで文字列を1文字ずつ扱う場面があって、改めてまとめてみる。
byte型とは
まずbyte
型について。
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is // used, by convention, to distinguish byte values from 8-bit unsigned // integer values. type byte = uint
そもそもバイト=8ビットで、10進数だと0~255(28-1)までを表現できる。Goではbyte
型は8ビットの符号なし整数のエイリアスになっており、整数として演算できる。
文字列をfor文で回す
文字列にインデックスでアクセスするとbyte
型が取得でき、文字列をfor range
で回すとrangeの第2返り値ではrune
型が取得できる。
s := "string" for i, r := range s { fmt.Println(s[i]) // byte fmt.Println(r) // rune // 出力はどちらも 115 116 114 105 110 103 fmt.Println(string(s[i])) // string fmt.Println(string(r)) // string // 出力はどちらも s t r i n g }
s := "string" for i := 0; i < len(s); i++ { fmt.Println(s[i]) // 115 116 114 105 110 103 fmt.Println(string(s[i])) // s t r i n g }
このように普段アルファベットをfor
文で回す際は、特に何も注意することなく、1文字ずつ処理することができる。
Goでは符号化方式としてUTF-8
を採用している。UTF-8
はASCII
互換の符号化方式で、ASCII
で定義されているアルファベットなどは1バイトで表現される。
そのため1バイト(Goのbyte
型で0~255)で表現できるASCII文字は問題なく扱えるが、文字によっては1バイトで表現できないため、1バイトごとに出力しても意図する値を出力できない可能性がある。
例えば以下のように、日本語の「あ」をbyte
型で出力すると、以下のように計3バイトで出力される。
ちなみに組み込み関数のlen()
は対象文字列のバイトサイズを取得する。
s := "あ" for i := 0; i < len(s); i++ { fmt.Println(s[i]) // 227 129 130 fmt.Println(string(s[i])) // ã ? ? }
よってGoでは1バイトで表現できない文字列でも1文字ずつ扱うために、コードポイント単位で文字を扱うrune
が用意されている。
rune
型は対象の文字列のコードポイント(16進数)を10進数に変換したものが割り当てられる。
// rune is an alias for int32 and is equivalent to int32 in all ways. It is // used, by convention, to distinguish character values from integer values. type rune = int32
コードポイントは最大で21ビットの数値(16進数で5~6桁)なので、rune
型は32ビットの整数のエイリアスになっており、整数として演算できる。
// runeは文字をシングルクオートで囲って書く // 実体はint32なので計算もできる fmt.Println('あ') // 12354 fmt.Println('あ' * 2) // 24708 fmt.Println('い' - 'あ') // 2
rune
型を利用することで、日本語のような1バイト以上で表される文字列でも1文字ずつ(コードポイント単位で)扱うことができる。
例えば「あ」をrune
型で出力する場合、「あ」のコードポイントは「U+3042」なので、これを10進数に変換した「12354」が出力される。16進数で出力したい時はPrintf
で%x
を使う。
s := "あ" for _, r := range s { fmt.Println(r) // 12354 fmt.Printf("%x", r) // 3042 }
rune
型をstring
型に直すにはstring()
を使う。
s := "あ" for _, r := range s { fmt.Println(string(r)) // あ }
スライスを使うとこんな感じ。
s := "あ" // コードポイントのスライス fmt.Println([]rune(s)) // [12354] // UTF-8で符号化したバイトスライス fmt.Println([]byte(s)) // [227 129 130]
ちなみにslicing
するとbyte
では返ってこないので注意。
s := "ABCD" fmt.Println(s[2:3]) // C fmt.Println(s[2]) // 67
16進数の計算とUnicodeとUTF-8
Goで文字列を1文字ずつ処理するときの仕組みが気になって深入りした内容をまとめておく。
16進数とバイトの関係
4ビットが表す数値は0から15(24-1)までである。 これは16進数で表現できる「0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F」と対応させて考えることができる。
つまり4ビットを16進数で表現すれば、1文字で表すことができる。
逆に言うと、16進数の1文字は4ビット、16進数の2文字は1バイト(8ビット)と考えることができる。
2進数から16進数への変換
「100110110110」という2進数を考える。 16進数では、10進数と区別するために数値の先頭に「0x」を付ける。
①2進数を4桁(4ビット)ごとに分割する。
4ビットごとに分割すると「1001」「1011」「0110」となる。
②2進数を10進数に変換する。
「20×1+21×0+22×0+23×1」「20×1+21×1+22×0+23×1」「20×0+21×1+22×1+23×0」となり「9」「11」「6」となる。
③10進数を16進数に変換して結合する。
16進数にすると「0x9」「0xB」「0x6」となり、最終的な16進数は「0x9B6」となる。
16進数から2進数への変換
「0x9B6」という16進数を考える。先程と逆の手順を踏む。
①1文字ずつに分解して10進数に変換する。
「0x9」「0xB」「0x6」を10進数に変換すると、「9」「11」「6」となる。
②10進数を2進数に変換する。
「9」「11」「6」を2進数に変換すると、「1001」「1011」「0110」となる。
③各2進数を結合する。
「100110110110」となる。
文字コードと符号化
コンピューター上では全てがバイナリで管理されている。 このバイナリを人間が読める文字に変換したり、人間が読める文字からバイナリに変換するためには文字とバイナリを対応させる必要がある。
このように、文字1つ1つに文字コードを割り当てた対応表のことを文字集合と言う。
文字集合には、ASCIIやUnicode、JIS系など様々な種類がある。
Unicodeについて
ASCIIはアルファベットなど限られた文字しか対応できず、国や地域を超えて文字コードをまとめた国際的な統一文字コードがUnicodeである。 例えばUnicodeでは「あ」という文字は「3042」という16進数で対応づけられる。
Unicodeではこの文字コードのことをコードポイントと言い、コードポイントの数値で文字を表す際、「U+16進数」(「U+3042」)のように表記する。
Unicodeの登場以前までは、最初に標準化された文字集合であるASCIIがHTTPなど主要なプロトコルで採用されていた。(HTTP/2ではバイナリやUTF-8を使うよう置き換えられている)
ASCIIは16進数で「0x0」から「0x7F」までの1バイトで文字を表現する。
符号化方式について
文字列をファイルに保存したり、ネットワークで通信したりする場合は、1バイトずつに分解して順番に書き込んだり、送信したりする必要がある。
Unicodeのコードポイントを単純に1バイトずつに分割してファイルに保存すると、コードポイントが被ることがある。
例えば「《」という文字は「U+300A」というコードポイントで表される。これを1バイトずつに単純に分けると、「U+0030」と「U+000A」となり、これらは「0」と改行文字のコードポイントであるため意図した文字列を保存することができない。
このようなことが起こらないように、一定のルールでバイト列に変換する方式を符号化方式と言い、UTF-8やUTF-16など様々な種類がある。
UTF-8による符号化
UTF-8はASCII互換の符号化方式で、1バイトから4バイトの可変長で変換し、8ビット(16進数で2文字)単位で表す。Go言語でも符号化方式はUTF-8が採用されている。
UTF-8では、コードポイントの範囲によって何バイトの値で表すかが以下のように決まっている。
コードポイント(16進数) | UTF-8のバイト列(2進) |
---|---|
00000000 ~ 0000007F | 0xxxxxxx |
00000080 ~ 000007FF | 110xxxxx 10xxxxxx |
00000800 ~ 0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
00010000 ~ 0010FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
⚠️ ASCII文字(コードポインタが00000000 ~ 0000007Fの範囲)は1バイトで表現されるので、ASCII互換で文字を表現できる
例えば「あ」という文字のコードポイント「U+3042」をUTF-8で符号化する。
①コードポイントを4バイトに変換する。
「0x3042」を4バイトに変換すると「00003042」となり、表の上から3行目の範囲に該当し、3バイトで表現されることが分かる。
②4バイトに変換したコードポイント(16進数)の各桁を10進数に変換する。
「3」「0」「4」「2」は10進数に変換しても「3」「0」「4」「2」となる。
③10進数に変換したコードポイントを2進数に変換する。
これを2進数に変換すると「0011」「0000」「0100」「0010」となる。
④UTF-8の符号化方式に当てはめて1バイト単位で2進数を得る。
これを表の3行目のバイト列に当てはめると、「11100011」「10000001」「10000010」となる。
⑤2進数を16進数に変換する。
16進数に変換すると、「0xE3」「0x81」「0x82」となり、UTF-8では「あ」という文字が「E3 81 82」というバイト列で表現される。
rmコマンドとunlinkコマンドについて
rm
コマンドのヘルプを出すと、Remove (unlink) the FILE(s).
と書いてあることから分かるように、rm
はファイルをunlink
するコマンド。
つまりオプションを指定しないと、rm
とunlink
は同じ。
user-name:~$rm --help Usage: rm [OPTION]... [FILE]... Remove (unlink) the FILE(s).
rm
コマンドは-rf
オプションなど色々できてしまうので、リンクを削除するときは安全にunlink
コマンドを使おう。
そもそもrm
やunlink
はどういう挙動なのか気になったのでまとめておく。
挙動
オプションを指定しない場合、rm
とunlink
は同じ挙動なので、適宜rm
=unlink
と読み替えてもらえれば良さそう。
rm
コマンドは該当するファイルのinodeの参照カウントをデクリメント(1つ上のディレクトリが持つ該当ファイルのレコードを削除)する。
inodeは参照カウントを持っているらしい。(知らなかった)
つまり、直接的にrm
コマンドによってinodeが解放されるわけではない。
(ハードリンクを張って同じinodeを参照しているファイル2つがあるとき、片方を削除してももう1方には影響がないことから考えても、inodeが解放されることはないことが分かる)
参照カウントが0になったinodeは解放される。
しかし、ファイルの実体の削除は対応するinodeを参照するプロセスがなくなるまで遅延される。
挙動の確認
test.txt
を作成する。
user-name:~$ touch test.txt
もう1つターミナルを開いてvimでtest.txt
を開いておく。
使用しているinodeの数を調べる。
user-name:~$ df -i | grep /dev/vda2 /dev/vda2 1966080 290521 1675559 15% /
1966080個のinodeのうち290521個が使われている。
test.txt
を削除する。
user-name:~$ rm test.txt
再度使用しているinodeの数を調べる。
user-name:~$ df -i | grep /dev/vda2 /dev/vda2 1966080 290520 1675560 15% /
使用しているinodeの数は減っているし、ls
コマンドでもファイルは確認できないが、vimは開いたままでプロセスが生きており、ファイル内容が参照できている。
この状態は、inodeは解放されているがファイルの実体の削除は遅延されている状態。 vimを閉じるとファイルを参照しているプロセスがなくなるのでファイルの実体も削除される。
参考
Linuxコマンドのディレクトリ末尾のスラッシュについて
mv
などLinuxコマンドでパスを指定するときに、ディレクトリの末尾に/
を付けるかどうかを迷いがちなので改めて整理する。
/
の有無
/
があると、
する
動作確認
まず使うディレクトリとファイルを作成
$ mkdir ~/test $ touch ~/test-dir/test.txt
mv
コマンドの動作確認
まずtest-dir
ディレクトリを~/Destkop
ディレクトリ配下に移動するとき
$ mv ~/test-dir ~/Desktop/
ちなみに移動先を表す第2引数の方は、ディレクトリと明示しても明示しなくて同じなので~/Destkop
でも~/Desktop/
でも同じ。
次にtest-dir
ディレクトリ配下のファイルを~/Desktop
ディレクトリ配下に移動するときは、test-dir
ディレクトリ配下を参照したいので/
を付ける
$ mv ~/test-dir/ ~/Desktop/
シンボリックリンクに対する挙動の違い
まずtest-dir
というディレクトリのシンボリックリンクを張る。
$ ln -s ~/test-dir ~/Desktop/test-dir-sym-link
ls -l
コマンドの挙動
ls -l
コマンドで/
をシンボリックリンクに付けずに実行すると、そのままシンボリックリンクの情報が参照される。
$ ls -l ~/Desktop/test-dir-sym-link lrwxr-xr-x 1 ryotarohayashi staff 30 2 14 22:14 /Users/ryotarohayashi/Desktop/test-dir-sym-link -> /Users/ryotarohayashi/test-dir
/
をシンボリックリンクに付けると、シンボリックリンク内のtest.txt
が参照される。
$ ls -l ~/Desktop/test-dir-sym-link/ total 0 -rw-r--r-- 1 ryotarohayashi staff 0 2 14 22:13 test.txt
ちなみにオプションなしのls
コマンドだと挙動は変わらない。
$ ls ~/Desktop/test-dir-sym-link test.txt
$ ls ~/Desktop/test-dir-sym-link/ test.txt
rm -r
コマンドの挙動
⚠︎ そもそもシンボリックリンクを削除するときはrm
ではなくunlink
を使おうというのは前提としてあって、rm -r
の挙動を確かめただけです。
まず、/
を付けないパターンは、普通にシンボリックリンクが削除される。
$ rm -r ~/Desktop/test-dir-sym-link
次に、/
を付けると、シンボリックリンク内を参照して-r
オプションで再起削除するので、シンボリックリンクは削除されずリンク元が削除される。
$ rm -r ~/Desktop/test-dir-sym-link/
ハードリンクとシンボリックリンクの挙動と使い道
リンクについてまとめていく。
コマンドの結果はmacOS Monterey 12.0.1
での実行結果です。
リンクとは
リンクとは異なるパスで同じファイルやディレクトリにアクセスできるようにする仕組み。 Unixのリンクはハードリンクとシンボリックリンクの大きく2つの種類がある。
ハードリンクはln
コマンド、シンボリックリンクはln
コマンドにs
オプションを付けて以下のように作成する。
$ ln -s <リンク元のパス> <リンクを張るパス>
ハードリンクはファイルにのみリンクを張ることができるが、シンボリックリンクはディレクトリにもリンクを貼ることができる。
ハードリンクとシンボリックリンクについて
ハードリンクについて
ハードリンクとは、広義には「あるinodeとそのファイルに付けれた名前との繋がり」のこと。 つまり「ハードリンクを張る」というのは同じinodeを参照する別名のファイルを作成するということである。
同じinodeを参照するのでファイルサイズなどのメタデータも同じで、参照しているデータブロックも同じ。
例えばこんなファイルを作成する。
❯ cat ~/Desktop/etc/hard-link-source.conf The file for hard link.
lsコマンドでinode番号などを確認すると
❯ ls -li ~/Desktop/etc/hard-link-source.conf 45031490 -rw-r--r-- 1 user-name staff 59 2 12 15:18 /Users/user-name/Desktop/etc/hard-link-source.conf
ハードリンクを/etc/
にhard.conf
という名前で張る。
sudo ln ~/Desktop/etc/hard-link-source.conf /etc/hard-link.conf
lsコマンドでハードリンクを確認してみる。
❯ ls -li /etc/hard-link.conf 45031490 -rw-r--r-- 2 user-name staff 59 2 12 15:18 /etc/hard-link.conf
このように同じinode番号を持った別名のファイルができている。
先ほども説明したように、同じinode番号を持っているので同じinodeを参照しており、参照しているデータブロックも同じなため実データも同じである。
片方でファイルを編集すると、もう片方からファイル内容を確認しても変更が反映されている。
例えば~/Desktop/etc/hard-link-source.conf
からファイルを編集して、/etc/hard-link.conf
を確認すると
❯ cat /etc/hard-link.conf The file for hard link. New line by hard-link-source.conf.
ハードリンクは同じinode番号を持った別名のファイルを作るので、どちらが親でどちらが子かなどの主従関係はない。
シンボリックリンクについて
シンボリックリンクを張ると、別のinode番号を持つファイルを作成する。
ファイルの実体はリンク元のファイルパスだが、そのファイルパスを参照するのでcat
コマンドなどでファイル内容を確認すると、リンク元のファイル内容と同じものが確認できる。
ファイルの実態はリンク元のファイルパスなのでファイル容量は非常に小さい。
以下のファイルを作成する。
❯ cat ~/Desktop/etc/symbolic-link-source.conf The file for symbolic link.
❯ ls -li ~/Desktop/etc/symbolic-link-source.conf 45029769 -rw-r--r-- 1 user-name staff 28 2 12 14:44 /Users/user-name/Desktop/etc/symbolic-link-source.conf
リンクを貼る。
❯ sudo ln -s ~/Desktop/etc/symbolic-link-source.conf /etc/symbolic-link.conf
❯ ls -li /etc/symbolic-link.conf 45029868 lrwxr-xr-x 1 root wheel 59 2 12 14:46 /etc/symbolic-link.conf -> /Users/user-name/Desktop/etc/symbolic-link-source.conf
このようにinode番号は異なる。 ハードリンクと同じように片方のファイルから変更を加えてももう片方から変更を確認することができる。
今度は/etc/symbolic-link.conf
から変更を加えて、~/Desktop/etc/symbolic-link-source.conf
を確認してみる。
❯ cat ~/Desktop/etc/symbolic-link-source.conf The file for symbolic link. New line by symbolic-link.conf.
その他の違い
削除時
移動時
その他
それぞれの用途
ハードリンクを利用している「.
」「..
」の仕組み
前述したようにディレクトリのハードリンクを張ることは基本的にできない。
しかしコンピューター内部の処理ではディレクトリのハードリンクが使われている。
カレントディレクトリを表す「.
」や1つ上のディレクトリを表す「..
」はディレクトリへのハードリンクである。
これらのハードリンクはディレクトリ作成時に作成される。
ディレクトリを作成して確認してみる。
❯ mkdir ~/test
作成したtest
というディレクトリの情報を確認してみる。
❯ ls -lid ~/test 45027781 drwxr-xr-x 2 user-name staff 64 2 12 13:53 /Users/user-name/test
user-nameの前の数字はハードリンクの数で、test
ディレクトリへのハードリンクの数は2
となっている。
つまり、test
ディレクトリをリンク元にしているハードリンクが2つあるということになる。
そもそもハードリンクとは、あるファイルのinodeとそのファイルにつけられた名前との繋がりのことなので、普通にファイルを作成するとそのファイルはハードリンクを1つ持つ。
ディレクトリの実体は、ディレクトリ配下のファイルのinode番号とファイル名のリストを格納したファイルなので、ディレクトリ自体がハードリンクとしてカウントされる。
もう1つ目はカレンとディレクトリ「.
」である。
❯ ls -lia ~/test total 0 45027781 drwxr-xr-x 2 user-name staff 64 2 12 13:53 . 362921 drwxr-xr-x+ 87 user-name staff 2784 2 12 15:18 ..
このようにディレクトリ作成によってカレントディレクトリ「.
」と1つ上のディレクトリ「..」が作成されている。
カレントディレクトリ「.
」のinode番号はtest
ディレクトリと同じになっていて、ハードリンクなのが分かる。
このようにディレクトリ作成時にはディレクトリそのものとカレントディレクトリ「.
」の2つによってハードリンクが2つカウントされる。
ちなみに同じくディレクトリ作成によって作成される1つ上のディレクトリ「..」は作成したディレクトリの1つ上のディレクトリへのハードリンクになっているので、test
ディレクトリの1つ上のディレクトリである~
のinode番号と同じになっている。
❯ ls -lid ~ 362921 drwxr-xr-x+ 87 user-name staff 2784 2 12 15:18 /Users/user-name
シンボリックの利用用途
例えば階層の違うファイルをGit管理したいときに使う。
元々Git管理されているのは~/service-name
だとして、設定ファイルは階層の違うディレクトリ/etc/hoge.conf
にあるとする。
/etc/hoge.conf
をGit管理下に置くには、まずGit管理下に設定ファイルをコピーする。
❯ cp /etc/hoge.conf ~/service-name/hoge.conf
コピーした内容を一旦コミットする。
次に元々あったファイルを削除してシンボリックリンクを張る。(conf
を参照しているサービス(Nginxとか)は再起動しないといけなそう)
❯ sudo rm /etc/hoge.conf ❯ sudo ln -s ~/service-name/hoge.conf /etc/hoge.conf
localhostについて
開発をしていると、ローカル環境で動作確認する際にlocalhostを使うことがよくある。 なんとなく考えずに使っていたので改めてまとめてみる。
IPアドレスについて
まずIPアドレスは32ビットの数字で構成され、1バイトずつに10進数でピリオドを区切り文字にして表記する。
例えばGoogleのIPアドレスは142.251.42.163
localhostについて
TCP/IPが有効なコンピューターでは自分自身を表す特別なIPアドレスとしてローカル・ループバック・アドレスというものがある。
一般的にループバック・アドレスは127.0.0.1
というIPアドレスが利用される。
ループバック・アドレスはlocalhost
という名前でも参照できる。
localhostはTCP/IPの概念。
localhostの確認
macOSであれば/private/etc/hosts
から確認できます。
❯ cat /private/etc/hosts ## # Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry. ## 127.0.0.1 localhost 255.255.255.255 broadcasthost ::1 localhost # Added by Docker Desktop # To allow the same kube context to work on the host and the container: 127.0.0.1 kubernetes.docker.internal # End of section
ジャーナリングファイルシステムについて
ジャーナリングシステムについて
ファイルへの書き込み中に遮断が発生しても、不整合が起きにくくするジャーナリングという仕組みがある。
これを利用したファイルシステムのことをジャーナリングファイルシステムという。 Linuxで使われているEXT4などはジャーナリングシステムの1つで、他にも多々ある。
ジャーナリングとは
ジャーナリングとは、実際のデータブロック(ファイル内のデータを保存する領域)に対して操作を行う前にジャーナルと呼ばれるログに変更内容を定期的に書き出しておくこと。 記録が完了してからジャーナル(ログ)を再生していくことで実際のデータを更新する。その後ジャーナルログを破棄してデータの一貫性を保つ。
これにより、データの整合性を保つことができる。 ExcelやWordを異常終了したときに復元できる機能のようなイメージ。
障害発生時の流れ
ジャーナルの記録が終了する前に異常終了した場合は、ジャーナル自体が不完全なので実データには変更が加えられず一貫性が保たれる。
ジャーナルを再生して実データに変更を加えている間に異常終了した場合、再起動時には未完了のジャーナルログの再生を再開するか、記録済みのジャーナルの操作を元に戻す操作を行うことによって一貫性が保たれる。