ふわっとテック日記

テック系のことをメインに書いていきます。go言語/typescript/javascriptが好きです。たまに趣味(筋トレ)の話や日常系の記事も書きたいな〜と思っています。

go言語での文字列変換

go言語での文字列の他データ型への変換についてさらっと書きます。

こちらの公式ドキュメントに言及、一部和訳しています。

go.dev


go言語の文字列(string型)は、rune型のスライス(rune)とbyte型のスライス(byte)にそれぞれ変換可能です。


rune型の実態はint32で、Unicodeのコードポイントを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

下記のように for range で文字列を繰り返し処理にかけると、rune型で一文字ずつ取得できます。

str := "あいうえお"
for _, s := range str {
    fmt.Printf("Type: %s, Value: %d\n", reflect.TypeOf(s), s)
}

/* OUTPUT:
Type: int32, Value: 12354
Type: int32, Value: 12356
Type: int32, Value: 12358
Type: int32, Value: 12360
Type: int32, Value: 12362
*/

Unicode文字一覧表 - instant tools

各文字の出力がUnicodeのコードポイントの数値に合致していることがわかります。



またbyte型の実態はuint8で、1バイトを10進数表記で表します。

// 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 = uint8

下記のように for 初期値; 条件; 変化式 で文字列を繰り返し処理にかけると、byte型で1バイトごとに取得できます。

str := "あいうえお"
for i := 0; i < len(str); i++ {
    fmt.Printf("Type: %s, Value: %d\n", reflect.TypeOf(str[i]), str[i])
}

/* OUTPUT:
Type: uint8, Value: 227
Type: uint8, Value: 129
Type: uint8, Value: 130
Type: uint8, Value: 227
Type: uint8, Value: 129
Type: uint8, Value: 132
Type: uint8, Value: 227
Type: uint8, Value: 129
Type: uint8, Value: 134
Type: uint8, Value: 227
Type: uint8, Value: 129
Type: uint8, Value: 136
Type: uint8, Value: 227
Type: uint8, Value: 129
Type: uint8, Value: 138
*/

このケースではバイトごとの繰り返しになるため、日本語のようなマルチバイト文字だと1文字ずつ抜き出せないことに注意してください。

rune型の場合はUnicodeコードポイントで1文字ごとに対応しているため、string(value)で文字列に変換できます。


数値をstring変換した場合は、その数値に対応するUnicodeコードポイントの文字に変換されます。 コードポイント範囲外の数値の場合は \ufffd に変換されます。

num1 := 12450
fmt.Println(string(num1))

/* OUTPUT:

*/

stringを介さずにruneとbyteを変換する場合はunicode/utf8パッケージのEncodeRuneDecodeRuneなどが使えるかと思います。

pkg.go.dev


余談ですが、公式ドキュメントのコード例に元横綱白鵬翔を発見しました。 ちょっと嬉しかったです。

パッケージ管理ツールrpm/yumことはじめ

今まで仕事でyum installでlinuxに色々なパッケージをインストールして使ってきたものの、正直仕組みの理解についてはかなりあやふやなままだったのである程度勉強してみました。

あくまでさわりの部分ですが、本記事はその備忘録ですd( ̄  ̄)

rpm/yumとは

rpm, yumRed Hat系のLinuxディストリビューションで使用されるパッケージ管理、及びそのコマンドラインツールのことです。

rpm依存関係管理機能を持たないのに対し、
yum依存関係管理機能を持つという違いがあります。


yumrpm形式のパッケージファイルを複数まとめて管理でき、なおかつ依存関係もイイ感じにしてくれるのでほとんどのケースではyumコマンドを用いてパッケージ管理することになると思います。

rpm

パッケージ管理ツール、コマンドラインツール、及びパッケージファイルの形式のこと。

Red Hat系のLinuxではrpm形式のパッケージファイルをインストールするなどして使用します。

rpmコマンドを用いて個々のパッケージファイルの操作などが行えます。ただしパッケージ間の依存関係は管理してくれません。

rpmbuildコマンド

rpmパッケージのビルドをするためのコマンドです。

SPECファイルと呼ばれるファイルを参照し、rpmパッケージを作成してくれます。 SPECファイルはパッケージを作成するための手順を記載したレシピのようなものです。

  • -bオプション : SPECファイルを指定
  • -baオプション: RPMファイルとSRPMファイルを作成
  • -bbオプション: RPMファイルのみ作成
  • -bsオプション: SRPMファイルのみ作成

SRPMファイル

xxx.src.rpmの形式。これをもとにRPMのリビルドができます。

RPM作成に必要なSPECファイルなどのアーカイブとなっています。

SRPMファイルを用いたビルドはこのようにできます。

rpmbuild --rebuild xxx.src.rpm

その他rpmコマンド

rpm -ivh xxxxxxxxx.rpm

パッケージをインストールします。

rpm -qa

インストール済みのパッケージ一覧を表示します。

rpm -qi xxxxxxxxx.rpm

パッケージファイルの情報を表示します。

rpm -ql xxxxxxxxx.rpm

パッケージに内包されるファイル一覧を表示します。

yum

数多あるパッケージファイルをまとめて管理、依存関係もよしなにイイ感じにしてくれる便利なツールです。

リポジトリ(後述)ベースでパッケージを管理しており、リポジトリ自体も多く存在します。また自分で作成することも可能です。

操作もリポジトリベースで行われ、リポジトリからrpmファイルを検索したりインストールしたりといった操作になります。

リポジトリ

数あるパッケージを保管、配布している場所のことです。

/etc/yum.conf/etc/yum.repos.d/xxx.repo ファイルでリポジトリを管理します。


yum repolistコマンドによってリポジトリの表示ができます。ステータス無効になっているリポジトリのパッケージは基本的には操作できません。

repoファイルのリポジトリの中でenabled=0となっているリポジトリは無効となります。

[samplerepo]
name=sample-repo
baseurl=http://sample/repo
enabled=0

上記の例のように無効となっているリポジトリも、次のようにコマンドでリポジトリを指定することでパッケージのインストールなどが可能となります。

yum --enablerepo=samplerepo install xxxxxxx

createrepoコマンド

リポジトリを作成できるコマンドです。

rpmファイルを集めたディレクトリパスを指定し、対象ディレクトリのファイルでリポジトリを作成します。

# pwd
/tmp/addrepo

# ls -l
~~~~~... xxxxxxxxxxxxxxx.rpm
~~~~~... yyyyyyyyyyyyyy.rpm
~~~~~... zzzzzzzzzzzzzz.rpm

このコマンドにより対象ディレクトリにrepodataという、リポジトリメタデータ情報を格納するディレクトリが作成されます。

# ls -l
~~~~~... xxxxxxxxxxxxxxx.rpm
~~~~~... yyyyyyyyyyyyyy.rpm
~~~~~... zzzzzzzzzzzzzz.rpm
~~~~~... repodata

createrepoコマンド実行後は、対象ディレクトリを上述のrepoファイルに記述し、yumコマンドにリポジトリとして認識させて使用する、という流れになります。

# vim /etc/yum.repos.d/add_repo.repo
[add_repo]
name=Add Repo
baseurl=file:///tmp/addrepo

その他yumコマンド

yum repolist [all/enabled/disabled]

リポジトリの一覧を表示します。

  • オプション未指定で有効のみ表示
  • allで有効/無効全て表示
  • enabledで有効のみ表示
  • disabledで無効のみ表示

となります。

yum list [installed/available/extras]

パッケージ一覧を表示します。

  • オプション未指定で全てのパッケージ表示
  • installedでインストール済みのみ表示
  • availableで未インストールの利用可能パッケージのみ表示
  • extarsで利用不可のパッケージのみ表示

となります。

Go言語のコンパイル/アセンブリ周りについて

Go言語はコンパイル言語なのでソースコードコンパイルしてバイナリを実行するという形になるのですが、 コンパイル → 実行ファイルまでの流れをよく理解できていなかったのでサラッと学んでみました。

こちらの記事を要所要所で翻訳しつつ、噛み砕いたものを記事にしています。

go.dev

間違い等あればご指摘いただければ幸いですm( )m

コンパイル

ソースをコンパイルしてgoアセンブリを作成し、それをアセンブラが解釈してオブジェクトファイルを作成します。

最終的にはオブジェクトファイルやアーカイブライブラリをリンカーがまとめてリンクし、実行可能バイナリファイルを作成するという手順です。

アセンブリを覗いてみる

以下のようなgoファイルを用意します

package calculate

func add(m int, n int) int {
        return m + n
}

func sum(m int, n int) int {
        return m - n
}

以下サンプルでコンパイルしてオブジェクトファイルを作ります。

ソースコードアセンブリ → オブジェクトファイルという作成の流れを踏むのですが、 -Sオプションによって標準出力にアセンブリが出力されます。

go tool compile -S add.go
↓
"".add STEXT size=16 args=0x10 locals=0x0 funcid=0x0 align=0x0 leaf
    0x0000 00000 (add.go:3) TEXT    "".add(SB), LEAF|NOFRAME|ABIInternal, $0-16
    0x0000 00000 (add.go:3) FUNCDATA    ZR, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (add.go:3) FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (add.go:3) FUNCDATA    $5, "".add.arginfo1(SB)
    0x0000 00000 (add.go:3) FUNCDATA    $6, "".add.argliveinfo(SB)
    0x0000 00000 (add.go:3) PCDATA  $3, $1
    0x0000 00000 (add.go:4) ADD R1, R0, R0
    0x0004 00004 (add.go:4) RET (R30)
    0x0000 00 00 01 8b c0 03 5f d6 00 00 00 00 00 00 00 00  ......_.........
"".sum STEXT size=16 args=0x10 locals=0x0 funcid=0x0 align=0x0 leaf
    0x0000 00000 (add.go:7) TEXT    "".sum(SB), LEAF|NOFRAME|ABIInternal, $0-16
    0x0000 00000 (add.go:7) FUNCDATA    ZR, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (add.go:7) FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (add.go:7) FUNCDATA    $5, "".sum.arginfo1(SB)
    0x0000 00000 (add.go:7) FUNCDATA    $6, "".sum.argliveinfo(SB)
    0x0000 00000 (add.go:7) PCDATA  $3, $1
    0x0000 00000 (add.go:8) SUB R1, R0, R0
    0x0004 00004 (add.go:8) RET (R30)
    0x0000 00 00 01 cb c0 03 5f d6 00 00 00 00 00 00 00 00  ......_.........
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
    0x0000 63 61 6c 63 75 6c 61 74 65                       calculate
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
    0x0000 01 00 00 00 00 00 00 00                          ........
"".add.arginfo1 SRODATA static dupok size=5
    0x0000 00 08 08 08 ff                                   .....
"".add.argliveinfo SRODATA static dupok size=2
    0x0000 00 00                                            ..
"".sum.arginfo1 SRODATA static dupok size=5
    0x0000 00 08 08 08 ff                                   .....
"".sum.argliveinfo SRODATA static dupok size=2
    0x0000 00 00           

以下のようにgo buildコマンドに-gcflags -Sオプションをつけても、同様にアセンブリが出力されます。

go build -gcflags -S add.go

goアセンブリには色々なディレクティブが存在するわけですが、以下の2つはガベージコレクタが使用するもので、コンパイラによってアセンブリに挿入されます。

  • FUNCDATA
  • PBDATA

またアーキテクチャによってアセンブリのシンボルは異なるのですが、以下の仮想レジスタを指すシンボルは全アーキテクチャで統一されています。

  • FP: Frame pointer: arguments and locals.
  • PC: Program counter: jumps and branches.
  • SB: Static base pointer: global symbols.
  • SP: Stack pointer: the highest address within the local stack frame.

goアセンブリは基本的に疑似命令で構成されています。

命令にはアセンブル後の機械語と1対1で対応しているものもあれば、直接対応していないものもあります。

関連コマンドetc

go tool compile

goパッケージをコンパイルしてオブジェクトファイルを生成します。 -Sオプションで、生成過程のアセンブリを標準出力に出すことができます。

compile command - cmd/compile - Go Packages

オブジェクトファイルやアーカイブライブラリをリンカーに渡し、実行ファイルを生成(リンク)します。

link command - cmd/link - Go Packages

go tool nm

オブジェクトファイル、アーカイブライブラリ、実行ファイルで定義、使用されているシンボルのリストを返します。

アドレス / 型 / シンボル名という書式になっています。

未定義シンボル(U)のアドレスは省略されます。

nm command - cmd/nm - Go Packages

go tool objdump

実行ファイルを逆アセンブルします。

出力されるのはリンク後の実行ファイルのアセンブリのため、オブジェクトファイルをgo tool compile -Sした時とは別の結果となります。

Go言語でのジェネリクス(1.18から)

Go言語ではバージョン1.18から待望のジェネリクスが実装されたのですが、

普段の業務でジェネリクスを使うことがあまりなく仕様をよく分かっていなかったので

備忘録的に残しておこうと思います。

ジェネリクスとは

関数の引数となるデータ型が複数考えられる時、考えられる全てのデータ型を受け入れるように関数を実装できる仕組みのこと。

関数呼び出し時に引数と一緒に引数の型も指定する。

関数定義時点では引数の型が分からないといった場合に、柔軟性のある関数を実装することができます。

Goでの実装例

例えば下のSumInt関数とSumStringは、それぞれ引数の2つのint, stringを足し合わせた結果を返す関数です。

当然、SumIntの引数にstring型は指定できないし、SumStringの引数にint型は指定できません。

func SumInt(num1 int, num2 int) int {
    return num1 + num2
}
    
func SumString(str1 string, str2 string) string {
    return str1 + str2
}

ここで、引数の型は関数実行時にしかわからないけど、とにかく引数の2つの値を足し合わせた結果を返す関数を定義したい、とします。

ここでジェネリクスを使った関数を定義します。

func SumIntOrString[T int | string](arg1, arg2 T) T {
    return arg1 + arg2
}

ここで定義したTは型パラメータと呼ばれるもので、実行時に取りうるTの型を表します。

この関数では2つの引数と返り値の型がTとなります。

int | stringはユニオンと呼ばれるもので、intかstringのどちらかになれるよ、という意味です。そのものずばり、型制約と呼ばれたりします。

この関数を実行してみましょう。

func main() {
    res := SumIntOrString[int](4, 5)
    fmt.Println(res)  // 9

    res := SumIntOrString[string]("qwe", "rty")
    fmt.Println(res) // qwerty
}

こんな感じで、関数名の直後に型引数[]で型パラメータの型を決めてあげます。

また、引数が型パラメータで定義されている場合は関数実行時の型引数は省略できます。コンパイラが型パラメータを参考に取りうる型を推測してくれるからです。

func main() {
    res := SumIntOrString(4, 5) // 型引数なしでもOK
    fmt.Println(res)  // 9

    res := SumIntOrString("qwe", "rty") // 型引数なしでもOK
    fmt.Println(res) // qwerty
}

引数が型パラメータで定義されていない場合(引数自体がない場合など)は、型引数は省略できないのでご注意を。


ジェネリクス関数を複数定義するときに毎回同じ型制約を指定する時などは、いちいち同じ型制約を全ての関数に書いていると煩雑です。

そんな時はインターフェースで型制約を定義して使いまわしてしまいましょう。

type DefaulOption interface {
    int | string
}

func SumIntOrString[T DefaulOption](arg1, arg2 T) T {
    return arg1 + arg2
}


comparableやanyなど、組み込み済みの型制約インターフェースも使用できます。

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }

Underlying Type

型パラメータの型にチルダが付いているパターン。このチルダはUnderlying Typeと呼ばれます。

func SumIntOrString[T ~int | ~string](arg1, arg2 T) T {
    return arg1 + arg2
}

これはUnderlying Typeがint/stringの型ならOKという意味です。

例えばこんな感じです。

func SumIntOrString[T int | string](arg1, arg2 T) T {
    return arg1 + arg2
}

func SumUnderlyingIntOrString[T ~int | ~string](arg1, arg2 T) T {
    return arg1 + arg2
}

type NewInt int // NewIntはUnderlying Typeがintの型

func main() {
    var num1 NewInt = 3
    var num2 NewInt = 5

    fmt.Println(SumIntOrString(num1, num2)) // これはエラー

    fmt.Println(SumUnderlyingIntOrString(num1, num2)) // これはセーフ
}

Underlying Typeの分かりやすい説明記事を発見したのでご参考までに。