8 チャンネル、リフレクション、ネットワーク プログラミング [Go 言語チュートリアル]
1チャンネル
errChan := make(chan err) パイプ長が指定されていない場合は、バッファなしのチャネルが作成されます。
- バッファなしチャネルの場合、送信操作と受信操作は同期します。送信操作は受信側が値を受信する準備ができるまで待機し、受信操作は送信側が値を送信する準備ができるまで待機します。したがって、デッドロックを避けるために、バッファリングされていないチャネルでの送信操作と受信操作は別のコルーチンで実行する必要があります。
Go では、チャネルの長さは固定されており、一度作成すると変更することはできません。make 関数を使用してチャネルを作成する場合、オプションでチャネルの容量 (長さ) を指定できます。
チャネルの容量を指定しない場合、デフォルトでバッファなしのチャネル、つまり長さが 0 のチャネルになります。バッファーなしチャネルは、コルーチンが値を受信する準備ができるまで送信操作をブロックし、コルーチンが値を送信する準備ができるまで受信操作をブロックします。
バッファなしチャネルの場合、送信操作と受信操作は同期します。送信操作は受信側が値を受信する準備ができるまで待機し、受信操作は送信側が値を送信する準備ができるまで待機します。したがって、デッドロックを避けるために、バッファリングされていないチャネルでの送信操作と受信操作は別のコルーチンで実行する必要があります。
バッファリングされたチャネルの場合、チャネルの作成時にチャネルの容量を指定できます。バッファリングされたチャネルでは、すぐにブロックせずに送信操作が可能になり、チャネルのバッファがいっぱいになった場合にのみブロックされます。同様に、受信操作では、チャネルのバッファが空の場合にのみブロックされます。
チャネルの容量はチャネルのバッファ サイズにのみ影響し、チャネルが保持できる値の数には影響しないことに注意してください。チャネルの容量に関係なく、受信者がタイムリーに値を受信する限り、任意の数の値をチャネルに送信できます。
要約すると、チャネルの容量とは、チャネルが保存できる値の数ではなく、チャネルのバッファ サイズを指します。バッファなしチャネルの容量は 0 ですが、バッファ付きチャネルでは 0 より大きい任意の容量を指定できます。
1.1 コンセプトとクイックスタート
channel:管道,主要用于不同goroutine之间的通讯
要件: 次に、1 ~ 200 の各数値の階乗を計算し、各数値の階乗をマップに入力します。ついに示されました。リクエストはゴルーチンを使用して実行されます
アイデアの分析:
- goroutine を使用して完了すると効率的ですが、同時実行性/並列性のセキュリティの問題が発生します。
- これにより、異なるゴルーチンがどのように通信するかという疑問が生じます。
package main
import(
"fmt"
"time"
)
var (
myMap = make(map[int]int, 10)
)
//计算n!,将计算结果放入myMap
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res //concurrent map writes?
}
func main(){
//开启200个协程
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10s[防止主线程直接跑完,而协程中的任务未完成]
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
コードを実行すると、同時実行の問題が発生します。
異なるゴルーチン間で通信する方法:
- グローバル変数のミューテックス
- パイプライン チャネルを使用して解決する
1 ~ 200 の各数値の階乗を計算し、各数値の階乗をマップに入力します。ついに示されました。次の方法で改善します。
① グローバル変数を使用したミューテックスロック【同時実行性、並列性の問題】
グローバル変数 m が追加されていない場合、リソース競合の問題が発生し、次のメッセージが表示されます: マップの同時書き込み
ミューテックスに参加する
数値の階乗が非常に大きいため、結果は範囲外になります。階乗を sum += i に変更できます。
package main
import(
"fmt"
"time"
"sync"
)
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
//sync 是包, synchronized 同步
//Mutex:是互斥
lock sync.Mutex
)
//计算n!,将计算结果放入myMap
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res += int(i)
}
//加锁
lock.Lock()
myMap[n] = res //concurrent map writes?
//解锁
lock.Unlock()
}
func main(){
//开启200个协程
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10s[防止主线程直接跑完,而协程中的任务未完成]
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
②チャンネルを利用する
1. チャンネルが必要な理由
- なぜチャンネルが必要なのでしょうか?
- 以前は、Goroutine 通信を解決するためにグローバル変数ロック同期が使用されていましたが、完全ではありませんでした。
- すべてのゴルーチンが完了するまでメインスレッドが待機する時間を決定するのは困難ですが、ここで設定した 10 秒は単なる推定値です。
- メインスレッドが長時間スリープすると待機時間が長くなりますが、待機時間が短い場合は動作状態のゴルーチンが残っている可能性があり、メインスレッドの終了時にそれらも破棄されます。
- 通信はグローバル変数のロックと同期によって実現され、グローバル変数の読み取りと書き込みに複数のコルーチンを使用しません。
- 上記の分析はすべて、新しいコミュニケーションメカニズムであるチャネルを必要としています。
2. 基本的な紹介
- チャネルの本質はデータ構造 - キュー [概略図]
- データは先入れ先出し [FIFO: 先入れ先出し]
- スレッド セーフ。複数のゴルーチンがアクセスする場合、ロックする必要はありません。つまり、チャネル自体はスレッド セーフです。
- チャネルには型があり、文字列チャネルには文字列型のデータのみを保存できます。
3. チャネルの宣言と定義
- var 変数名 chan データ型
例:
var intChan chan int (intChan は int データの保存に使用されます)
var mapChan chan map[int]string (mapChan は map[int]string 型の保存に使用されます) var perChan chan person var
perChan2 chan *人
…- 説明:
チャネルは参照型です
。チャネルはデータを書き込むために初期化する必要があります。つまり、チャネルは make が入力された後でのみ使用でき、intChan は整数 int のみを書き込むことができます。
4. クイックスタートと注意事項
(1) クイックスタート:
パイプラインの初期化、パイプラインへのデータの書き込み、パイプラインからのデータの読み取りと基本的な注意事項
package main
import (
"fmt"
)
func main(){
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan的值=%v intChan本身地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- -50
//intChan <- 100 //注意:我们在给管道写入数据时,不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//5. 从管道中读取数据
var num2 int
num2 = <- intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//6. 在没有使用协程的情况下,如果管道中的数据已经全部取出,再取就会报告deadlock
num3 := <- intChan
num4 := <- intChan
fmt.Printf("num3=%v, num4=%v", num3, num4)
// num5 := <- intChan
// fmt.Println("num5=", num5) //fatal error: all goroutines are asleep - deadlock!
}
(2) 注意すべき事項:
- 指定されたデータ型のみをチャネルに保存できます
- チャンネルのデータがいっぱいになると、それ以上入れられなくなります
- データがチャンネルから取り出された場合は、引き続き入れ続けることができます
- コルーチンを使用しない場合、チャネルデータを取得して再度取得するとデッドロックが報告されます。
5. チャネルの使用
練習問題:
package main
import (
"fmt"
"math/rand"
"time"
"strconv"
)
type Person struct {
Name string
Age int
Address string
}
func main(){
var personChan chan Person
//make给chan开辟空间
personChan = make(chan Person, 10)
//取纳秒时间戳作为种子,保证每次的随机种子都不同
//给rand种种子
rand.Seed(time.Now().UnixNano())
for i := 1; i <= 10; i++ {
index := rand.Int()
fmt.Println("index===", index)
person := Person{
Name: "zhangsan" + strconv.Itoa(index),
Age: i,
Address: "beijing" + strconv.Itoa(index),
}
personChan <- person
}
len := len(personChan)
for i := 0; i < len; i++ {
p := <- personChan
fmt.Println(p)
}
}
6. チャネルのクローズとトラバーサル
(1) チャネルの閉鎖
組み込み関数 close を使用してチャネルを閉じます。チャネルを閉じると、チャネルにデータを書き込むことはできなくなりますが、チャネルからデータを読み取ることはできます。
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 5)
intChan <- 10
intChan <- 20
close(intChan) //close
//关闭之后不能再向chan写入数据,但是可以读取
// intChan <- 30 //panic: send on closed channel
n1 := <- intChan
fmt.Println("n1=", n1) //n1= 10
}
(2) チャネルトラバーサル
チャネルは範囲トラバーサルをサポートしています。2 つの詳細に注意してください。
- トラバース時にチャネルが閉じられていない場合、デッドロック エラーが表示されます
- トラバース中にチャネルが閉じている場合、データは通常どおりトラバースされ、トラバースが完了するとトラバースが終了します。
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 5)
intChan <- 10
intChan <- 20
intChan <- 50
close(intChan) //close
//关闭之后不能再向chan写入数据,但是可以读取
// intChan <- 30 //panic: send on closed channel
n1 := <- intChan
fmt.Println("n1=", n1) //n1= 10
for v := range intChan {
fmt.Printf("value=%v\n", v)
}
}
7. 包括的な例
- 要件:
1 から 8000 までの数を数える必要があるのはどれが素数ですか? goroutine
とチャネルの知識を使用して完了- 分析のアイデア:
従来の方法は、サイクルを使用して各数値が素数であるかどうかを判断することです [ok]。
同時並列方式を使用すると、素数を数えるタスクが複数 (4 個) のゴルーチンに割り当てられて完了するため、タスクの完了時間が短くなります。
package main
import (
"fmt"
"time"
)
//向intChan放入1-8000个数
func putNum(intChan chan int){
for i := 1; i <= 8000; i++ {
intChan <- i
}
//关闭chan
close(intChan)
}
//从intChan取出数据,并判断是否是素数,如果是,就放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool){
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <- intChan
if !ok {
//取不到数据了,就退出
break
}
flag = true //假设是素数
for i := 2; i < num; i++ {
if num % i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据,退出")
//这里我们还不能关闭primeChan
//向exitChan写入true
exitChan <- true
}
func main(){
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
//标识退出的管道
exitChan := make(chan bool, 4)
//开启一个协程,向intChan放入1-8000个数
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否是素数
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//主线程进行处理
go func(){
for i:= 0; i < 4; i++{
<-exitChan
}
//当我们从exitChan取出了4个结果,就可以放心的关闭primeChan
close(primeChan)
}()
//遍历primeChan,把结果取出
for {
res, ok := <- primeChan
if !ok {
break
}
//将结果取出
fmt.Printf("素数=%d\n", res)
}
}
結果:
8. 読み取り専用、書き込み専用パイプと注意事項
- 読み取り専用、書き込み専用のパイプ:
- 予防
- チャネルは読み取り専用または書き込み専用として宣言できます。
- 読み取り専用と書き込み専用のケース
3) select を使用してパイプラインからデータをフェッチする際のブロック問題を解決する
用途を選択してください:
package main
import (
"fmt"
"time"
)
func main(){
//使用select可以解决管道取数据的阻塞问题
//1. 定义一个管道 10 int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2. 定义一个管道 5 string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭,则会因阻塞导致deadlock
//可是我们在实际开发中,我们可能不好确定什么时候关闭管道
//办法:我们可以使用select方式解决
//label:
for {
select {
//注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
case v := <- intChan:
fmt.Printf("从intChan读取的数据=%d\n", v)
time.Sleep(time.Second)
case v := <- stringChan:
fmt.Printf("从stringChan读取的数据=%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("不玩了,都取不到了【程序员可以在这里加入自己的逻辑】\n")
time.Sleep(time.Second)
return
//break label
}
}
}
2回の反射
2.1 コンセプト
- リフレクションは変数の型(type)、カテゴリ(kind)など変数の様々な情報を実行時に動的に取得できます。
- 構造体変数の場合は、構造体そのものの情報(構造体のフィールドやメソッドを含む)も取得できます。
- リフレクションを通じて、変数の値を変更したり、関連するメソッドを呼び出すことができます。
- リフレクションを使用するには、インポート (「リフレクト」) が必要です。
5) リフレクションの一般的なアプリケーション シナリオ
2.2 リフレクションにおける重要な機能
- 変数、interface{}、reflect.Value は交換可能であり、実際の開発でよく使用されます。
2.3 クイックスタート
(構造体の型、インターフェース{}、reflect.Value) に対するリフレクションの基本的な操作を示すケースを作成してください。
package main
import (
"reflect"
"fmt"
)
type Student struct {
Name string
Age int
}
func reflectTest01(b interface{
}){
//通过反射获取到传入变量的type、kind值
//1. 先获取到reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)
//2. 获取到reflect.Value
rVal := reflect.ValueOf(b)
n2 := 2 + rVal.Int()
fmt.Println("n2=", n2)
fmt.Printf("rVal=%v rType=%T\n", rVal, rType)
//下面我们将rVal转成interface{}
iV := rVal.Interface()
//将interface{}通过断言转成需要的类型
num2 := iV.(int)
fmt.Println("num2=", num2)
}
//对结构体的反射
func reflectTest02(b interface{
}){
//通过反射获取到传入的变量的type、kind,值
//1. 先获取到reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)
//2. 获取到reflect.Value
rVal := reflect.ValueOf(b)
//下面我们将rVal转成interface{}
iV := rVal.Interface()
fmt.Printf("iv=%v iv type=%T\b", iV, iV)
//将interface{}通过断言转成需要的类型
//这里,我们使用类型断言【同学们可以使用switch的断言形式来更加灵活的判断】
stu, ok := iV.(Student)
if ok {
fmt.Printf("stu.Name=%v\n", stu.Name)
}
}
func main(){
//1. 基本数据类型 反射
var num int = 100
reflectTest01(num)
//2. 定义一个Student实例
stu := Student{
Name: "tom",
Age: 20,
}
reflectTest02(stu)
}
2.4 リフレクションの詳細と考慮事項
- reflect.Value.Kind、変数のカテゴリを取得し、定数を返します
- Type と Kind の違い [type はより具体的]
Type はタイプ、Kind はカテゴリ、Type と Kind は同じでも異なっていても構いません 例: var num int = 10 num の Type は int で、Kind はint 例
: var stu Student stu の Type は pkg1.Student、Kind は struct
- リフレクションによる変数の変更 SetXxx メソッドを使用して設定する場合は、渡された変数の値を変更するために、対応するポインター型を介して設定を完了する必要があることに注意してください。 Value.Elem() メソッド
- Reflect.Value.Elem() をどのように理解すればよいでしょうか?
2.5 包括的なケース
リフレクションを使用して構造体のフィールドを走査し、構造体のメソッドを呼び出し、構造体のラベルの値を取得します。
package main
import (
"reflect"
"fmt"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成绩"`
Sex string
}
//方法,返回两数的和
func (s Monster) GetSum(n1, n2 int) int {
return n1 + n2
}
//方法,接收四个值,给结构体赋值
func (s Monster) Set(name string, age int, score float32, sex string){
s.Name = name
s.Age = age
s.Score = score
s.Sex = sex
}
//方法,显示结构体的值
func (s Monster) Print(){
fmt.Println("----start----")
fmt.Println(s)
fmt.Println("----end----")
}
func TestStruct(a interface{
}){
//获取reflect.Type类型
typ := reflect.TypeOf(a)
//获取reflect.Value类型
val := reflect.ValueOf(a)
//获取到a对应的类别
kd := val.Kind()
//如果传入的不是struct,就退出
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}
//是结构体,获取该结构体有几个字段
num := val.NumField()
fmt.Printf("struct has %d fields\n", num)
for i := 0; i < num; i++ {
fmt.Printf("Field %d值为=%v\n", i, val.Field(i))
//获取到struct标签,注意需要通过reflect.Type来获取tag标签的值
tagVal := typ.Field(i).Tag.Get("json") //因为前面定义结构体用到了'json标签'
//如果该字段有tag标签就显示,否则就不显示
if tagVal != "" {
fmt.Printf("Field %d tag 为=%v\n", i, tagVal)
}
}
//获取到该结构体有多少个方法
numOfMethod := val.NumMethod()
fmt.Printf("struct has %d methods\n", numOfMethod)
//var params []reflect.Value
//方法的排序默认是按照函数名的排序(ASCII码)
val.Method(1).Call(nil) //获取到第二个【下标为1】方法,调用它 【传参为空】
//调用结构体的第1个方法 Method(0)
var params []reflect.Value //声明了 []reflect.Value()
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params) //传入的参数是[]reflect.Value,返回[]reflect.Value
fmt.Println("res=", res[0].Int()) //返回结果,返回的结果是[]reflect.Value
}
func main(){
var a Monster = Monster{
Name: "黄鼠狼精",
Age: 400,
Score: 30.9,
}
TestStruct(a)
}
3 ネットワークプログラミング
3.1 概念と予備知識
- TCP ソケット プログラミングはネットワーク プログラミングの主流です。これが Tcp ソケット プログラミングと呼ばれる理由は、基礎となる層が Tcp/ip プロトコルに基づいているためです (例: QQ チャット)。
- b/s 構造を使用した http プログラミングでは、ブラウザを使用してサーバーにアクセスするときに http プロトコルを使用しますが、http の最下層は依然として tcp ソケットで実装されています。例: Jingdong Mall [これは Go Web 開発のカテゴリに属します]
- コンピュータ間で通信するには、ネットワーク ケーブル、ネットワーク カード、またはワイヤレス ネットワーク カードが必要です。
4. 同意
5. ポート
- 0 は予約ポートです。
- 1-1024 は固定ポート (プログラマは使用しない) で、ウェルノウン ポートとも呼ばれます。つまり、一部のプログラムで固定的に使用され、一般のプログラマは使用しません。
- 共通ポート: 22: SSH リモート ログイン プロトコル 23: Telnet を使用 21: FTP を使用
25: SMTP サービスを使用 80: IIS を使用 7: エコー サービス- 1025 ~ 65535 は動的ポートです。
これらのポートはプログラマが使用できます。
注意:
-
コンピューター上で開くポートをできるだけ少なくします (特にサーバーとして)
-
ポートは 1 つのプログラムでのみ監視できます
-
netstat –an を使用すると、このマシンでどのポートがリッスンしているかを確認できます。
-
netstat –anb を使用すると、リスニング ポートの PID を表示し、タスク マネージャーと連携して安全でないポートを閉じることができます。
3.2 クイックスタート
- サーバー側の処理の流れ
- ポート 8888 でリッスンする
- クライアントの TCP 接続を受信し、クライアントとサーバー間の接続を確立します。
- リンクのリクエストを処理するゴルーチンを作成します (通常、クライアントはリンク経由でリクエスト パケットを送信します)。
- お客様の処理フロー
- サーバーとの接続を確立する
- リクエストデータを送信[端末]、サーバーから返された結果データを受信
- リンクを閉じる
①サーバー関数とコード
関数:
- ポート 8888 でリッスンし、複数のクライアントとのリンクを作成できるサーバー側プログラムを作成します。
リンクが成功すると、クライアントはデータを送信できるようになり、サーバーはデータを受け取り、端末に表示します。最初に Telnet を使用してテストします。次に、テストするクライアント プログラムを作成します。
コード:
package main
import (
"fmt"
"net"
)
func process(conn net.Conn) {
//循环接收客户端发送的数据
defer conn.Close()
for {
//创建一个新的切片
buf := make([]byte, 1024)
//1. 等待客户端通过conn发送消息
//2. 如果客户端没有write(发送消息),那么协程就阻塞在这里
fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
n, err := conn.Read(buf) //从conn中读取
if err != nil {
fmt.Printf("客户端退出 err=%v", err)
return
}
//3. 显示客户端给服务端发送的数据(打印在控制台上)
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听...")
//1. tcp表示使用的网络协议是tcp
//2. 0.0.0.0:8888表示在本地监听8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close() //延时关闭listen
//循环等待客户端来连接服务端
for {
//等待客户端连接
fmt.Println("等待客户端来连接...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)
} else {
fmt.Printf("Accept() success con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
}
//这里准备起一个协程,为客户端服务
go process(conn)
}
}
実行後の効果:
服务器开始监听...
等待客户端来连接...
②クライアント関数とコード
- サーバー上のポート 8888 にリンクできるクライアント プログラムを作成します。
- クライアントは 1 行のデータを送信して終了できます。
- ターミナルからデータを入力(1行入力、1行送信)し、サーバーに送信できます。
- ターミナルに「exit」と入力してプログラムを終了します。
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "192.168.1.100:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 表示标准输入:【终端】
for {
//从终端读取一行用户输入,并发送给服务端
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
//功能二:当用户输入exit就退出
line = strings.Trim(line, "\r\n")
if line == "exit" {
fmt.Println("客户端退出...")
break
}
n, err := conn.Write([]byte(line))
if err != nil {
fmt.Println("conn Write err=", err)
}
fmt.Printf("客户端发送了 %d字节的数据\n", n)
}
}