ただの技術メモ

個人備忘録

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-8ASCII互換の符号化方式で、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」と改行文字のコードポイントであるため意図した文字列を保存することができない。

unicode-table.com

unicode-table.com

unicode-table.com

このようなことが起こらないように、一定のルールでバイト列に変換する方式を符号化方式と言い、UTF-8UTF-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するコマンド。

つまりオプションを指定しないと、rmunlinkは同じ。

user-name:~$rm --help
Usage: rm [OPTION]... [FILE]...
Remove (unlink) the FILE(s).

rmコマンドは-rfオプションなど色々できてしまうので、リンクを削除するときは安全にunlinkコマンドを使おう。

そもそもrmunlinkはどういう挙動なのか気になったのでまとめておく。

挙動

オプションを指定しない場合、rmunlinkは同じ挙動なので、適宜rm=unlinkと読み替えてもらえれば良さそう。

rmコマンドは該当するファイルのinodeの参照カウントをデクリメント(1つ上のディレクトリが持つ該当ファイルのレコードを削除)する。

inodeは参照カウントを持っているらしい。(知らなかった)

つまり、直接的にrmコマンドによってinodeが解放されるわけではない。 (ハードリンクを張って同じinodeを参照しているファイル2つがあるとき、片方を削除してももう1方には影響がないことから考えても、inodeが解放されることはないことが分かる)

参照カウントが0になったinodeは解放される。

しかし、ファイルの実体の削除は対応するinodeを参照するプロセスがなくなるまで遅延される。

挙動の確認

test.txtを作成する。

user-name:~$ touch test.txt

もう1つターミナルを開いてvimtest.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を閉じるとファイルを参照しているプロセスがなくなるのでファイルの実体も削除される。

参考

linuxjm.osdn.jp

stackoverflow.com

qiita.com

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進数でピリオドを区切り文字にして表記する。 例えばGoogleIPアドレス142.251.42.163

localhostについて

TCP/IPが有効なコンピューターでは自分自身を表す特別なIPアドレスとしてローカル・ループバック・アドレスというものがある。 一般的にループバック・アドレスは127.0.0.1というIPアドレスが利用される。 ループバック・アドレスはlocalhostという名前でも参照できる。 localhostTCP/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を異常終了したときに復元できる機能のようなイメージ。

障害発生時の流れ

  • ジャーナルの記録が終了する前に異常終了した場合は、ジャーナル自体が不完全なので実データには変更が加えられず一貫性が保たれる。

  • ジャーナルを再生して実データに変更を加えている間に異常終了した場合、再起動時には未完了のジャーナルログの再生を再開するか、記録済みのジャーナルの操作を元に戻す操作を行うことによって一貫性が保たれる。