ethan

ethan

新知,热爱生活,码农,读书
twitter
email
github

Goの標準ライブラリの使用説明

Go 中の時間操作#

Golang における時間に関連する操作は、主に time パッケージに関係しており、コアデータ構造はtime.Timeです。以下に示します。

type Time struct {
    wall uint64
    ext  int64
    loc *Location
}

1、時間関連関数の取得#

1.1 現在の時間を取得#

// 現在の時間を返します。この時、返されるのはtime.Time型です。
now := time.Now()
fmt.Println(now)
// 現在のタイムスタンプ
fmt.Println(now.Unix())
// ナノ秒単位のタイムスタンプ
fmt.Println(now.UnixNano())
// タイムスタンプの小数部分 単位:ナノ秒
fmt.Println(now.Nanosecond())

出力:

2021-01-10 14:56:15.930562 +0800 CST m=+0.000124449
1610261775
1610261775930562000
930562000

1.2 現在の年月日、時分秒、曜日、一年の何日目かなどの操作を返す#

now := time.Now()
// 日付を返す
year, month, day := now.Date()
fmt.Printf("year:%d, month:%d, day:%d\n", year, month, day)
// 年
fmt.Println(now.Year())
// 月
fmt.Println(now.Month())
// 日
fmt.Println(now.Day())
// 時分秒
hour, minute, second := now.Clock()
fmt.Printf("hour:%d, minute:%d, second:%d\n", hour, minute, second)
// 時
fmt.Println(now.Hour())
// 分
fmt.Println(now.Minute())
// 秒
fmt.Println(now.Second())
// 曜日を返す
fmt.Println(now.Weekday())
// 一年の何日目かを返す
fmt.Println(now.YearDay())
// タイムゾーンを返す
fmt.Println(now.Location())
// 一年の何日目かを返す
fmt.Println(now.YearDay())

1.3 時間をフォーマットする#

Go 言語は時間型フォーマット関数Format()を提供しています。注意すべきは、Go 言語の時間フォーマットテンプレートは一般的なY-m-d H:i:sではなく、2006-01-02 15:04:05であり、非常に覚えやすい(2006 1 2 3 4 5)。

now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05"))
fmt.Println(now.Format("2006-01-02"))
fmt.Println(now.Format("15:04:05"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))

2、タイムスタンプと日付文字列の相互変換#

タイムスタンプを日付形式に変換するには、まずタイムスタンプをtime.Time型に変換し、その後日付形式にフォーマットします。

2.1 秒数、ナノ秒数に基づいてtime.Time型を返す#

now := time.Now()
layout := "2006-01-02 15:04:05"
t := time.Unix(now.Unix(),0)    // 引数はそれぞれ:秒数,ナノ秒数
fmt.Println(t.Format(layout))

2.2 指定した時間に基づいてtime.Time型を返す、関数time.Date()を使用#

now := time.Now()
layout := "2006-01-02 15:04:05"
// 指定した時間に基づいてtime.Time型を返す
// 年、月、日、時、分、秒、ナノ秒、タイムゾーンをそれぞれ指定
t := time.Date(2011, time.Month(3), 12, 15, 30, 20, 0, now.Location())
fmt.Println(t.Format(layout))

2.3 日付文字列をtime.Time型に解析する#

t, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Now().Format("2006-01-02 15:04:05"), time.Local)
fmt.Println(t)  
// 出力 2021-01-10 17:28:50 +0800 CST
// time.Localはローカル時間を指定

解析する際には、特にタイムゾーンの問題に注意が必要です:

fmt.Println(time.Now())
fmt.Println(time.Now().Location())
t, _ := time.Parse("2006-01-02 15:04:05", "2021-01-10 15:01:02")
fmt.Println(t)

出力:

2021-01-10 17:22:10.951904 +0800 CST m=+0.000094166
Local
2021-01-10 15:01:02 +0000 UTC

time.Now()が使用しているのは CST(中国標準時間)であり、time.Parse()のデフォルトは UTC(ゼロ時区)です。両者は 8 時間の差があります。したがって、解析時にはtime.ParseInLocation()をよく使用し、タイムゾーンを指定できます。img

3、日付の計算と比較#

日付の計算に触れると、time パッケージが提供する新しい型Durationを紹介せざるを得ません。ソースコードは以下のように定義されています:

type Duration int64

底層の型は int64 で、時間間隔を表し、単位はナノ秒です。

3.1 24 時間以内の時間計算#

now := time.Now()
fmt.Println(now)
// 1時間1分1秒後
t1, _ := time.ParseDuration("1h1m1s")
fmt.Println(t1)
m1 := now.Add(t1)
fmt.Println(m1)
// 1時間1分1秒前
t2, _ := time.ParseDuration("-1h1m1s")
m2 := now.Add(t2)
fmt.Println(m2)
// 3時間前
t3, _ := time.ParseDuration("-1h")
m3 := now.Add(t3 * 3)
fmt.Println(m3)
// 10分後
t4, _ := time.ParseDuration("10m")
m4 := now.Add(t4)
fmt.Println(m4)
// Subは二つの時間差を計算
sub1 := now.Sub(m3)
fmt.Println(sub1.Hours())   // 相差時間数
fmt.Println(sub1.Minutes()) // 相差分数

さらに、2 つの関数time.Since()time.Until()を紹介します:

// 現在の時間とtの時間差を返します。返り値はDurationです。
time.Since(t Time) Duration
// tと現在の時間の時間差を返します。返り値はDurationです。
time.Until(t Time) Duration

now := time.Now()
fmt.Println(now)
t1, _ := time.ParseDuration("-1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(time.Since(m1))
fmt.Println(time.Until(m1))

出力:

2021-01-10 20:41:48.668232 +0800 CST m=+0.000095594
2021-01-10 19:41:48.668232 +0800 CST m=-3599.999904406
1h0m0.000199007s
-1h0m0.000203035s

3.2 24 時間以上の時間計算#

1 日以上の時間計算に関しては、time.AddDate()を使用する必要があります。関数のプロトタイプは以下の通りです:

func (t Time) AddDate(years int, months int, days int) Time

例えば、1 年 1 ヶ月 1 日後の時間を知りたい場合は、次のようにします:

now := time.Now()
fmt.Println(now)
m1 := now.AddDate(1,1,1)
fmt.Println(m1)

次に、2 日前の時間を取得したい場合:

now := time.Now()
fmt.Println(now)
m1 := now.AddDate(0,0,-2)
fmt.Println(m1)

3.3 日付の比較#

日付の比較には 3 つの種類があります:以前、以降、等しい。

// tがuより前の時間点を表す場合は真を返します。そうでない場合は偽を返します。
func (t Time) Before(u Time) bool
// tがuより後の時間点を表す場合は真を返します。そうでない場合は偽を返します。
func (t Time) After(u Time) bool
// 時間が等しいかどうかを比較し、等しい場合は真を返します。そうでない場合は偽を返します。
func (t Time) Equal(u Time) bool

now := time.Now()
fmt.Println(now)
// 1時間後
t1, _ := time.ParseDuration("1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(m1.After(now))
fmt.Println(now.Before(m1))
fmt.Println(now.Equal(m1))

出力:

2021-01-10 21:00:44.409785 +0800 CST m=+0.000186800
2021-01-10 22:00:44.409785 +0800 CST m=+3600.000186800
true
true
false

4、一般的な例#

以下に一般的な例と関数のラッピングをいくつか示します。

4.1 日付形式からタイムスタンプへ#

func TimeStr2Time(fmtStr,valueStr, locStr string) int64 {
    loc := time.Local
    if locStr != "" {
        loc, _ = time.LoadLocation(locStr) // タイムゾーンを設定
    }
    if fmtStr == "" {
        fmtStr = "2006-01-02 15:04:05"
    }
    t, _ := time.ParseInLocation(fmtStr, valueStr, loc)
    return t.Unix()
}

4.2 現在の時間の日付形式を取得#

func GetCurrentFormatStr(fmtStr string) string {
    if fmtStr == "" {
        fmtStr = "2006-01-02 15:04:05"
    }
    return time.Now().Format(fmtStr)
}

4.3 タイムスタンプから日付形式へ#

func Sec2TimeStr(sec int64, fmtStr string) string {
    if fmtStr == "" {
        fmtStr = "2006-01-02 15:04:05"
    }
    return time.Unix(sec, 0).Format(fmtStr)
}

Go-regexp 正規表現#

package main

import (
    "fmt"
    "regexp"
)

const text = "My email is [email protected]"

func main() {
    compile := regexp.MustCompile(`[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+`)
    match := compile.FindString(text)
    fmt.Println(match)
}

Go ストレージ基礎 — ファイル IO 操作#

img

2 つの IO 分類#

計算の体系構造、CPU、メモリ、ネットワーク、IO。では IO とは何でしょうか?一般的には Input、Output の略と理解され、簡単に言えば入力出力の意味です。

IO はネットワーク IO とストレージ IO の 2 種類に分けられます(実際、Go の中ではネットワーク IO とディスク IO には根本的な違いがあります)。ネットワーク IO はネットワークデータ伝送プロセスに対応し、ネットワークは分散システムの基礎であり、ネットワークを通じて離散的な物理ノードを接続し、有機的なシステムを形成します。

ストレージ IO はデータを物理メディアに保存するプロセスに対応し、通常物理メディアはディスクに対応します。ディスク上には通常パーティションが分かれており、その上にファイルシステムがフォーマットされるため、一般的なプログラマーが最もよく目にするのはファイル IO の形式です。

Golang では、ファイルの読み書きの方法を 2 つに分類できます:

  1. 標準ライブラリのラッピング:操作対象はFileです;
  2. システムコール:操作対象はfdです;

読み書きデータの要素#

ファイルの読み書きで最も重要な要素は何でしょうか?

簡単に言えば:ファイルを読むとは、ディスク上のファイルの特定の位置のデータをメモリのバッファに読み込むことです。ファイルに書き込むとは、メモリバッファのデータをディスクのファイルの特定の位置に書き込むことです。

ここで 2 つのキーワードに注意してください:

  1. 特定の位置;
  2. メモリバッファ;

特定の位置はどう理解すればよいでしょうか?いわゆる特定の位置をどう指定するのでしょうか?

非常に簡単で、[ offset, length ]の 2 つのパラメータで位置を識別できます。

img

つまり IO のオフセットと長さ、Offset と Length です。

メモリバッファはどう理解すればよいでしょうか?

根本的には、ファイルのデータは誰と直接やり取りするのでしょうか?メモリです。書き込み時はメモリからディスクファイルに書き込み、読み込み時はディスクファイルからメモリに読み込まれます。

本質的に、以下の IO 関数は Offset、Length、buffer の 3 つの要素から外れることはありません。

標準ライブラリのラッピング#

Go はファイルの読み書きを非常に簡単に行うことができます。なぜなら、Go は非常に便利な使用インターフェースを標準ライブラリ os にラッピングしているからです。Go の標準ライブラリによるファイル IO のラッピングは、Go がファイルに対して IO を行う際に推奨される操作方法です。

ファイルを開く(Open)#

func OpenFile(name string, flag int, perm FileMode) (*File, error)

ファイルを開くと、ハンドルを取得します。つまりFile構造体を取得し、その後のファイルの読み書きはすべてFile構造体に基づいて行われます。

type File struct {
    *file // os specific
}

ファイルの読み書きは、このハンドル構造体に対して操作を行うだけで済みます。

もう 1 つ隠れた知識点を提起する必要があります:オフセット。つまり、最初に強調した読み書きの 3 つの要素の 1 つの Offset です。ファイルを開く(Open)とき、ファイルの現在のオフセットはデフォルトで 0 に設定されます。つまり、IO の開始位置はファイルの最初です。例えば、この時点で 4K のデータをファイルに書き込むと、[0, 4K] の位置にデータが書き込まれます。もし以前にこの上にデータがあった場合、上書きされます。

Openファイルの際にO_APPENDオプションを指定しない限り、オフセットはファイルの末尾に設定され、IO はファイルの末尾から開始されます。

ファイル書き込み操作(Write)#

ファイルFileハンドルオブジェクトには 2 つの書き込みメソッドがあります:

1 つ目:ファイルにバッファを 1 つ書き込み、ファイルの現在のオフセットを使用します。

func (f *File) Write(b []byte) (n int, err error)

注意:この書き込み操作はファイルのオフセットを増加させます。

2 つ目:指定されたファイルオフセットからバッファをファイルに書き込みます。

func (f *File) WriteAt(b []byte, off int64) (n int, err error)

注意:この書き込み操作はファイルのオフセットを更新しません。

ファイル読み取り操作(Read)#

書き込みに対応して、ファイルFileハンドルオブジェクトには 2 つの読み取りメソッドがあります:

1 つ目:ファイルの現在のオフセットからバッファのデータを 1 つ読み取ります。

func (f *File) Read(b []byte) (n int, err error)

注意:この読み取り操作はファイルのオフセットを増加させます。

2 つ目:指定されたファイルオフセットからバッファサイズのデータを 1 つ読み取ります。

func (f *File) ReadAt(b []byte, off int64) (n int, err error)

注意:この読み取り操作はファイルのオフセットを更新しません。

指定オフセット(Seek)#

func (f *File) Seek(offset int64, whence int) (ret int64, err error)

このハンドルメソッドは、ユーザーがファイルのオフセット位置を指定することを許可します。これは非常に理解しやすいです。例えば、ファイルが最初は 0 バイトで、1M のデータを書き込むと、サイズは 1M に変わり、オフセットは 1M 後ろに移動します。デフォルトでは後ろに移動します。

現在、Seek メソッドは書き込みオフセットを任意の位置に設定でき、任意の場所からデータを上書きすることができます。

したがって、Go ではファイル IO は非常に簡単です。まずファイルを Open し、Fileハンドルを取得し、その後はこのハンドルを使用して Write、Read、Seek を行うことで IO ができます。

基本原理#

Go の標準ライブラリosは非常に便利なラッピングを提供しています。最も基本的な本質に深く入り込むと、最も重要なものが見えてきます:システムコール

Go の標準ライブラリによるファイルストレージ IO は、システムコールに基づいています。os.OpenFileの呼び出しを少し追ってみましょう:

os ライブラリのOpenFile関数:

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    f, err := openFileNolog(name, flag, perm)
    if err != nil {
        return nil, err
    }
    f.appendMode = flag&O_APPEND != 0
    return f, nil
}

openFileNolog関数を少し見てみましょう:

func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
    var r int
    for {
        var e error
        r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
        if e == nil {
            break
        }
        if runtime.GOOS == "darwin" && e == syscall.EINTR {
            continue
        }
        return nil, &PathError{"open", name, e}
    }
    return newFile(uintptr(r), name, kindOpenFile), nil
}

ここでsyscall.Open関数が整数を取得します。これは C 言語で最も一般的な fd ハンドルです。そしてFile構造体は、実際にはこれに基づく 1 層のラッピングに過ぎません。

なぜこの標準ライブラリのラッピングが存在するのでしょうか?

重要なポイント:オペレーティングシステムの違いを隠蔽するためです。この標準ライブラリを使用するすべての操作はクロスプラットフォームです。言い換えれば、特定のオペレーティングシステムに特有の機能がある場合、os ライブラリには対応するラッピングされた IO 操作が存在しません。

img

では、システムコールをどう使用するのでしょうか?

直接syscallライブラリを使用します。つまり、システムコールです。名前からもわかるように、システムコールはオペレーティングシステムと強く関連しています。なぜなら、オペレーティングシステムが提供する呼び出しインターフェースだからです。したがって、システムコールはオペレーティングシステムによって異なる特性を持つ可能性があります。

したがって、syscallライブラリを直接使用してシステムコールを使用する場合、システムがもたらす互換性の問題を自分で引き受ける必要があります。

システムコール#

システムコールは syscall で最も基本的なラッピングがあります:

ファイルを開く#

func Open(path string, mode int, perm uint32) (fd int, err error) 

ファイルを読み取る#

func Read(fd int, p []byte) (n int, err error) func Pread(fd int, p []byte, offset int64) (n int, err error) 

ファイルの読み取りには 2 つのインターフェースがあります。1 つはReadで、現在のデフォルトオフセットからバッファデータを 1 つ読み取りますPreadインターフェースは、指定された位置からデータを読み取るインターフェースです。

考えてみてください:PreadSeekReadを組み合わせて使用する効果がありますが、PreadSeek + Readに置き換えられると考えることはできますか?

いいえ!根本的な理由は、Seek + Readはユーザーレベルでの 2 ステップ操作ですが、PreadSeek + Readの効果を持っていますが、オペレーティングシステムがユーザーに提供する意味は:Preadは原子操作です。もう 1 つの重要な違いは、Preadの呼び出しは現在のファイルオフセットを更新しないことです。

したがって、要約すると、**Pread** と順次呼び出し **Seek** の後に呼び出す **Read** には 2 つの重要な違いがあります:

  1. Preadはユーザーに提供する意味は原子操作であり、Preadを呼び出すとき、SeekRead操作を中断することはできません;
  2. Preadの呼び出しは現在のファイルオフセットを更新しません;

ファイルの書き込み#

func Write(fd int, p []byte) (n int, err error) func Pwrite(fd int, p []byte, offset int64) (n int, err error) 

ファイルの書き込みには 2 つのインターフェースがあり、WritePwriteはそれぞれReadPreadに対応します。同様に、Pwriteの効果もSeekの後にWriteを呼び出すのと同じですが、同様に 2 つの異なる点があります:

  1. PwriteSeekWriteの対外的な意味は原子操作です;
  2. Pwriteの呼び出しは現在のファイルオフセットを更新しません;

ファイルのシーク#

func Seek(fd int, offset int64, whence int) (off int64, err error) 

この関数呼び出しは、ユーザーがオフセットを指定することを許可します。一般的に、各オープンファイルには関連付けられた「現在のファイルオフセット」(current file offset)があります。読み取り(Read)、書き込み(Write)操作はすべて現在のファイルオフセットから開始され、ReadWriteはオフセットを増加させます。増加量は読み書きされたバイト数です。

要約すると:Go のコアの Open、Read、Write、Seek のシステムコールを見てみると、標準 IO ライブラリとの明らかな違いがあります:システムコールの操作対象は整数ハンドルですOpenファイルを取得すると整数 fd が得られ、その後のすべての IO はこの fd に対して操作されます。これは標準ライブラリとは異なり、os 標準ライブラリの OpenFile はFile構造体を取得し、すべての IO もこの構造体に対して行われます。

階層構造#

では、ラッピングの階層は一般的にどのようなものなのでしょうか。Unix プログラミングの冒頭には以下のような図があります:

img

この図は、全体の Unix 体系構造を非常に明確に説明しています。

  • カーネルは最もコアな実装であり、IO デバイスやハードウェアとの相互作用などの機能を含みます。カーネルに密接に関連する層は、カーネルが外部に提供するシステムコールです。システムコールはユーザーモードからカーネルモードへの呼び出しの通路を提供します;

  • システムコールに対して、各言語の標準ライブラリにはいくつかのラッピングがあります。例えば、C 言語の libc ライブラリ、Go 言語の os、syscall ライブラリは同様の地位にあります。これがいわゆる公共ライブラリです。この層のラッピングの主な目的は、一般的なプログラマーの使用効率を簡素化し、システムの詳細を隠蔽し、クロスプラットフォームの基盤を提供することです(同様に、クロスプラットフォームの特性のために、互換性のない機能が多く削除される可能性があるため、システムコールを直接呼び出す必要が生じます);

  • もちろん、右上隅には欠口があり、アプリケーションは公共関数ライブラリを使用するだけでなく、実際にはシステムコールを直接呼び出すこともできますが、その場合の複雑さはアプリケーション自身が負担する必要があります。このようなニーズは非常に一般的であり、標準ライブラリは一般的なものをラッピングしている一方で、多くのシステムコールの機能を削除しているため、その場合はシステムコールを使用して取得する必要があります。

まとめ#

  1. IO の大分類はネットワーク IO とディスク IO に分かれ、IO はファイルに対しては読み書き操作であり、書き込み時はデータがメモリからディスクに移動し、読み込み時はデータがディスクからメモリに移動します

  2. Go ファイル IO で最も一般的なのは os ライブラリであり、Go がラッピングした標準ライブラリを使用して、os.OpenFileで開き、File.WriteFile.Readで読み書きし、操作対象はすべてFile構造体です;

  3. Go の標準ライブラリによる IO のラッピングは、複雑なシステムコールを隠蔽し、クロスプラットフォームの使用姿勢を提供するためです。そして、syscallライブラリを個別に提供し、プログラマーがより豊富なシステムコール機能を使用するかどうかを自己決定できるようにします。もちろん、その結果は自己責任です;

  4. Go の標準ライブラリ IO 操作対象はFileであり、システムコール IO 操作対象は fd(非負整数)です。

  5. Openファイルのデフォルトの現在のオフセットは 0(ファイルの最初)であり、O_APPENDパラメータを追加するとオフセットはファイルの末尾になります。Seek 呼び出しを通じて任意のファイルオフセットを指定することで、ファイル IO の位置に影響を与えることができます;

  6. ReadWrite関数はバッファ(バッファには長さがあります)だけで、オフセットは現在のファイルオフセットを使用します;

  7. PreadPwriteのシステムコールの効果はSeekオフセットの後にReadWriteを呼び出すのと同等ですが、また大きく異なります。対外的な意味は原子操作であり、現在のファイルオフセットを更新しません。

Go - ファイル読み書き操作#

ファイルの読み書き#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*既存のファイルをクリアして内容を追加する*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_RDWR|os.O_APPEND, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    /*ファイルストリームを閉じる*/
    defer file.Close()
    /*読み取り*/
    reader := bufio.NewReader(file)
    for {
        str, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        fmt.Print(str)
    }
    /*ファイルに書き込む*/
    str := "hello FCC您好!!!\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*writerはバッファを持っているため、flushしてディスクに書き込む必要があります*/
    writer.Flush()
}

ファイル内容を新しいファイルにコピー#

package main

import (
    "fmt"
    "io/ioutil"
)

/*ファイル1の内容をファイル2にコピー*/
func main() {
    file1Path := "D:\\fcofficework\\DNS\\1.txt"
    file2Path := "D:\\fcofficework\\DNS\\2.txt"
    data, err := ioutil.ReadFile(file1Path)
    if err != nil {
        fmt.Printf("read file err=%v", err)
        return
    }
    err = ioutil.WriteFile(file2Path, data, 0666)
    if err != nil {
        fmt.Printf("write file err=%v\n", err)
    }
}

ファイルまたはディレクトリの存在を確認#

package main

import (
    "fmt"
    "os"
)

/*ファイルおよびディレクトリの存在を確認*/
func PathExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        fmt.Println("現在のファイルは存在します!")
        return true, nil
    }
    if os.IsNotExist(err) {
        fmt.Println("現在のファイルは存在しません!")
        return false, nil
    }
    return false, nil
}

func main() {
    path := "D:\\fcofficework\\2.txt"
    PathExists(path)
}

ファイルのコピー#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*ファイルのコピー*/

func CopyFile(dstFileName string, srcFileName string) (written int64, err error) {
    srcFile, err := os.Open(srcFileName)
    if err != nil {
        fmt.Printf("open file err=%v\n", err)
    }
    reader := bufio.NewReader(srcFile)

    dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Printf("open file err=%v\n", err)
        return
    }
    writer := bufio.NewWriter(dstFile)
    defer dstFile.Close()
    return io.Copy(writer, reader)
}

func main() {
    srcFile := "D:\\Photos\\Datapicture\\mmexport1530688562488.jpg"
    dstFile := "D:\\Photos\\1.jpg"
    _, err := CopyFile(dstFile, srcFile)
    if err == nil {
        fmt.Println("コピー完了!")
    } else {
        fmt.Println("コピー失敗、err=", err)
    }
}

ファイルを読み取り、ファイル内の文字の数をカウント#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*ファイルの文字数をカウント*/

type CharCount struct {
    /*英字の数*/
    ChCount int
    /*数字の数*/
    NumCount int
    /*スペースの数*/
    SpaceCount int
    /*その他の文字の数*/
    OtherCount int
}

func main() {
    fileName := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.Open(fileName)
    if err != nil {
        fmt.Printf("open file err=%v\n", err)
        return
    }
    defer file.Close()
    var count CharCount
    reader := bufio.NewReader(file)
    for {
        str, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        for _, v := range str {
            switch {
            case v >= 'a' && v <= 'z':
                fallthrough
            case v >= 'A' && v <= 'Z':
                count.ChCount++
            case v == ' ' || v == '\t':
                count.SpaceCount++
            case v >= '0' && v <= '9':
                count.NumCount++
            default:
                count.OtherCount++
            }
        }
    }
    fmt.Printf("文字の数は:%v 数字の数は:%v スペースの数は:%v その他の文字の数は:%v",
        count.ChCount, count.NumCount, count.SpaceCount, count.OtherCount)
}

3 つのファイル読み取り方法#

os を通じて読み取る#

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("d:\\Photos\\Screenshots\\暗物質\\IMG_20180927_194619.jpg")
    if err != nil {
        fmt.Println("open file err", err)
    }
    fmt.Printf("file=%v", file)
    err1 := file.Close()
    if err1 != nil {
        fmt.Println("close file err = ", err1)
    }
}

バッファ式でファイルを読み取る#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*バッファ式でファイルを読み取る*/
func main() {
    file, err := os.Open("d:\\Photos\\Screenshots\\暗物質\\IMG_20180927_194619.jpg")
    if err != nil {
        fmt.Println("open file err", err)
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        str, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        fmt.Print(str)
    }
    fmt.Println("ファイルの読み取りが終了しました!")
}

ioutil を通じて読み取る#

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    file := "D:\\fcofficework\\DNS\\authorized_keys"
    content, err := ioutil.ReadFile(file)
    if err != nil {
        fmt.Printf("read file err=%v", err)
    }
    fmt.Printf("%v", string(content))
}

ファイルへの書き込みの例#

ファイルに内容を書き込み、存在しない場合は再作成#

package main

import (
    "bufio"
    "fmt"
    "os"
)

/*ファイルに内容を書き込み、存在しない場合は再作成*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    defer file.Close()
    str := "hello world\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*writerはバッファを持っているため、flushしてディスクに書き込む必要があります*/
    writer.Flush()
}

既存のファイルをクリアして再度書き込む#

package main

import (
    "bufio"
    "fmt"
    "os"
)

/*既存のファイルをクリアして再度書き込む*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    defer file.Close()
    str := "hello FCC\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*writerはバッファを持っているため、flushしてディスクに書き込む必要があります*/
    writer.Flush()
}

既存のファイルをクリアして内容を追加#

package main

import (
    "bufio"
    "fmt"
    "os"
)

/*既存のファイルをクリアして内容を追加*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    defer file.Close()
    str := "hello FCC您好!!!\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*writerはバッファを持っているため、flushしてディスクに書き込む必要があります*/
    writer.Flush()
}

コマンドライン引数の解析#

package main

import (
    "fmt"
    "os"
)

/*コマンドライン引数の解析*/
func main() {
    fmt.Println("コマンドライン引数の数:", len(os.Args))
    for i, v := range os.Args {
        fmt.Printf("args[%v]=%v\n", i, v)
    }
}
package main

import (
    "flag"
    "fmt"
)

/*コマンドライン引数の解析*/
func main() {
    var user string
    var pwd string
    var host string
    var port int
    flag.StringVar(&user, "u", "", "ユーザー名、デフォルトは空")
    flag.StringVar(&pwd, "pwd", "", "パスワード、デフォルトは空")
    flag.StringVar(&host, "h", "localhost", "ホスト名、デフォルトは空")
    flag.IntVar(&port, "port", 3306, "ポート番号、デフォルトは空")
    /*変換*/
    flag.Parse()
    fmt.Printf("user=%v pwd=%v host=%v port=%v", user, pwd, host, port)
}

Go-json シリアル化#

シリアル化#

package main

import (
    "encoding/json"
    "fmt"
)

type Monster struct {
    Name     string  `json:"name"`
    Age      int     `json:"age"`
    Birthday string  `json:"birthday"`
    Sal      float64 `json:"sal"`
    Skill    string  `json:"skill"`
}

/*構造体シリアル化*/
func NewMinsterStruct() {
    monster := Monster{
        Name:     "孫悟空",
        Age:      500,
        Birthday: "2011-11-11",
        Sal:      8000.0,
        Skill:    "如意七十二変",
    }
    data, err := json.Marshal(&monster)
    if err != nil {
        fmt.Printf("シリアル化エラーerr:%v\n", err)
    }
    fmt.Printf("Mapシリアル化後=%v\n", string(data))
}

/*Mapシリアル化*/
func MapSerlizer() {
    var a map[string]interface{}
    a = make(map[string]interface{})
    a["name"] = "牛魔王"
    a["age"] = 10
    a["address"] = "火云洞"
    data, err := json.Marshal(a)
    if err != nil {
        fmt.Printf("シリアル化エラーerr:%v\n", err)
    }
    fmt.Printf("monsterシリアル化後=%v\n", string(data))
}

/*スライスシリアル化*/
func SliceSerlizer() {
    var slice []map[string]interface{}
    var m1 map[string]interface{}
    m1 = make(map[string]interface{})
    m1["name"] = "TGH"
    m1["age"] = "19"
    m1["address"] = "北京"
    slice = append(slice, m1)

    var m2 map[string]interface{}
    m2 = make(map[string]interface{})
    m2["name"] = "FCC"
    m2["age"] = "18"
    m2["address"] = [2]string{"華府", "映像帝国"}
    slice = append(slice, m2)

    data, err := json.Marshal(slice)
    if err != nil {
        fmt.Printf("シリアル化エラーerr:%v\n", err)
    }
    fmt.Printf("スライスシリアル化後=%v\n", string(data))
}

/*基本データ型シリアル化*/
func FloatSerlize() {
    var num1 float64 = 245.56
    data, err := json.Marshal(num1)
    if err != nil {
        fmt.Printf("シリアル化エラーerr:%v\n", err)
    }
    fmt.Printf("基本データ型シリアル化後=%v\n", string(data))
}

func main() {
    NewMinsterStruct()
    MapSerlizer()
    SliceSerlizer()
    FloatSerlize()
}
Mapシリアル化後={"name":"孫悟空","age":500,"birthday":"2011-11-11","sal":8000,"skill":"如意七十二変"}
monsterシリアル化後={"address":"火云洞","age":10,"name":"牛魔王"}
スライスシリアル化後=[{"address":"北京","age":"19","name":"TGH"},{"address":["華府","映像帝国"],"age":"18","name":"FCC"}]
基本データ型シリアル化後=245.56

逆シリアル化#

package main

import (
    "encoding/json"
    "fmt"
)

type Monster struct {
    Name     string  `json:"name"`
    Age      int     `json:"age"`
    Birthday string  `json:"birthday"`
    Sal      float64 `json:"sal"`
    Skill    string  `json:"skill"`
}

func unmarshalStruct() {
    str := "{\"name\":\"孫悟空\",\"age\":500,\"birthday\":\"2011-11-11\",\"sal\":8000,\"skill\":\"如意七十二変\"}"
    var monster Monster
    err := json.Unmarshal([]byte(str), &monster)
    if err != nil {
        fmt.Printf("逆シリアル化失敗err:%v\n", err)
    }
    fmt.Printf("逆シリアル化後monster:%v\n", monster)
}

func unmarshallMap() {
    str := "{\"address\":\"火云洞\",\"age\":10,\"name\":\"牛魔王\"}"
    var a map[string]interface{}
    err := json.Unmarshal([]byte(str), &a)
    if err != nil {
        fmt.Printf("逆シリアル化失敗err:%v\n", err)
    }
    fmt.Printf("逆シリアル化Map後:%v\n", a)
}

func unmarshalSlice() {
    str := "[{\"address\":\"北京\",\"age\":\"19\",\"name\":\"TGH\"}," +
        "{\"address\":[\"華府\",\"映像帝国\"],\"age\":\"18\",\"name\":\"FCC\"}]"
    var slice []map[string]interface{}
    err := json.Unmarshal([]byte(str), &slice)
    if err != nil {
        fmt.Printf("逆シリアル化失敗err:%v\n", err)
    }
    fmt.Printf("逆シリアル化Slice後:%v\n", slice)
}

func main() {
    unmarshalStruct()
    unmarshallMap()
    unmarshalSlice()
}

出力結果:

逆シリアル化後monster:{孫悟空 500 2011-11-11 8000 如意七十二変} 
逆シリアル化Map後:map[address:火云洞 age:10 name:牛魔王] 
逆シリアル化Slice後:[map[address:北京 age:19 name:TGH] map[address:[華府 映像帝国] age:18 name:FCC]]

Go-HTTP パッケージの使用#

Web は HTTP プロトコルに基づくサービスであり、Go 言語には net/http パッケージが提供されており、HTTP パッケージを使用することで、非常に簡単に実行可能な Web サービスを構築できます。また、このパッケージを使用すると、Web のルーティング、静的ファイル、テンプレート、クッキーなどのデータを簡単に設定および操作できます。

http パッケージで Web サーバーを構築#

package main
import (
    "fmt"
    "net/http"
    "strings"
    "log"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()  // パラメータを解析します。デフォルトでは解析されません。
    fmt.Println(r.Form)  // これらの情報はサーバー側の出力情報です。
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"])
    for k, v := range r.Form {
        fmt.Println("key:", k)
        fmt.Println("val:", strings.Join(v, ""))
    }
    fmt.Fprintf(w, "Hello golang!") // これがwに書き込まれるのはクライアントへの出力です。
}
func main() {
    http.HandleFunc("/", sayhelloName) // アクセスルートを設定します。
    err := http.ListenAndServe(":8080", nil) // リッスンするポートを設定します。
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

上記のコードをビルドして実行すると、8080 ポートで HTTP 接続リクエストをリッスンしている状態になります。

ブラウザにhttp://localhost:8080と入力すると、ブラウザのページにHello golang!と出力されます。

ブラウザに次のアドレスを入力します:

http://localhost:8080/?url_long=var1&url_long=var2

ブラウザの出力を確認してみてください。

上記のコードを見て、Web サーバーを構築するのは非常に簡単で、HTTP パッケージの 2 つの関数を呼び出すだけで済みます。

http パッケージでページをリクエストする#

package main

import (
    "fmt"
    "net/http"
    "net/http/httputil"
)

func main() {
    request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
    if err != nil {
        panic(err)
    }
    request.Header.Add("User-Agent",
        "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")

    client := http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            fmt.Println("Redirect:", req)
            return nil
        },
    }

    resp, err := client.Do(request)
    //resp, err := http.DefaultClient.Do(request)
    //resp, err := http.Get("http://www.imooc.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    s, err := httputil.DumpResponse(resp, true)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(s))
}

プログラムを実行すると、HTML 内容が出力されます。

img

net/http パッケージの落とし穴 ——i/o timeout#

問題#

日常のコードを見てみましょう。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net"
    "net/http"
    "time"
)

var tr *http.Transport

func init() {
    tr = &http.Transport{
        MaxIdleConns: 100,
        Dial: func(netw, addr string) (net.Conn, error) {
            conn, err := net.DialTimeout(netw, addr, time.Second*2) // 接続タイムアウトを設定
            if err != nil {
                return nil, err
            }
            err = conn.SetDeadline(time.Now().Add(time.Second * 3)) // データ送受信のタイムアウトを設定
            if err != nil {
                return nil, err
            }
            return conn, nil
        },
    }
}

func main() {
    for {
        _, err := Get("http://www.baidu.com/")
        if err != nil {
            fmt.Println(err)
            break
        }
    }
}


func Get(url string) ([]byte, error) {
    m := make(map[string]interface{})
    data, err := json.Marshal(m)
    if err != nil {
        return nil, err
    }
    body := bytes.NewReader(data)
    req, _ := http.NewRequest("Get", url, body)
    req.Header.Add("content-type", "application/json")

    client := &http.Client{
        Transport: tr,
    }
    res, err := client.Do(req)
    if res != nil {
        defer res.Body.Close()
    }
    if err != nil {
        return nil, err
    }
    resBody, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    return resBody, nil
}

やっていることは非常にシンプルで、http://www.baidu.com/をループしてリクエストし、応答を待っています。

見たところ、問題はなさそうです。

コードを実行すると、確かに正常にメッセージを送受信できます。

しかし、このコードをしばらく実行していると、i/o timeout のエラーが発生します。

これは最近調査した問題で、比較的簡単に踏み込むことができる落とし穴です。このコードを簡略化して示します。

実際の生産環境で発生した現象は、golang サービスが HTTP 呼び出しを行う際、http.Transportで 3 秒のタイムアウトを設定しているにもかかわらず、時折 i/o timeout のエラーが発生します。

しかし、下流サービスを確認すると、下流サービスは実際には 100ms で応答を返しています。

調査#

img

五層のネットワークプロトコルに対応するメッセージ体の変化を分析します。

非常に奇妙です。サーバー側の処理時間は 100ms であるにもかかわらず、クライアントのタイムアウトは 3 秒に設定されているのに、なぜタイムアウトエラーが発生するのでしょうか?

ここで考えられる可能性は 2 つあります。

  • サーバー側のログは、実際にはサーバー側のアプリケーションが出力したログに過ぎません。しかし、クライアントアプリケーションがデータを送信した後、クライアントの伝送層、ネットワーク層、データリンク層、物理層を経由し、サーバーの物理層、データリンク層、ネットワーク層、伝送層を経てサーバーのアプリケーション層に到達します。サーバー側のアプリケーション層での処理時間は 100ms ですが、残りの 3 秒 - 100ms はプロセス全体の各層での遅延に費やされる可能性があります。例えば、ネットワークが悪化した場合、伝送層 TCP がパケットを捨てて再送するなどの理由です。
  • ネットワークに問題がない場合、クライアントからサーバーへのリンク全体の送受信プロセスはおおよそ 100ms 程度である可能性があります。クライアントの処理ロジックの問題が原因でタイムアウトが発生します。

一般的に、問題が発生した場合、大部分のケースでは底層のネットワークに問題があるわけではありません。自分の問題を大胆に疑うべきです。もし疑念が残る場合は、パケットをキャプチャして確認してみてください。

img

キャプチャ結果

最初の三回のハンドシェイク(赤枠の部分)から、最後にタイムアウトエラーが発生するまでの間隔を見てみると、time 列は 7 から 10 まで、確かに 3 秒の間隔があります。また、右下の青枠は 51169 ポートから 80 ポートへのリセット接続です。

80 ポートはサーバーのポートです。言い換えれば、クライアントが 3 秒のタイムアウトで接続を自発的に切断したことを示しています。

しかし、三回のハンドシェイクからクライアントがタイムアウトを自発的に切断するまでの間に、実際には非常に多くの HTTP リクエストが行われています。

コードを見直して、タイムアウト設定の方法を確認します。

tr = &http.Transport{
    MaxIdleConns: 100,
    Dial: func(netw, addr string) (net.Conn, error) {
        conn, err := net.DialTimeout(netw, addr, time.Second*2) // 接続タイムアウトを設定
        if err != nil {
            return nil, err
        }
        err = conn.SetDeadline(time.Now().Add(time.Second * 3)) // データ送受信のタイムアウトを設定
        if err != nil {
            return nil, err
        }
        return conn, nil
    },
}

ここでの 3 秒のタイムアウトは、接続後にカウントが始まるため、単一の呼び出しの開始時にカウントが始まるタイムアウトではありません。

コメントには次のように書かれています。

SetDeadline は、接続に関連付けられた読み取りおよび書き込みの期限を設定します。

タイムアウトの原因#

皆さんは HTTP がアプリケーション層のプロトコルであり、伝送層は TCP プロトコルであることを知っています。

HTTP プロトコルは 1.0 以前、デフォルトで短い接続を使用しており、毎回リクエストを発起するたびに TCP 接続を確立し、データを送受信し、その後切断します。

TCP 接続は毎回三回のハンドシェイクが必要です。切断する際には四回の揮手が必要です。

実際には、毎回新しい接続を確立する必要はありません。確立した接続を切断せずに再利用すれば良いのです。

そのため、HTTP プロトコルは 1.1 以降、デフォルトで長い接続を使用します。具体的な関連情報は以前のこの記事を参照してください。

したがって、golang 標準ライブラリもこの実装を互換性を持たせています。

接続プールを確立し、各ドメインに対して TCP 長接続を確立します。例えば、http://baidu.comhttp://golang.comは異なるドメインです。

最初にhttp://baidu.comドメインにアクセスするとき、新しい接続が確立されます。その後、使用後に空き接続プールに放置されます。次回再度http://baidu.comにアクセスする際には、この接続を再利用します。

img

接続の再利用

なぜ同じドメインである必要があるのか:1 つのドメインは 1 つの接続を確立し、1 つの接続は 1 つの読み取り goroutine と 1 つの書き込み goroutine に対応します。したがって、同じドメインであるため、最終的に 3 つの goroutine が漏れます。異なるドメインの場合、1 + 2 * N の goroutine が漏れることになります。N はドメイン数です。

最初のリクエストが 100ms かかると仮定し、http://baidu.comに対してリクエストを送信した後、接続は空き接続プールに放置されます。次回再利用する際、29 回繰り返すと、2900ms かかります。

30 回目のリクエストでは、接続が確立されてからサーバーが応答するまでに 3000ms かかり、ちょうど設定された 3 秒のタイムアウトしきい値に達します。この時、クライアントはタイムアウトエラーを報告します。

この時、サーバー側は実際には 100ms しかかかっていませんが、前の 29 回の遅延が長くなってしまったためです。

つまり、http.Transportで設定されたerr = conn.SetDeadline(time.Now().Add(time.Second * 3))と長接続を使用している場合、サーバー側の処理がどれだけ早くても、クライアントが設定したタイムアウトがどれだけ長くても、常にタイムアウトエラーが発生する瞬間が存在します。

正しい姿勢#

本来は、各呼び出しにタイムアウトを設定することを期待していましたが、接続全体にタイムアウトを設定しているため、長接続を使用すると、たとえサーバー側の処理が早くても、クライアントが設定したタイムアウトが長くても、常にタイムアウトエラーが発生する瞬間が存在します。

したがって、コードを修正します。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

var tr *http.Transport

func init() {
    tr = &http.Transport{
        MaxIdleConns: 100,
        // ここでのコードは削除されました
        //Dial: func(netw, addr string) (net.Conn, error) {
        //  conn, err := net.DialTimeout(netw, addr, time.Second*2) // 接続タイムアウトを設定
        //  if err != nil {
        //      return nil, err
        //  }
        //  err = conn.SetDeadline(time.Now().Add(time.Second * 3)) // データ送受信のタイムアウトを設定
        //  if err != nil {
        //      return nil, err
        //  }
        //  return conn, nil
        //},
    }
}


func Get(url string) ([]byte, error) {
    m := make(map[string]interface{})
    data, err := json.Marshal(m)
    if err != nil {
        return nil, err
    }
    body := bytes.NewReader(data)
    req, _ := http.NewRequest("Get", url, body)
    req.Header.Add("content-type", "application/json")

    client := &http.Client{
        Transport: tr,
        Timeout: 3*time.Second,  // タイムアウトをここで設定します。これは各呼び出しのタイムアウトです。
    }
    res, err := client.Do(req) 
    if res != nil {
        defer res.Body.Close()
    }
    if err != nil {
        return nil, err
    }
    resBody, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    return resBody, nil
}

func main() {
    for {
        _, err := Get("http://www.baidu.com/")
        if err != nil {
            fmt.Println(err)
            break
        }
    }
}

コメントを見てみると、変更点は 2 つあります。

  • http.Transportの接続時のタイムアウト設定を削除しました。
  • HTTP リクエストを発起する際にhttp.Clientを作成し、ここでタイムアウトを設定します。このタイムアウトは単一のリクエストのタイムアウトとして理解できます。

これでコードが修正され、実際の生産環境での問題も解決されます。

インスタンスコードを実行すると、以下のようなエラーが発生することがあります。

Get http://www.baidu.com/: EOF

これは、呼び出しがあまりにも激しいため、http://www.baidu.com側が接続を自発的に切断したことを示しています。これは、サーバーを保護するための制限措置と考えられます。なぜなら、誰もがこのように行動すれば、サーバーはクラッシュしてしまいます。。。

解決策は非常に簡単で、各 HTTP 呼び出しの間に少しのスリープ時間を追加すれば良いのです。

これで問題は解決されました。以下では、ソースコードレベルで問題の原因を分析します。

ソースコード分析#

使用している Go のバージョンは 1.12.7 です。

ネットワークリクエストを発起するところから追ってみましょう。

res, err := client.Do(req)
func (c *Client) Do(req *Request) (*Response, error) {
    return c.do(req)
}

func (c *Client) do(req *Request) {
    // ...
    if resp, didTimeout, err = c.send(req, deadline); err != nil {
        // ...
    }
    // ...  
}  
func send(ireq *Request, rt RoundTripper, deadline time.Time) {
    // ...    
    resp, err = rt.RoundTrip(req)
    // ...  
} 

// ここからRoundTripのロジックに入ります
/src/net/http/roundtrip.go: 16
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    return t.roundTrip(req)
}

func (t *Transport) roundTrip(req *Request) (*Response, error) {
    // 空き接続を取得しようとします
    pconn, err := t.getConn(treq, cm)
    // ...
}

// ここでの重点はこの関数です。長接続を返します。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
    // 省略された多くのロジックがあり、以下の2つに注目します。
    // 空き接続があれば返す
    pc := <-t.getIdleConnCh(cm)

    // 接続を作成しない
    pc, err := t.dialConn(ctx, cm)

}

最初のリクエストを発起するとき、空き接続は確立されていないため、新しい接続が確立されます。同時に、読み取り goroutine と書き込み goroutine が作成されます。

img

読み書きの goroutine

上記のコードのt.dial(ctx, "tcp", cm.addr())を見てみると、もし上記のようにhttp.Transport

Dial: func(netw, addr string) (net.Conn, error) {
    conn, err := net.DialTimeout(netw, addr, time.Second*2) // 接続タイムアウトを設定
    if err != nil {
        return nil, err
    }
    err = conn.SetDeadline(time.Now().Add(time.Second * 3)) // データ送受信のタイムアウトを設定
    if err != nil {
        return nil, err
    }
    return conn, nil
},

このように設定されている場合、ここでのdialの中で実行されます。

func (t *Transport) dial(ctx context.Context, network, addr string) (net.Conn, error) {
    // ...
    c, err := t.Dial(network, addr)
    // ...
}

// /src/net/net.go
func (c *conn) Read(b []byte) (int, error) {
    // ...
    n, err := c.fd.Read(b)
    // ...
}

func (fd *netFD) Read(p []byte) (n int, error) {
    n, err = fd.pfd.Read(p)
    // ...
}

func (pd *pollDesc) waitRead(isFile bool) error {
    return pd.wait('r', isFile)
}

ここで、最初のリクエストを発起すると、接続が確立され、タイムアウトが設定されます。仮にタイムアウトが 3 秒に設定されている場合、このイベントは 3 秒後に発生し、登録された関数が実行されます。この登録イベントはnetpollDeadlineです。注意してください、このnetpollDeadlineは後で説明します。

img

読み書きの goroutine のタイムアウトイベント

タイムアウトイベントが発生し、3 秒後にデータが返されるのを待っている間、以下のように待機します。

// /src/net/http/transport.go: 1642
func (pc *persistConn) readLoop() {
    //...
    for alive {
        _, err := pc.br.Peek(1)  // サーバーからのデータをブロックして読み取ります
    //...
}

その後、次のコードに従います。

src/bufio/bufio.go: 129
func (b *Reader) Peek(n int) ([]byte, error) {
    // ...
    b.fill() 
    // ...   
}

func (b *Reader) fill() {
    // ...
    n, err := b.rd.Read(b.buf[b.w:])
    // ...
}

最終的にruntime_pollWaitに到達します。この関数は、データが返されるのを待機することを簡単に理解できます。

//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {

    // 1. ネットワークが正常でデータが返される場合は抜け出す
  for !netpollblock(pd, int32(mode), false) {
    // 2. エラーが発生した場合も抜け出す
        err = netpollcheckerr(pd, int32(mode))
        if err != 0 {
            return err
        }
    }
    return 0
}

このコードの全体の流れは、データを待機し、待機の結果は次の 2 つです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。