いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-680 (通常ファイルでタイムアウトを発生させる)(os.Open, os.File, os.Pipe, netpoller, epoll)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。

Goのファイル処理で「タイムアウトを設定出来ないの?」ってたまに聞かれます。

この場合のファイルとは通常ファイルのことです。普通に os.Open とかで開くファイル。

os.FileにはSetDeadline, SetReadDeadline, SetWriteDeadline メソッドがあります。

通常ファイルの場合でも問題なく呼び出しを行うことが出来ます。が、何も起こりません。

これは、Goの内部では netpoller という処理が各OSごとに非同期I/O処理を移譲するようになってて

たとえば linux の場合は epoll となります。つまり、poll可能なファイルディスクリプタしかタイムアウト設定出来ません。

poll 可能なディスクリプタって何があるの?ってなりますが、基本は ソケット、パイプ、TTY です。ここに通常ファイルは入っていません。

んじゃ、絶対無理なのかというとやり方は沢山あります。読み込みを非同期でやって所定時間経過でタイムアウトとして判定したりとか。

その中の一つにパイプを使ってタイムアウト設定するというものがあります。個人的に結構使っているやり方です。知っていると、いつか使えるときが来るかもしれません。

サンプル

main.go

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "log"
    "os"
    "time"
)

var (
    ErrIoTimeout = errors.New("タイムアウト発生")
)

func main() {
    log.SetFlags(log.Lmicroseconds)
    log.SetOutput(os.Stderr)

    var (
        rootCtx  = context.Background()
        ctx, cxl = context.WithTimeout(rootCtx, 5*time.Second)
        err      error
    )
    defer cxl()

    log.Println("start")
    if err = run(ctx); err != nil {
        if errors.Is(err, ErrIoTimeout) {
            log.Println(err)
            return
        }

        panic(err)
    }
}

func run(pCtx context.Context) error {
    //
    // os.FileにはSetDeadlineメソッドがあり、設定することでタイムアウトを仕込むことが出来る。
    // しかし、このメソッドは通常ファイルの場合は何も起こらない。(タイムアウトが発生しない)
    // 理由は通常ファイルがpoll可能なFDでは無いため。
    //
    // Goは内部で netpoller (Linuxの場合は epoll, macOSの場合は kqueue) を用いて非同期I/Oを実現している。
    //
    // - https://go.dev/src/runtime/netpoll.go
    // - https://morsmachine.dk/netpoller.html
    // - https://internals-for-interns.com/posts/go-netpoller/
    //
    // なので、poll可能なFDしかタイムアウトを設定出来ない。
    //
    // ソケット  : 可能
    // パイプ   : 可能
    // TTY     : 可能
    // 通常ファイル: 不可
    //
    // 通常ファイルは poll 出来ないFDなので、SetDeadlineメソッドの呼び出しは出来るが何も起こらない。
    // いろいろなやり方があるが、パイプを使ってデータを流し、タイムアウトを設定するというやり方もある。
    //

    // (1) まずファイルを普通に開く
    const (
        fpath = "main.go" // このファイル
    )
    var (
        file *os.File
        err  error
    )
    if file, err = os.Open(fpath); err != nil {
        return fmt.Errorf("os.Open(%s): %w", fpath, err)
    }
    defer file.Close()

    // (2) パイプを生成
    var (
        pr *os.File
        pw *os.File
    )
    if pr, pw, err = os.Pipe(); err != nil {
        return fmt.Errorf("os.Pipe(): %w", err)
    }
    defer pr.Close()

    // (3) ファイルデータをパイプに流す
    //     w.Close() の呼び出しで pr 側にEOFが返る
    //
    // 実際に実行すると、本サンプルの場合は io.Copy は即座に完了する。
    // これは パイプ が、カーネル空間のバッファを利用しており、そのデフォルト値に収まっているから。
    //
    // 通常のLinuxの場合、Linuxカーネルのパイプバッファ初期値は実際には16KB (4096バイトが4ページ分)となっている。
    // この値を超えるデータが流れている場合は、当然 pr 側が読み取りを行うまでブロックされる。
    //
    // pythonで確認するのが楽
    //   import os,fcntl
    //   r,w = os.pipe()
    //   print(fcntl.fcntl(w, 1032)) # 1032 == F_GETPIPE_SZ
    go func(r io.Reader, w io.WriteCloser) {
        defer w.Close()
        io.Copy(w, r)
        log.Println("pw: io.Copy()")
    }(file, pw)

    // (4) パイプから読み取りながら、所定時間後にタイムアウト発生させる
    //
    // サンプルなので500msごとに1文字ずつ読み取っていき確実にタイムアウトするようにしている
    var (
        timeout  = 3 * time.Second
        interval = 500 * time.Millisecond
        tick     = time.NewTicker(interval)
        buf      = make([]byte, 1)
    )
    defer tick.Stop()

    // タイムアウト設定
    //   パイプは poll 可能なので SetDeadline が有効
    //   SetDeadline/SetReadDeadline/SetWriteDeadlineに現在時刻を設定することで即座にタイムアウトとなる
    go func(f *os.File, timeout time.Duration) {
        time.Sleep(timeout)
        f.SetDeadline(time.Now())
        log.Println("pr: SetDeadline()")
    }(pr, timeout)

    for {
        clear(buf)

        select {
        case <-pCtx.Done():
            return pCtx.Err()
        case <-tick.C:
            if _, err = pr.Read(buf); err != nil {
                if errors.Is(err, io.EOF) {
                    return nil
                }

                if errors.Is(err, os.ErrDeadlineExceeded) {
                    // タイムアウト
                    return fmt.Errorf("%w (%w)", ErrIoTimeout, err)
                }

                return err
            }

            os.Stdout.Write(buf)
        }
    }
}

Taskfile.yml

version: '3'

tasks:
  default:
    cmds:
      - go build -o app main.go
      - defer: rm -f ./app ./out.txt
      - ./app 2>out.txt; echo
      - cat out.txt
    ignore_error: true
    silent: true

実行

$ task
packa
16:45:50.924541 start
16:45:50.924720 pw: io.Copy()
16:45:53.924922 pr: SetDeadline()
16:45:53.925109 タイムアウト発生 (read |0: i/o timeout)

参考情報

go.dev

morsmachine.dk

internals-for-interns.com

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

sql-tap (PostgreSQLやMySQLのSQLトラフィックをリアルタイムで監視・表示できるTUIツール)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。忘れないうちにメモメモ。。。

実務でもDBに発行されているクエリを監視することがあったりするので、素晴らしいツールですね。

Goで作成されているツールなので、go install でもインストール可能。

作成してくださった方に感謝。

zenn.dev

github.com

参考情報


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-679 (cgoメモ-39)(Cの関数呼び出しで戻り値とerrorが受け取れる)

関連記事

Goメモ-502 (cgoメモ-01)(cgoヘッダ) - いろいろ備忘録日記

Goメモ-506 (cgoメモ-02)(cgoヘッダ) - いろいろ備忘録日記

Goメモ-507 (cgoメモ-03)(C.int) - いろいろ備忘録日記

Goメモ-508 (cgoメモ-04)(C言語の構造体) - いろいろ備忘録日記

Goメモ-509 (cgoメモ-05)(C.CString)(Cの文字列) - いろいろ備忘録日記

Goメモ-510 (cgoメモ-06)(C.GoString)(Cの文字列をGoの文字列へ) - いろいろ備忘録日記

Goメモ-511 (cgoメモ-07)(C.CBytes)([]byteをCのバイト列に) - いろいろ備忘録日記

Goメモ-512 (cgoメモ-08)(C.GoBytes)(Cのバイト列をGoの[]byteへ) - いろいろ備忘録日記

Goメモ-514 (cgoメモ-09)(C.GoStringN)(C.GoStringのサイズ指定版) - いろいろ備忘録日記

Goメモ-515 (cgoメモ-10)([]byteを(void *)へ変換) - いろいろ備忘録日記

Goメモ-516 (cgoメモ-11)([]byteを(char *)へ変換) - いろいろ備忘録日記

Goメモ-518 (cgoメモ-12)(Cのmallocをcgo経由で呼び出し) - いろいろ備忘録日記

Goメモ-519 (cgoメモ-13)(ポインタ演算) - いろいろ備忘録日記

Goメモ-520 (cgoメモ-14)(Goの関数をCの世界に公開 (export)) - いろいろ備忘録日記

Goメモ-522 (cgoメモ-15)(Goでsoファイルを作成してC言語から呼び出し) - いろいろ備忘録日記

Goメモ-525 (cgoメモ-16)(C側にて関数ポインタを引数に要求する関数にGo側で定義した関数を設定) - いろいろ備忘録日記

Goメモ-526 (cgoメモ-17)(cgoとdlopen関数を使って既存ライブラリの呼び出しをフックする) - いろいろ備忘録日記

Goメモ-527 (cgoメモ-18)(cgoを利用している場合のinit関数について) - いろいろ備忘録日記

Goメモ-528 (cgoメモ-19)(cgoを利用して作成したsoファイル経由でのinit関数の呼び出し) - いろいろ備忘録日記

Goメモ-529 (cgoメモ-20)(C言語のNULLをcgoから渡す) - いろいろ備忘録日記

Goメモ-530 (cgoメモ-21)(CGOヘッダーで指定出来るCFLAGS, LDFLAGS) - いろいろ備忘録日記

Goメモ-531 (cgoメモ-22)(CGOで利用するコンパイラを変更する) - いろいろ備忘録日記

Goメモ-532 (cgoメモ-23)(CGOヘッダで使えるSRCDIR変数) - いろいろ備忘録日記

Goメモ-534 (cgoメモ-24)(C側の構造体にて固定要素数の文字配列をGo側で文字列に変換) - いろいろ備忘録日記

Goメモ-535 (cgoメモ-25)(cgo.Handleを用いてCとGoの間で値をやり取りする) - いろいろ備忘録日記

Goメモ-536 (cgoメモ-26)(C側の構造体をGo側で利用する) - いろいろ備忘録日記

Goメモ-537 (cgoメモ-27)(Go側でCの構造体のサイズを知る方法) - いろいろ備忘録日記

Goメモ-539 (cgoメモ-28)(Go側でCの文字列リスト(**char)を扱う) - いろいろ備忘録日記

Goメモ-623 (cgoメモ-29)(memcpyの呼び出し) - いろいろ備忘録日記

Goメモ-627 (cgoメモ-30)(Goでビルドした静的ライブラリのC言語からの利用) - いろいろ備忘録日記

Goメモ-628 (cgoメモ-31)(Cの関数からGoの関数をコールバックする) - いろいろ備忘録日記

Goメモ-629 (cgoメモ-32)(Cの関数からGoの関数をコールバックする(2)) - いろいろ備忘録日記

Goメモ-630 (cgoメモ-33)(LD_PRELOAD を利用したモック) - いろいろ備忘録日記

Goメモ-641 (cgoメモ-34)(C側で定義された配列の操作)(アクセサ関数を用意) - いろいろ備忘録日記

Goメモ-644 (cgoメモ-35)(マクロ定数は参照できるけどマクロは使えない) - いろいろ備忘録日記

Goメモ-654 (cgoメモ-36)(structs.HostLayout) - いろいろ備忘録日記

Goメモ-660 (cgoメモ-37)(go-md4c, MD4Cのラッパーライブラリ) - いろいろ備忘録日記

Goメモ-661 (cgoメモ-38)(cgoのランタイムメトリクス)(/cgo/go-to-c-calls:calls) - いろいろ備忘録日記

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。

これまでのサンプルは以下のリポジトリにアップしています。

github.com

cgo では C 関数を多値返却形式で呼び出すと、Go コンパイラが呼び出し直後に errno を読み取り、error インターフェースとして返します。

Go references to C に以下の記載があります。

Any C function (even void functions) may be called in a multiple assignment context to retrieve both the return value (if any) and the C errno variable as an error (use _ to skip the result value if the function returns void).

n, err = C.sqrt(-1)
_, err := C.voidFunc()
var n, err = C.sqrt(1)

内部的には以下の型変換が行われています。

errno (C の int)
  → syscall.Errno (Go の uintptr、error インターフェースを実装)
  → error インターフェースとして返される

注意点として

err != nil だけで判定してはいけない。

Cの関数が成功しても errno が前回呼び出しの残留値のままの場合があります。(実務では結構あったりしますね)

正しいパターンは「戻り値で成否を判断し、失敗時に err を参照する」です。

通常のGoでのセオリーに反しますが、このあたりはCと同じです。

サンプル

main.go

package main

/*
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

// open(2) は glibc で varargs 宣言されている。そのため cgo から直接呼べないので
// 固定引数の static inline ラッパーを用意して回避する。
// macOSの場合は、openが固定引数で見えるので、この問題は発生しない。
static inline int go_open(const char *p, int flags) {
   return open(p, flags);
}
*/
import "C"
import (
    "context"
    "errors"
    "fmt"
    "log"
    "slices"
    "syscall"
    "unsafe"
)

const (
    exists    = "README.md"
    notExists = "NOT-EXISTS.md"
)

func main() {
    log.SetFlags(0)

    var (
        ctx = context.Background()
        err error
    )
    if err = run(ctx); err != nil {
        log.Println(err)
    }
}

func run(_ context.Context) error {
    var (
        p1 = C.CString(exists)
        p2 = C.CString(notExists)
    )
    defer C.free(unsafe.Pointer(p1))
    defer C.free(unsafe.Pointer(p2))

    type (
        loopVal struct {
            fpath string
            cpath *C.char
        }
    )
    var (
        fd   C.int
        err  error
        vals = []loopVal{{exists, p1}, {notExists, p2}}
    )
    for v := range slices.Values(vals) {
        // cgo経由でCの関数を呼び出す場合、Goの多値返却の仕組みにより
        // 本来の戻り値にプラスerrorが返ってくる。この error には、C側のerrnoの値がセットされる。
        //
        // errも返ってきているが C の関数を扱う場合は err != nil だけで判定するのは危険。
        // Cのセオリー通り、まず戻り値で判定し、その後で必要であれば err を使う。
        // errが存在する場合 syscall.Errno となる。(呼び出しが成功している場合、通常nilとなる)
        fd, err = C.go_open(v.cpath, C.int(syscall.O_RDONLY))
        if fd == -1 {
            // err は、実際には syscall.Errno となっている
            // 今回の場合では、ファイルが存在しないため、syscall.ENOENT となる。
            var (
                errno   = err.(syscall.Errno)
                eno     = uintptr(errno)
                isNOENT = errors.Is(err, syscall.ENOENT)
            )
            return fmt.Errorf("NG: open(%s): fd=%d (%w)(0x%x:ENOENT=%v)", v.fpath, fd, err, eno, isNOENT)
        }

        C.close(fd)

        log.Printf("OK: open(%s): fd=%d (%v)", v.fpath, fd, err)
    }

    return nil
}

実行

$ task
OK: open(README.md): fd=5 (<nil>)
NG: open(NOT-EXISTS.md): fd=-1 (no such file or directory)(0x2:ENOENT=true)

ちゃんと errno(syscall.ENOENT) になってますね。

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-678 (eBPFについてのリソース)(libbpf, libbpf-bootstrap, cilium/ebpf)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。

そろそろ、ちゃんと eBPF 覚えようと思っているので、勉強用のリソースをメモメモ。。。

以下のリポジトリの eBPF 部分をちゃんと理解したいというのが目標。

最終的には cilium/ebpf でいろいろ遊べるようになりたい。

github.com

参考情報

ebpf.io

ebpf.io

www.oreilly.co.jp

github.com

eunomia.dev

nakryiko.com

nakryiko.com

github.com

nakryiko.com

github.com

ebpf-go.dev

zenn.dev

zenn.dev

note.com

zebian.hatenablog.com

edgedelta.com

labs.iximiuz.com

oneuptime.com

dev.to

zenn.dev

www.youtube.com

www.youtube.com

networkop.co.uk

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-677 (devlights/pkgdoc2md)(Goの標準ライブラリのパッケージページをMarkdownに変換)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。

前回見つけた html-to-markdown ライブラリを使って、Goの標準ライブラリの特定パッケージの内容をマークダウンにするツールを自分用に作成。ついでなので、ここにもメモしておこうと思いました。Claudeとかに渡す際にMarkdownで渡したほうが楽なので作成しただけです。

github.com

まあ、内部でHTTPリクエストを発行して、html-to-markdownライブラリを呼び出しているだけなので、自分自身で何か難しいことは何もしてないツールです。素晴らしい標準ライブラリと外部ライブラリに感謝。

html-to-markdownですが、最新版であるv2ではGFMプラグインが無くなっていたので、意図的に v1.6.0 を使っています。

ソース

main.go

package main

// pkg.go.dev の特定パッケージページをMarkdown形式に変換して出力するCLIツール。
// LLMへのアップロード用途を想定しているため、ナビゲーションやフッターなどの
// 不要なHTML要素を除去し、ドキュメント本文のみを抽出する。

import (
    "context"
    "flag"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strings"
    "time"

    md "github.com/JohannesKaufmann/html-to-markdown"
    "github.com/JohannesKaufmann/html-to-markdown/plugin"
)

const (
    pkgBaseURL = "https://pkg.go.dev"
)

type Args struct {
    pkg     string
    timeout int
    output  string
    debug   bool
}

var (
    args Args

    appLog = log.New(os.Stdout, "", 0)
    errLog = log.New(os.Stderr, "[ERROR] ", log.Lmicroseconds)
    dbgLog = log.New(os.Stdout, "[DEBUG] ", log.Lmicroseconds)
)

func init() {
    flag.StringVar(&args.pkg, "pkg", "", "変換対象のパッケージパス (例: net/http, encoding/json) [必須]")
    flag.IntVar(&args.timeout, "timeout", 30, "HTTPリクエストのタイムアウト秒数")
    flag.StringVar(&args.output, "output", "", "出力先ファイルパス (省略時はstdout)")
    flag.BoolVar(&args.debug, "debug", false, "デバッグログを有効にする")
}

func main() {
    flag.Parse()

    if !args.debug {
        dbgLog.SetOutput(io.Discard)
    }

    if args.pkg == "" {
        flag.Usage()
        errLog.Println("パッケージパスは必須: -pkg フラグを指定してください")
        return
    }

    var (
        ctx = context.Background()
        err error
    )
    if err = run(ctx); err != nil {
        errLog.Panic(err)
    }
}

func run(pCtx context.Context) error {
    var (
        ctx, cxl = context.WithCancel(pCtx)
        err      error
    )
    defer cxl()

    var (
        pkgUrl   = pkgBaseURL + "/" + strings.TrimLeft(args.pkg, "/")
        timeout  = time.Duration(args.timeout) * time.Second
        html     string
        body     string
        markdown string
    )
    dbgLog.Printf("取得開始: %s", pkgUrl)
    {
        if html, err = fetch(ctx, pkgUrl, timeout); err != nil {
            return fmt.Errorf("HTMLの取得に失敗しました: %w", err)
        }
    }
    dbgLog.Printf("HTML取得完了: %d bytes", len(html))
    {
        if body, err = extract(html); err != nil {
            dbgLog.Printf("本文抽出失敗、HTML全体を使用します: %v", err)
            body = html
        }
    }
    dbgLog.Printf("本文抽出完了: %d bytes", len(body))
    {
        if markdown, err = convert(body, pkgUrl); err != nil {
            return fmt.Errorf("Markdownへの変換に失敗しました: %w", err)
        }
    }
    dbgLog.Printf("Markdown変換完了: %d bytes", len(markdown))
    {
        if err = write(markdown, args.output); err != nil {
            return fmt.Errorf("出力の書き込みに失敗しました: %w", err)
        }

        appLog.Printf("出力: %s", args.output)
    }
    dbgLog.Printf("完了: Markdown %d bytes 出力", len(markdown))

    return nil
}

// fetch は指定URLのHTMLを取得して文字列として返す。
func fetch(ctx context.Context, url string, timeout time.Duration) (string, error) {
    var (
        client = &http.Client{
            Timeout: timeout,
        }
        req *http.Request
        err error
    )
    if req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, nil); err != nil {
        return "", fmt.Errorf("リクエスト生成に失敗しました: %w", err)
    }

    req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; pkgdoc2md/1.0)") // 一般的なブラウザを模倣
    req.Header.Set("Accept", "text/html,application/xhtml+xml")

    var (
        resp *http.Response
    )
    if resp, err = client.Do(req); err != nil {
        return "", fmt.Errorf("HTTPリクエストに失敗しました: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("HTTPステータスエラー: %d %s", resp.StatusCode, resp.Status)
    }

    const (
        maxBodySize = 10 * 1024 * 1024
    )
    var (
        r    = io.LimitReader(resp.Body, maxBodySize)
        body []byte
    )
    if body, err = io.ReadAll(r); err != nil {
        return "", fmt.Errorf("レスポンスボディの読み取りに失敗しました: %w", err)
    }

    return string(body), nil
}

// extract はpkg.go.devのHTMLから、ドキュメント本文部分のHTMLを抽出する。
//
// 抽出対象の要素:
//   - <main> タグの内容 (主要コンテンツ領域)
func extract(htmlContent string) (string, error) {
    // <main>タグの内容を単純な文字列探索で抽出する。
    // ネストしたmainタグがある場合は誤動作する可能性があるが
    // pkg.go.devのページではmainタグは1つだけなので実用上問題ない。
    const (
        openTag  = "<main" // > が無いのは意図的。属性が存在する場合を考慮。
        closeTag = "</main>"
    )
    var (
        start = strings.Index(htmlContent, openTag)
        end   = strings.LastIndex(htmlContent, closeTag)
    )
    if start == -1 {
        return "", fmt.Errorf("<main>タグが見つかりませんでした")
    }

    if end == -1 {
        return "", fmt.Errorf("</main>タグが見つかりませんでした")
    }

    // </main>タグ自体も含める
    end += len(closeTag)

    return htmlContent[start:end], nil
}

// convert はHTML文字列をMarkdown形式に変換する。
func convert(htmlContent string, sourceURL string) (string, error) {
    var (
        converter = md.NewConverter(
            "pkg.go.dev",
            true,
            nil,
        )
    )
    converter.Use(plugin.GitHubFlavored()) // GitHub Flavored Markdown (GFM) を有効化

    var (
        markdown string
        err      error
    )
    if markdown, err = converter.ConvertString(htmlContent); err != nil {
        return "", fmt.Errorf("変換処理に失敗しました: %w", err)

    }

    // ヘッダーコメントを先頭に付与 (LLMがコンテキストを把握しやすくなるため)
    header := fmt.Sprintf("<!-- source: %s -->\n\n", sourceURL)

    return header + markdown, nil
}

// write はMarkdown文字列を指定の出力先に書き込む。
// outputPath が空文字の場合は標準出力に書き込む。
func write(content string, outputPath string) error {
    if outputPath == "" {
        _, err := fmt.Fprint(os.Stdout, content)
        return err
    }

    var (
        file *os.File
        err  error
    )
    if file, err = os.Create(outputPath); err != nil {
        return fmt.Errorf("ファイルの作成に失敗しました %q: %w", outputPath, err)
    }
    defer file.Close()

    if _, err = fmt.Fprint(file, content); err != nil {
        return fmt.Errorf("ファイルへの書き込みに失敗しました %q: %w", outputPath, err)
    }

    return nil
}

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-676 (JohannesKaufmann/html-to-markdown)(HTMLからMarkdownへ変換してくれるライブラリ)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。忘れない内にメモメモ。。。

HTMLページをマークダウンに変換したくて何か良いライブラリないかなって探してたら知りました。

github.com

スターが3.6Kとか付いているので、かなり有名なライブラリみたいですね。

変換用のサイトも用意されているみたいです。

html-to-markdown.com

使い方は、リポジトリのREADMEに記載されているのであまり惑うことは無いと思いますが、v1のときと比べてv2の方がなんか複雑になってますね。functional option patternが全面適用されてるのもあって何指定したら良いのか、パッと分からない。。。

試してみる

インストール

go get するだけですが、v1とv2があるので使いたい方をダウンロードしましょう。

go get -u github.com/JohannesKaufmann/html-to-markdown/v2

main.go

package main

import (
        "fmt"

        tomd "github.com/JohannesKaufmann/html-to-markdown/v2"
)

func main() {
        var (
                in = `<h1>へろーわーるど</h1><strong>HELLOWORLD</strong>`
                markdown string
                err error
        )
        if markdown, err = tomd.ConvertString(in); err != nil {
                panic(err)
        }

        fmt.Println(markdown)
}

justfile

# https://just.systems

default:
        @go run .

実行結果

$ just
# へろーわーるど

**HELLOWORLD**

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Gitea 1.26がリリース

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。忘れない内にメモメモ。。。

Giteaのv1.26.0がリリースされていました。いつのまにか、1.26とかになっていたのですねー。

blog.gitea.com

実行バイナリ一つでどこでも動くし、内部データベースもSQLite3とか選べるし、結構便利なので重宝しています。

参考情報

blog.gitea.com


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-675 (binary.NativeEndian)(ホストエンディアン)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。ほぼ利用することは無いのですが、一応メモメモ。。。

最近になって、binary.NativeEndianという変数が encoding/binary パッケージに定義されていることを知りました。

ホストエンディアンがBigEndianなのかLittleEndianなのかを意識しなくて良いようになっているのですね。まあ、最近ホストエンディアンがBigEndianの環境に出会うことがほぼ無いので、利用することは稀かもしれませんが、こういうのって良いものです。

同じマシン内でプロセス間通信とかパイプでやり取りする際に使えそう。

試してみる

main.go

package binaryop

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "math/rand/v2"
    "os"
    "unsafe"
)

// NativeEndian は、binary.NativeEndian のサンプルです。
//
// encoding/binary には、バイトオーダーを扱う3つの変数が定義されている。
//
//   - binary.BigEndian    : 最上位バイトから順に格納(ネットワークバイトオーダーとも呼ばれる)
//   - binary.LittleEndian : 最下位バイトから順に格納(x86/x86-64系CPUの標準)
//   - binary.NativeEndian : 実行環境のCPUに合わせたバイトオーダー
//
// 昨今のデスクトップ・サーバー向けCPU(x86/x86-64)はリトルエンディアンが標準であり、
// ARMもiOS/Androidを含む多くの環境でリトルエンディアンモードで動作する。
// そのため、NativeEndianはほぼリトルエンディアンと同義になるケースが多い。
//
// NativeEndianの主な用途は、ローカルIPC(共有メモリ・パイプ等)のように
// 送受信が同一ホスト内で完結し、エンディアンを意識せずに記述したい場面で有用となる。
// ネットワーク通信ではBigEndian(ネットワークバイトオーダー)を明示するのが原則。
//
// # REFERENCES
//   - https://pkg.go.dev/encoding/binary@go1.26.2#pkg-variables
func NativeEndian() error {
    var (
        v  uint32 = 0x01020304
        sz        = int(unsafe.Sizeof(v))
        be        = make([]byte, sz) // big-endian
        le        = make([]byte, sz) // little-endian
        ne        = make([]byte, sz) // native-endian
    )
    binary.BigEndian.PutUint32(be, v)
    binary.LittleEndian.PutUint32(le, v)
    binary.NativeEndian.PutUint32(ne, v)

    fmt.Printf("[Original     ] 0x%08X\n", v)
    fmt.Printf("[Big    Endian] %v\n", be)
    fmt.Printf("[Little Endian] %v\n", le)
    fmt.Printf("[Native Endian] %v\n", ne)
    fmt.Println("-----------------------")

    //
    // bianry.NativeEndianで読み取るサンプル
    // わざわざソケット用意するのは面倒なので os.Pipe() で代用
    //
    var (
        pr, pw *os.File
        err    error
    )
    if pr, pw, err = os.Pipe(); err != nil {
        return nil
    }

    // SEND SIDE
    type (
        Header struct {
            Length uint32
        }
    )
    const (
        charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    )
    var (
        // ランダム文字列生成
        rnd = func(n int) []byte {
            b := make([]byte, n)
            for i := range b {
                b[i] = charset[rand.IntN(len(charset))]
            }
            return b
        }
        errCh = make(chan error, 1)
    )
    defer close(errCh)

    go func() {
        defer pw.Close()

        var (
            buf     = new(bytes.Buffer)
            payload = rnd(10 + rand.IntN(21))
            header  = Header{Length: uint32(len(payload))}
        )
        if err = binary.Write(buf, binary.NativeEndian, header); err != nil {
            errCh <- err
            return
        }

        if err = binary.Write(buf, binary.NativeEndian, payload); err != nil {
            errCh <- err
            return
        }

        if _, err = pw.Write(buf.Bytes()); err != nil {
            errCh <- err
            return
        }
    }()

    // RECV SIDE
    var (
        header Header
    )
    if err = binary.Read(pr, binary.NativeEndian, &header); err != nil {
        return err
    }

    var (
        payload = make([]byte, header.Length)
    )
    if err = binary.Read(pr, binary.NativeEndian, payload); err != nil {
        return err
    }

    fmt.Printf("length=%d, payload=%s\n", header.Length, payload)

    select {
    case err = <-errCh:
        return err
    default:
    }

    return nil
}

実行結果

$ task
task: [build] go build -o "/home/dev/dev/try-golang/try-golang" .
task: [run] ./try-golang -onetime

ENTER EXAMPLE NAME: binaryop_nativeendian

[Name] "binaryop_nativeendian"
[Original     ] 0x01020304
[Big    Endian] [1 2 3 4]
[Little Endian] [4 3 2 1]
[Native Endian] [4 3 2 1]
-----------------------
length=29, payload=UX0ORrRx8FyzwPpnwkjE9fPqw3xOC


[Elapsed] 888.021µs

参考情報

pkg.go.dev

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-674 (Go 1.22以降ldflagsで-sの指定で-wも暗黙で含むようになっていた)(シンボルテーブル, DWARF)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。忘れないうちにメモメモ。。。

Go 1.22 以降、ldflagsにて-sを指定するだけで-wも暗黙的に含むことになっていることを今更しりました。

なので、これまで

go build -ldflags="-s -w" main.go

のようにやっていたのですが、-ldflags="-s" の指定で良くなっていたのですね。知らなかったです。

Go 1.22のリリースノートにちゃんと書いてありました。。

go.dev

The linker’s -s and -w flags are now behave more consistently across all platforms. The -w flag suppresses DWARF debug information generation. The -s flag suppresses symbol table generation. The -s flag also implies the -w flag, which can be negated with -w=0. That is, -s -w=0 will generate a binary with DWARF debug information generation but without the symbol table.

試してみる

main.go

package main

import "fmt"

func main() {
        // 処理に特に意味は無い

        var (
                p = func() <-chan int {
                        out := make(chan int)
                        go func() {
                                defer close(out)
                                for i := range 5 {
                                        out <- i
                                }
                        }()
                        return out
                }
                c = func(in <-chan int) <-chan bool {
                        out := make(chan bool)
                        go func() {
                                defer close(out)
                                for v := range in {
                                        fmt.Println(v)
                                }
                        }()
                        return out
                }
        )
        <-c(p())
}

Taskfile.yml

# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: '3'

tasks:
  default:
    cmds:
      - task: build
      - task: verify
  build:
    cmds:
      - go build -o app0 main.go
      - go build -ldflags="-s" -o app1 main.go
      - go build -ldflags="-s -w=0" -o app2 main.go
  verify:
    cmds:
      - file app0 app1 app2
      - readelf -S app0 | grep -E '\.debug|\.symtab|\.strtab'
      - readelf -S app1 | grep -E '\.debug|\.symtab|\.strtab'
      - readelf -S app2 | grep -E '\.debug|\.symtab|\.strtab'
    ignore_error: true

実行結果

$ task
task: [build] go build -o app0 main.go
task: [build] go build -ldflags="-s" -o app1 main.go
task: [build] go build -ldflags="-s -w=0" -o app2 main.go
task: [verify] file app0 app1 app2
app0: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, ..., with debug_info, not stripped
app1: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, ..., stripped
app2: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, ..., with debug_info, not stripped
task: [verify] readelf -S app0 | grep -E '\.debug|\.symtab|\.strtab'
  [15] .debug_abbrev     PROGBITS         0000000000000000  00180000
  [16] .debug_line       PROGBITS         0000000000000000  00180160
  [17] .debug_frame      PROGBITS         0000000000000000  001a1ad7
  [18] .debug_gdb_s[...] PROGBITS         0000000000000000  001a8c29
  [19] .debug_info       PROGBITS         0000000000000000  001a8c59
  [20] .debug_loclists   PROGBITS         0000000000000000  001f1d60
  [21] .debug_rnglists   PROGBITS         0000000000000000  0020bdfe
  [22] .debug_addr       PROGBITS         0000000000000000  0021a4ce
  [23] .symtab           SYMTAB           0000000000000000  0021b548
  [24] .strtab           STRTAB           0000000000000000  00229a20
task: [verify] readelf -S app1 | grep -E '\.debug|\.symtab|\.strtab'
task: [verify] readelf -S app2 | grep -E '\.debug|\.symtab|\.strtab'
  [15] .debug_abbrev     PROGBITS         0000000000000000  00180000
  [16] .debug_line       PROGBITS         0000000000000000  00180160
  [17] .debug_frame      PROGBITS         0000000000000000  001a1ad7
  [18] .debug_gdb_s[...] PROGBITS         0000000000000000  001a8c29
  [19] .debug_info       PROGBITS         0000000000000000  001a8c59
  [20] .debug_loclists   PROGBITS         0000000000000000  001f1d60
  [21] .debug_rnglists   PROGBITS         0000000000000000  0020bdfe
  [22] .debug_addr       PROGBITS         0000000000000000  0021a4ce

app0は通常通りなので割愛。

app1は想定通りstrippedと表示されていますね。なので、シンボルテーブル無しでDWARFも無し。ちゃんと-s -wの状態になってる。

app2の方は、想定ではシンボルテーブル無しでDWARF有りとなるはずですが、not strippedと表示されています。

これは、fileコマンドがDWARFが存在しているので、このような表示になっています。

readelfコマンドで各テーブルの情報を確認してみると、app2の方は想定通りsymtabとstrtabは存在せずdebugのみとなっています。

なので、シンボルテーブル無しでDWARF有りの状態になっていますね。

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-673 (TCPコネクションプールを提供してくれるライブラリ)(github.com/soyvural/connpool)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。忘れないうちにメモメモ。。。

いつか使うかもしれないライブラリ。作者の方が Production Ready と記載しているので好印象。

また、プールの状況(stats) を細かく取得出来るのも良いですね。

dev.to

github.com

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-672 (実行されていないコードパスを見つけてくれるツール)(goreach)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。忘れないうちにメモメモ。。。

実際には全く実行されていないコードを探してくれるツールって無いのかなーって探していたら以下の記事を発見。

zenn.dev

github.com

探していたニーズにピッタリのものでした。製作者の方に感謝です m(_ _)m

試してみた

インストール

go install するだけです。

go install github.com/yag13s/goreach/cmd/goreach@latest

main.go

内容に意味はありません。とりあえず絶対に通らないパスがあるサンプルを用意。

package main

import "fmt"

func main() {
        for i := range 10 {
                if i > 10 {
                        fmt.Println("THIS PATH IS UNREACHABLE")
                        break
                }

                fmt.Printf("i=%d\n", i)
        }
}

Taskfile.yml

-coverフラグをオンにしてビルドし、実行した後に goreach さんに分析してもらいます。

# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: '3'

vars:
  APP: app
  COVERDIR: cov
  REPORT_FILE: report.json

tasks:
  default:
    cmds:
      - go build -cover -covermode=set -o {{.APP}} .
      - mkdir -p {{.COVERDIR}}
      - defer: rm -rf {{.COVERDIR}}
      - GOCOVERDIR={{.COVERDIR}} ./{{.APP}}
      - goreach analyze -coverdir {{.COVERDIR}} -pretty > {{.REPORT_FILE}}
      - goreach view -src . {{.REPORT_FILE}}
    silent: false

実行結果

$ task
task: [default] go build -cover -covermode=set -o app .
task: [default] mkdir -p cov
task: [default] GOCOVERDIR=cov ./app
i=0
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9
task: [default] goreach analyze -coverdir cov -pretty > report.json
task: [default] goreach view -src . report.json
goreach view: serving at http://127.0.0.1:43271
Press Ctrl+C to stop.

素晴らしい。

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。

Goメモ-671 (Go 1.26でゴルーチンリーク用のプロファイルが追加)(実験的機能, GOEXPERIMENT=goroutineleakprofile)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。

Go 1.26で、ゴルーチンリークのプロファイルが採取出来るようになりました。1.26では実験的機能で、1.27で正式追加になりそうですね。

go.dev

antonz.org

deadlockと違い、ゴルーチンリークって実際なかなか気づかないので、このような機能追加が本体に入るのはとても良いですね。

実験的機能なので GOEXPERIMENT=goroutineleakprofile の付与が必要なのに注意です。

指定していない場合は、pprof.Lookup("goroutineleak")nil となります。

サンプル

main.go

package main

import (
    "context"
    "flag"
    "os"
    "runtime/pprof"
    "time"
)

type (
    _Args struct {
        withprof bool
    }
)

var (
    args _Args
)

func init() {
    flag.BoolVar(&args.withprof, "prof", false, "with pprof")
}

func main() {
    flag.Parse()

    var (
        ctx = context.Background()
        err error
    )
    if args.withprof {
        //
        // Go 1.26 時点では goroutineleak プロファイル利用は実験的要素。
        // GOEXPERIMENT=goroutineleakprofileの指定が必要となる。
        //
        // https://go.dev/doc/go1.26#goroutineleak-profiles
        // https://antonz.org/go-1-26/#pprof-goroutineleak
        //

        var (
            prof = pprof.Lookup("goroutineleak")
        )
        defer func() {
            // WriteToメソッドの第二引数
            //
            //     The debug parameter enables additional output.
            //     Passing debug=0 writes the gzip-compressed protocol buffer described in https://github.com/google/pprof/tree/main/proto#overview.
            //     Passing debug=1 writes the legacy text format with comments translating addresses to function names and line numbers, so that a programmer can read the profile without tools.
            // The predefined profiles may assign meaning to other debug values; for example,
            //         when printing the "goroutine" profile, debug=2 means to print the goroutine stacks in the same form that a Go program uses when dying due to an unrecovered panic.
            //
            // 本サンプルでは goroutine のプロファイルが見たいので 2 を渡している
            //
            // 事前定義されているプロファイルは以下の名前で登録されている。(go/src/runtime/pprof/pprof.go)
            //
            // goroutine      - stack traces of all current goroutines
            // goroutineleak  - stack traces of all leaked goroutines
            // allocs         - a sampling of all past memory allocations
            // heap           - a sampling of memory allocations of live objects
            // threadcreate   - stack traces that led to the creation of new OS threads
            // block          - stack traces that led to blocking on synchronization primitives
            // mutex          - stack traces of holders of contended mutexes
            //
            prof.WriteTo(os.Stdout, 2)
        }()

        err = run(ctx)
    } else {
        err = run(ctx)
    }

    if err != nil {
        panic(err)
    }
}

func run(pCtx context.Context) error {
    var (
        ctx, cxl = context.WithTimeout(pCtx, 1*time.Second)
    )
    defer cxl()

    leak() // 戻り値のチャネルを利用していないためゴルーチンがリークする
    <-ctx.Done()

    return nil
}

func leak() <-chan int {
    var (
        out = make(chan int)
    )
    go func() {
        out <- 0xBEEF
    }()

    return out
}

Taskfile.yml

# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: '3'

tasks:
  default:
    cmds:
      - go vet main.go
      - go run main.go
      - GOEXPERIMENT=goroutineleakprofile go run main.go -prof
    silent: false

実行結果

$ task
task: [default] go vet main.go
task: [default] go run main.go
task: [default] GOEXPERIMENT=goroutineleakprofile go run main.go -prof
goroutine 1 [running]:
runtime/pprof.writeGoroutineStacks({0x524c28, 0xa2480740040})
        /home/dev/.local/go/src/runtime/pprof/pprof.go:819 +0x6b
runtime/pprof.writeGoroutineLeak({0x524c28, 0xa2480740040}, 0x2)
        /home/dev/.local/go/src/runtime/pprof/pprof.go:806 +0xa8
runtime/pprof.(*Profile).WriteTo(0x0?, {0x524c28?, 0xa2480740040?}, 0xa248076c090?)
        /home/dev/.local/go/src/runtime/pprof/pprof.go:408 +0x149
main.main.func1()
        /home/dev/github/try-golang/examples/go126/goroutine_leak/main.go:65 +0x2a
main.main()
        /home/dev/github/try-golang/examples/go126/goroutine_leak/main.go:76 +0xea

goroutine 7 [chan send (leaked)]:
main.leak.func1()
        /home/dev/github/try-golang/examples/go126/goroutine_leak/main.go:96 +0x25
created by main.leak in goroutine 1
        /home/dev/github/try-golang/examples/go126/goroutine_leak/main.go:94 +0x5f

leak関数でチャネルの送信部分がリークしてるぞってちゃんと教えてくれてますね。

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。