package basic import ( "bufio" "context" "errors" "fmt" "net" "os" "reflect" "strconv" "strings" "sync" "sync/atomic" "time" "unsafe" ) /** make 和 new 的区别 new和make都是在堆上分配内存。 堆和栈的区别 堆(Heap):用于动态内存分配,如使用new或make函数分配的内存。堆中的内存手动分配和释放,因此存在内存泄漏的风险。 栈(Stack):用于存储局部变量、函数调用的上下文等。栈中的内存由编译器自动管理,不需要手动干预。 new和make的使用场景和区别 new:用于为结构体、数组、指针等类型分配内存。它返回一个指向新分配内存的指针。例如,new(int)会分配一个整数大小的内存,并返回一个指向该内存的指针。 make:专门用于创建切片(slice)、映射(map)、通道(channel)等内置数据结构。它返回一个对数据结构的引用,而不是指针。例如,make([]int, 0, 100)创建一个长度为0、容量为100的整数切片。 */ func MakeAndNew() { // 使用 new 创建一个整数指针 numPtr := new(int) fmt.Println("numPtr:", *numPtr) // 输出:numPtr: 0,因为 int 类型的零值是 0 // 使用 new 创建一个长度为 5 的整数数组指针 arrPtr := new([5]int) fmt.Println("arrPtr:", arrPtr) // 输出:arrPtr: &[0 0 0 0 0] // 使用 make 创建一个切片 slice := make([]int, 3, 5) fmt.Println("slice:", slice) // 输出:slice: [0 0 0],因为 int 类型的零值是 0 // 使用 make 创建一个map m := make(map[string]int) fmt.Println("m:", m) // 输出:m: map[] // 使用 make 创建一个信道 ch := make(chan int) fmt.Println("ch:", ch) // 输出:ch: 0xc000016090,信道的地址 } /** 1 长度固定 vs 动态长度:数组的长度在声明时就确定了,无法改变;而切片的长度可以动态增长或缩小。 2 内存分配方式:数组在声明时会分配固定大小的连续内存空间;而切片则是引用一个数组,通过指针指向底层数组,并记录切片的长度和容量。 3 传递方式:数组在函数传递时会进行值拷贝,即传递的是数组的副本;而切片在函数传递时是通过引用传递,传递的是指向底层数组的指针。 4 长度信息:数组的长度是固定的,通过len()函数获取;而切片有两个长度信息:长度(len())和容量(cap()),分别表示当前切片的实际长度和底层数组的容量。 5 灵活性:切片可以动态增长或缩小,方便进行数据操作和处理;而数组的长度固定,无法动态改变。 6 总的来说,数组适合存储固定长度的数据,而切片适合存储不固定长度的数据,并且在实际开发中更常用。 */ // runtime/slice.go type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 } func ArrayAndSlice() { // 声明一个整数数组 arr := [3]int{} fmt.Println("arr:", arr) var a1 [3]int fmt.Println("a1:", a1) a2 := [...]int{} fmt.Println("a2:", a2) // 声明一个整数切片 slice := []int{1, 2, 3} fmt.Println("slice:", slice) // 修改数组的元素 arr[0] = 100 fmt.Println("修改后的 arr:", arr) // 输出:修改后的 arr: [100 0 0] // 修改切片的元素 slice[0] = 100 fmt.Println("修改后的 slice:", slice) // 输出:修改后的 slice: [100 2 3] } /* * defer,多个 defer 的顺序,defer 在什么时机会修改返回值? 1 defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic; 2 defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。 3 多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中 defer,return,return value(函数返回值) 执行顺序:首先return,其次return value,最后defer。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针 https://blog.csdn.net/Cassie_zkq/article/details/108567205 */ func Defer1() { //defer 先进后出方式执行 var whatever [5]struct{} defer fmt.Println(222) for i := range whatever { defer fmt.Println(i) } /** 输出: 4 3 2 1 0 222 */ } func Defer2() { unres := unnamedReturn() fmt.Println("unres:", unres) // 输出:res: 10 nres := namedReturn() fmt.Println("nres:", nres) // 输出:res: 15 } func unnamedReturn() int { result := 10 ret := &result defer func() { *ret += 5 fmt.Printf("ret:%v \n", *ret) //15 }() return result } func namedReturn() (result int) { defer func() { result += 5 }() result = 10 return } /** uint: uint8-uint64 32位操作系统:uint32 64位操作系统:uint64 */ func Uint() { var a uint8 // 0~255(0~2^8-1) a = 255 var b uint8 = 1 b = 1 fmt.Printf("a=%v,b=%v,a+b=%v \n", a, b, a+b) } /* rune 相当int32 golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8 byte 等同于int8,常用来处理ascii字符 rune 等同于int32,常用来处理unicode或utf-8字符 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. */ func Rune() { // 使用 rune 类型表示单个字符 r := 'A' fmt.Printf("r: %c, type: %T \n", r, r) // 输出:r: A, type: int32 // 使用 rune 类型处理字符串中的字符 s := "Hello, 世界" runes := []rune(s) fmt.Println("runes: \n", runes) // 输出:runes: [72 101 108 108 111 44 32 19498 30028] // 遍历字符串中的字符 for _, r := range s { fmt.Printf("%c ", r) } // 输出:H e l l o , 世 界 } /* * goroutine 协程 goruntime.GOMAXPROCS(1) 设置最大并发数为1 waitGroup.Wait() 等待所有协程执行完毕 */ var wg sync.WaitGroup func Goroutine() { wg.Add(5) ctx, cancel := context.WithCancel(context.Background()) collectStart := time.Now() collectStartMills := collectStart.UnixNano() / 1e6 ctx = context.WithValue(ctx, "StartTime", collectStartMills) for i := 0; i < 5; i++ { go sayHello(ctx, i) } go func() { defer cancel() //暂时没有生效,需要再看看 time.Sleep(time.Second * 2) fmt.Printf("Context cancel\n") }() wg.Wait() fmt.Printf("is aaaaaaaa...\n") } func sayHello(ctx context.Context, i int) { go func() { defer wg.Done() if i == 4 { time.Sleep(time.Second * 10) } startime := ctx.Value("StartTime").(int64) fmt.Printf("hello, world %d,time:%v \n", i, startime) }() for { select { case <-ctx.Done(): fmt.Println("ctx.Done") return default: fmt.Println("is runing...") time.Sleep(time.Second * 1) } } } /** reflect TypeOf(nil) 返回的是 reflect.Type 类型,可以通过该类型获取到具体的类型信息 IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。 IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。 IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。 */ func ReflectGetValue() { a := 100 atp := reflect.TypeOf(a) avu := reflect.ValueOf(a) fmt.Println(atp, avu) fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) // nil值 fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // 实例化一个匿名结构体 b := struct{}{} // 尝试从结构体中查找"abc"字段 fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // 尝试从结构体中查找"abc"方法 fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid()) // map c := map[string]int{} // 尝试从map中查找一个不存在的键 fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("mm")).IsValid()) } /** 在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成, 并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。 */ func ReflectSetValue() { a := 100 if reflect.TypeOf(a).Kind() == reflect.Int { /**ca := reflect.ValueOf(a) ca.SetInt(1000) fmt.Println(ca) //修改的是副本,reflect包会引发panic */ reflect.ValueOf(&a).Elem().SetInt(1000) } fmt.Println(a) } type Student struct { Name string `json:"name"` Age int `json:"age"` Sex string `json:"sex"` Address string `json:"address"` } /* 任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体, 可以通过反射值对象(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。 Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息。 NumField() int 返回结构体成员字段数量。 FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。 FieldByIndex(index []int) StructField 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。 FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。 NumMethod() int 返回该类型的方法集中方法的数目 Method(int) Method 返回该类型方法集中的第i个方法 MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法 type StructField struct { // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。 // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers Name string PkgPath string Type Type // 字段的类型 Tag StructTag // 字段的标签 Offset uintptr // 字段在结构体中的字节偏移量 Index []int // 用于Type.FieldByIndex时的索引切片 Anonymous bool // 是否匿名字段 } */ func ReflectStruct() { st1 := Student{ Name: "zhangsan", Age: 20, Sex: "man", Address: "beijing", } st2 := &Student{ Name: "lisi", Age: 21, Sex: "man", Address: "wuhan", } /* st1 是一个 Student 类型的值,存储的是结构体的副本。 st2 是一个指向 Student 类型的指针,存储的是结构体的内存地址。 在实际使用中,如果需要修改原始结构体的内容或者关心性能(例如,避免复制大型结构体),通常会选择使用指针。 如果结构体较小且不需要修改原始内容,可以直接使用值类型。 */ rt1 := reflect.TypeOf(st1) fmt.Printf("name:%v,kind:%v \n", rt1.Name(), rt1.Kind()) rt2 := reflect.TypeOf(st2) fmt.Printf("name:%v,kind:%v \n", rt2.Name(), rt2.Kind()) for i := 0; i < rt1.NumField(); i++ { field := rt1.Field(i) fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json")) } /*for j := 0; j < rt2.NumField(); j++ { field := rt2.Field(j) fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json")) } 报错panic: reflect: NumField of non-struct type *basic.Student 这个错误信息表明在使用反射(reflect)包时,尝试获取一个非结构体类型的字段数量。具体来说,reflect: NumField of non-struct type *basic.Student 表示你试图对一个 *basic.Student 类型的值调用 NumField 方法,但 *basic.Student 不是一个结构体类型。 在 Go 语言中,reflect 包提供了对结构体字段的反射操作。要正确使用 reflect 包,你需要确保你操作的对象确实是一个结构体类型 */ } /** 反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。 大量使用反射的代码通常难以理解。 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。 */ func ReflectGetMethod() { GetMethod(Student{}) /* reflect.DeepEqual reflect.DeepEqual 是 Go 语言中 reflect 包提供的一个函数,用于比较两个值是否深度相等。它会递归地比较两个值的所有字段和元素,包括结构体、数组、切片、映射等复杂类型。 如果两个值的类型和内容都相同,则返回 true,否则返回 false。 需要注意的是:reflect.DeepEqual 在比较某些类型的值时可能会产生意外的结果。例如,它将 nil 切片和空切片视为不相等,将 nil 映射和空映射视为不相等。 此外,它还会比较函数指针的值,这可能导致不正确的结果。因此,在使用 reflect.DeepEqual 时,请确保了解其行为,并在必要时进行适当的自定义比较。 如果你只需要比较基本类型(如整数、浮点数、字符串等)的值,可以使用 == 运算符,而无需使用 reflect.DeepEqual。对于复杂类型,可以考虑使用第三方库, 如 github.com/google/go-cmp,它提供了更强大且灵活的比较功能。 */ p1 := Person{Name: "Alice", Age: 30} p2 := Person{Name: "Alice", Age: 30} p3 := Person{Name: "Bob", Age: 25} fmt.Println(reflect.DeepEqual(p1, p2)) // 输出:true fmt.Println(reflect.DeepEqual(p1, p3)) // 输出:false } type Person struct { Name string Age int } func GetMethod(i interface{}) { t := reflect.TypeOf(i) v := reflect.ValueOf(i) for j := 0; j < v.NumMethod(); j++ { methodName := t.Method(j).Name methodType := v.Method(j).Type() fmt.Printf("method name:%s,method type:%v \n", methodName, methodType) // 通过反射调用方法传递的参数必须是 []reflect.Value 类型 var args = []reflect.Value{} v.Method(j).Call(args) } } // 给student添加两个方法 Study和Sleep(注意首字母大写) func (s Student) Study() string { msg := "好好学习,天天向上。" fmt.Println(msg) return msg } func (s Student) Sleep() string { msg := "好好睡觉,快快长大。" fmt.Println(msg) return msg } /* select golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作。 在执行select语句的时候,运行时系统会自上而下地判断每个case中的发送或接收操作是否可以被立即执行【立即执行:意思是当前Goroutine不会因此操作而被阻塞,还需要依据通道的具体特性(缓存或非缓存)】 每个case语句里必须是一个IO操作 所有channel表达式都会被求值、所有被发送的表达式都会被求值 如果任意某个case可以进行,它就执行(其他被忽略)。 如果有多个case都可以运行,Select会随机公平地选出一个执行(其他不会执行)。 如果有default子句,case不满足条件时执行该语句。 如果没有default字句,select将阻塞,直到某个case可以运行;Go不会重新对channel或值进行求值。 可处理一个或多个 channel 的发送/接收操作。 如果多个 case 同时满足,select 会随机选择一个执行。 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。 */ func ChannelSelect() { chan1 := make(chan int, 4) chan1 <- 11 select { case a := <-chan1: fmt.Printf("a:%v \n", a) default: fmt.Println("222") } //有缓冲区与无缓冲区 ch2 := make(chan int) //无缓冲通道,需要现有一个goroutine来接收数据,不然向里边发数据会panic var aaa int go func() { aaa = <-ch2 }() ch2 <- 1 fmt.Println(aaa) ch3 := make(chan int, 2) //有缓冲通道 ch3 <- 1 ab := <-ch3 fmt.Println(ab) } func Channel() { var wg sync.WaitGroup ch := make(chan int) wg.Add(10) // 要与循环里边的wg.Add(1)区别 for i := 0; i < 10; i++ { go func() { time.Sleep(time.Second * 2) ch <- i //wg.Add(1) fmt.Printf("ch <- %v \n", i) }() } go func() { for i := 0; i < 10; i++ { v := <-ch wg.Done() fmt.Printf("%v <- ch \n", v) } }() close(ch) wg.Wait() fmt.Println("main end") } /* 断言:在运行时检查接口类型的机制,通常用于确定接口的具体类型,并将其转换为该类型以便进行操作; 有两种方式:t := 变量.(T) 如果断言失败,会直接panic; t,ok := 变量.(T) func (thisObj *DescribeAppAnalysisResultController) Process(req interface{}, traceID int64) (code int32, reply interface{}) { request := req.(*DescribeAppAnalysisResultRequest) //接口类型变结构体,此处原理为断言 requestLogger := servicecom.NewRequestLogger(request) */ func CaseType() { stu := &Student{ Name: "zhangsan", Age: 18, } detectType(stu) } func detectType(req interface{}) { student, ok := req.(*Student) //断言 if ok { fmt.Printf("student.Name:%v \n", student.Name) } } /* 锁 有时候我们的代码中可能会存在多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)。 1、互斥锁;2、读写互斥锁;3、sync.WaitGroup;4、sync.Once;5、sync.Map;6、atomic包 */ var ( x int64 m sync.Mutex // 互斥锁 rwMutex sync.RWMutex mutex sync.Mutex ) func Mutex() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) } func add() { for i := 0; i < 5000; i++ { m.Lock() // 修改x前加锁 x = x + 1 m.Unlock() // 改完解锁 } wg.Done() } func RWMutex() { /* 读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待; 而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。 */ // 使用互斥锁,10并发写,1000并发读 do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s // 使用读写互斥锁,10并发写,1000并发读 do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms /* 从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。 不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来。 */ } // writeWithLock 使用互斥锁的写操作 func writeWithLock() { mutex.Lock() // 加互斥锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒 mutex.Unlock() // 解互斥锁 wg.Done() } // readWithLock 使用互斥锁的读操作 func readWithLock() { mutex.Lock() // 加互斥锁 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒 mutex.Unlock() // 释放互斥锁 wg.Done() } // writeWithLock 使用读写互斥锁的写操作 func writeWithRWLock() { rwMutex.Lock() // 加写锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒 rwMutex.Unlock() // 释放写锁 wg.Done() } // readWithRWLock 使用读写互斥锁的读操作 func readWithRWLock() { rwMutex.RLock() // 加读锁 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒 rwMutex.RUnlock() // 释放读锁 wg.Done() } func do(wf, rf func(), wc, rc int) { start := time.Now() // wc个并发写操作 for i := 0; i < wc; i++ { wg.Add(1) go wf() } // rc个并发读操作 for i := 0; i < rc; i++ { wg.Add(1) go rf() } wg.Wait() cost := time.Since(start) fmt.Printf("x:%v cost:%v\n", x, cost) } func WaitGroup() { /* WaitGroup 在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法: 方法名 功能 func (wg * WaitGroup) Add(delta int) 计数器+delta (wg *WaitGroup) Done() 计数器-1 (wg *WaitGroup) Wait() 阻塞直到计数器变为0 sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done 方法将计数器减1。通过调用 Wait 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。 */ wg.Add(1) go hello() wg.Wait() fmt.Println("ni hao ya !!!") } func hello() { defer wg.Done() fmt.Println("hello world....") } func SyncOnce() { /* 在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。 Go语言中的sync包中提供了一个针对只执行一次场景的解决方案——sync.Once,sync.Once只有一个Do方法, 延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量 (比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。 */ GetInstance() //并发安全的单例模式 } type singleton struct{} var instance *singleton var once sync.Once func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance } // 并发安全的map var smp = sync.Map{} func SyncMap() { /* sync.Map sync.Map 是 Go 语言中提供的一个并发安全的 map 类型,它是 Go 语言中 map 的替代品,它是并发安全的,并且它是通过引入 sync.RWMutex 来实现的。 在 Go 语言中,map 是无序的,而 sync.Map 是基于 sync.RWMutex 实现的,所以它是并发安全的。 sync.Map 的底层实现是通过哈希表来实现的,哈希表的底层实现是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来 */ wg := sync.WaitGroup{} // 对m执行20个并发的读写操作 for i := 0; i < 20; i++ { wg.Add(1) go func(n int) { key := strconv.Itoa(n) smp.Store(key, n) // 存储key-value value, _ := smp.Load(key) // 根据key取值 fmt.Printf("k=:%v,v:=%v\n", key, value) wg.Done() }(i) } wg.Wait() } /* atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。 除了某些特殊的底层应用,使用通道或者 sync 包的函数/类型实现同步更好 针对整数数据类型(int32、uint32、int64、uint64)我们还可以使用原子操作来保证并发安全,通常直接使用原子操作比使用锁操作效率更高。Go语言中原子操作由内置的标准库sync/atomic提供 func (o *TrinoQueryRunningExporter) AcquireLock() bool { return atomic.CompareAndSwapInt64(&o.Lock, 0, 1) } func (o *TrinoQueryRunningExporter) ReleaseLock() { atomic.CompareAndSwapInt64(&o.Lock, 1, 0) } 这里的锁就是原子操作的使用案例 */ func Atomic() { c1 := CommonCounter{} // 非并发安全 test(c1) c2 := MutexCounter{} // 使用互斥锁实现并发安全 test(&c2) c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高 test(&c3) } type Counter interface { Inc() Load() int64 } // 普通版 type CommonCounter struct { counter int64 } func (c CommonCounter) Inc() { c.counter++ } func (c CommonCounter) Load() int64 { return c.counter } // 互斥锁版 type MutexCounter struct { counter int64 lock sync.Mutex } func (m *MutexCounter) Inc() { m.lock.Lock() defer m.lock.Unlock() m.counter++ } func (m *MutexCounter) Load() int64 { m.lock.Lock() defer m.lock.Unlock() return m.counter } // 原子操作版 type AtomicCounter struct { counter int64 } func (a *AtomicCounter) Inc() { atomic.AddInt64(&a.counter, 1) } func (a *AtomicCounter) Load() int64 { return atomic.LoadInt64(&a.counter) } func test(c Counter) { var wg sync.WaitGroup start := time.Now() for i := 0; i < 1000; i++ { wg.Add(1) go func() { c.Inc() wg.Done() }() } wg.Wait() end := time.Now() fmt.Println(c.Load(), end.Sub(start)) } /* error Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch捕获异常的方式 Go 语言中使用一个名为 error 接口来表示错误类型。 error 接口只包含一个方法——Error,这个函数需要返回一个描述错误信息的字符串。当一个函数或方法需要返回错误时,我们通常是把错误作为最后一个返回值 我们可以根据需求自定义 error,最简单的方式是使用errors 包提供的New函数创建一个错误。 errors.New 当我们需要传入格式化的错误描述信息时,使用fmt.Errorf是个更好的选择。 */ func ErrorNew() { id := -1 var err error if id < 0 { err = errors.New("无效的id") fmt.Printf("id error:%v \n", err) } fmt.Println(fmt.Errorf("查询数据库失败,v err:%v \n", err)) //但是上面的方式会丢失原有的错误类型,只拿到错误描述的文本信息。 //为了不丢失函数调用的错误链,使用fmt.Errorf时搭配使用特殊的格式化动词%w,可以实现基于已有的错误再包装得到一个新的错误。 fmt.Println(fmt.Errorf("查询数据库失败,w err:%w \n", err)) //自定义结构体类型,可以自己定义结构体类型,实现 error`接口 oper := &OpError{ Op: "update", } fmt.Println(oper.Error()) } // Error OpError 类型实现error接口 func (e *OpError) Error() string { return fmt.Sprintf("无权执行%s操作", e.Op) } // OpError 自定义结构体类型 type OpError struct { Op string } /* 类型转换 strconv包 strconv包实现了基本数据类型与其字符串表示的转换,主要有以下常用函数: Atoi()、Itoa()、parse系列、format系列、append系列。 【扩展阅读】这是C语言遗留下的典故。C语言中没有string类型而是用字符数组(array)表示字符串,所以Itoa对很多C系的程序员很好理解 strconv.Atoi strconv.Itoa Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint() Format系列函数实现了将给定类型数据格式化为string类型数据的功能 */ func TypeTransfer() { s1 := "100" i1, err := strconv.Atoi(s1) // Atoi()函数用于将字符串类型的整数转换为int类型 if err != nil { fmt.Println("can't convert to int") } else { fmt.Printf("type:%T value:%#v\n", i1, i1) //type:int value:100 } v := int64(-42) s10 := strconv.FormatInt(v, 10) fmt.Printf("%T, %v\n", s10, s10) s16 := strconv.FormatInt(v, 16) fmt.Printf("%T, %v\n", s16, s16) } /* ini 在Go语言中,init()函数是一种特殊的函数,用于在程序启动时自动执行一次。它的存在为我们提供了一种机制,可以在程序启动时进行一些必要的初始化操作,为程序的正常运行做好准备 go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。 1 init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等 2 每个包可以拥有多个init函数 3 包的每个源文件也可以拥有多个init函数 4 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明) 5 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序 6 init函数不能被其他函数调用,而是在main函数执行之前,自动被调用 init函数和main函数的异同: 相同点: 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。 不同点: init可以应用于任意包中,且可以重复定义多个。 main函数只能用于main包中,且只能定义一个。 go中包的初始化顺序: 首先初始化包内声明的变量 之后调用 init 函数 最后调用 main 函数 两个函数的执行顺序: 对同一个go文件的init()调用顺序是从上到下的。 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。 对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。 如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。 https://www.cnblogs.com/chenjiazhan/p/17473207.html https://www.cnblogs.com/XiaoXiaoShuai-/p/14642055.html init 函数的用途 1 初始化全局变量 2 执行一些必要的验证操作 注意: init 函数不能被显式调用 init 函数只执行一次 避免在 init 函数中执行耗时操作 */ func Init() { fmt.Println("Init Test!!!") } func init() { fmt.Println("hello world") } /* test Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。 go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。 在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。 类型 格式 作用 测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确 基准函数 函数名前缀为Benchmark 测试函数的性能 示例函数 函数名前缀为Example 为文档提供示例文档 go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。 */ func TestChannel() { /* 第一次循环时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以x := <-ch 这个 case 分支不满足,而ch <- i这个分支可以执行,会把1发送到通道中,结束本次 for 循环; 第二次 for 循环时,i = 2,由于通道缓冲区已满,所以ch <- i这个分支不满足,而x := <-ch这个分支可以执行,从通道接收值1并赋值给变量 x ,所以会在终端打印出 1; 后续的 for 循环以此类推会依次打印出3、5、7、9。 */ ch := make(chan int, 1) for i := 1; i <= 10; i++ { select { case x := <-ch: fmt.Println(x) case ch <- i: } } } /* 指针 Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。 */ func Pointer() { a := 100 b := &a fmt.Printf("a:%v, b:%v,bp:%p \n", a, b, b) fmt.Printf("&b:%v,\n", &b) modValue(b) fmt.Printf("a:%v, b:%v,bp:%p \n", a, b, b) } func modValue(i *int) { if i != nil { *i += 101 } } /* context */ func Context() { wg.Add(2) go func() { time.Sleep(time.Second * 2) fmt.Println("job 1 done") wg.Done() }() go func() { time.Sleep(time.Second * 1) fmt.Println("job 2 done") wg.Done() }() wg.Wait() fmt.Println("all job done") } func Context2() { stop := make(chan bool) go func() { for { select { case <-stop: fmt.Println("got the stop channel") return default: fmt.Println("still working") time.Sleep(time.Second * 1) } } }() time.Sleep(time.Second * 5) fmt.Println("stop the goroutine") stop <- true time.Sleep(time.Second * 5) } func Context3() { ctx, cancel := context.WithCancel(context.Background()) go worker(ctx, "worker1") go worker(ctx, "worker2") go worker(ctx, "worker3") time.Sleep(time.Second * 5) fmt.Println("stop the goroutine") cancel() time.Sleep(time.Second * 5) } func worker(ctx context.Context, name string) { go func() { for { select { case <-ctx.Done(): fmt.Println("got the stop channel") return default: fmt.Println(name, " still working") time.Sleep(time.Second * 1) } } }() } func Context4() { // 创建一个带有取消功能的上下文 ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 设置一个截止时间为5秒后 ctx, cancel = context.WithDeadline(ctx, time.Now().Add(5*time.Second)) defer cancel() // 向上下文中添加一个值 ctx = context.WithValue(ctx, "key", "value") // 启动一个goroutine来监听上下文的取消信号 go func() { select { case <-ctx.Done(): fmt.Println("Context done:", ctx.Err()) } }() // 启动一个goroutine来获取上下文中的值 go func() { time.Sleep(2 * time.Second) value := ctx.Value("key") fmt.Println("Context value:", value) }() // 模拟一个耗时操作 select { case <-time.After(10 * time.Second): fmt.Println("Operation completed") case <-ctx.Done(): fmt.Println("Operation canceled due to context") } // 获取上下文的截止时间 if deadline, ok := ctx.Deadline(); ok { fmt.Println("Context deadline:", deadline) } else { fmt.Println("No deadline set for context") } } func ContextWaitGroup() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { select { case <-ctx.Done(): fmt.Println("Goroutine canceled due to context") return default: // 模拟一些工作 fmt.Println("Goroutine working...") time.Sleep(1 * time.Second) } } }() wg.Wait() fmt.Println("Main goroutine finished") } func ContextWaitGroup1() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go func() { for { select { case <-ctx.Done(): fmt.Println("goroutine exiting...") return default: fmt.Println("goroutine working...") time.Sleep(500 * time.Millisecond) } } }() time.Sleep(3 * time.Second) fmt.Println("main function exiting...") } /* 闭包 (Closure)是一种特殊的函数,它可以捕获其创建时所在作用域中的变量。闭包通常与匿名函数一起使用,匿名函数可以访问并操作不在其参数列表中的外部变量。 https://www.jb51.net/jiaoben/31436815s.htm Go语言中的闭包有几个特殊的用途和优势: 状态封装,控制变量生命周期,函数工厂,实现回调和延后执行,模块化和封装,实现接口,高阶函数,迭代器和生成器,避免命名冲突 使用闭包的注意事项:内存泄漏,并发安全,循环引用 */ func Closure() { /* 这里这里start := time.Now() 已经运行了 返回的函数再跟后面做减法 */ defer TimeCost("test closure", 11)() time.Sleep(time.Second * 2) } func TimeCost(handlerName string, req ...interface{}) func() { fmt.Printf(fmt.Sprintf("TimeCost for %s start now.", handlerName)) start := time.Now() return func() { tc := time.Since(start) fmt.Printf(fmt.Sprintf("handle %s for request:%+v time cost is:%+v", handlerName, req, tc)) } } /* http UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。 */ func process(conn net.Conn) { defer conn.Close() for { reader := bufio.NewReader(conn) var buf [128]byte n, err := reader.Read(buf[:]) if err != nil { fmt.Printf("read failed,err:%v", err) break } recv := string(buf[:n]) fmt.Printf("接收到的数据:%v", recv) conn.Write([]byte("ok")) } } func HttpServer() { listen, err := net.Listen("tcp", "127.0.0.1:8801") if err != nil { fmt.Printf("listen failed,err:%v", err) return } for { conn, err := listen.Accept() if err != nil { fmt.Printf("Accept failed ,err:%v", err) continue } go process(conn) } } func HttpClinet() { conn, err := net.Dial("tcp", "127.0.0.1:8801") if err != nil { fmt.Printf("connect failed,err:%v", err) return } input := bufio.NewReader(os.Stdin) for { s, _ := input.ReadString('\n') s = strings.TrimSpace(s) if strings.ToUpper(s) == "Q" { return } //给服务端发消息 _, err := conn.Write([]byte(s)) if err != nil { fmt.Printf("send failed,err:%v \n", err) return } //从服务端接收消息 var buf [1023]byte n, err := conn.Read(buf[:]) if err != nil { fmt.Printf("read failed,err:%v \n", err) return } fmt.Printf("收到服务单回复:%v", string(buf[:n])) } } /* RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。要实现RPC就需要解决以下三个问题。 如何确定要执行的函数?调用方和被调用方都需要维护一个{ function <-> ID }映射表,以确保调用正确的函数 如何表达参数? 参数或返回值需要在传输期间序列化并转换成字节流,反之亦然 如何进行网络传输?只要能够完成传输,调用方和被调用方就不受某个网络协议的限制 svc := pb.NewEMSServiceClient(conn) */ /* 查看pprof文件和其他堆栈信息的文件以及如何查看 获取go 进程堆栈信息 (1)在启动进程加代理 -httpprof localhost:6060 (2) url 获取堆栈信息 https://eddycjy.gitbook.io/golang curl http://localhost:6060/debug/pprof/heap > profile.out 查看当前总览:访问 http://127.0.0.1:6060/debug/pprof/ debug/pprof/ profiles: 0 block 5 goroutine 3 heap 0 mutex 9 threadcreate full goroutine stack dump (3) 在本地安装 graphviz (4) 在本地打开 profile.out go tool pprof -http=:8080 profile.out 系统进程日志查询 进程情况 cat /proc/<PID>/status 系统日志 /var/log/messages 获取进程堆栈信息 jstack pid > /tmp/pid.log */ /* gin */
golang 基础案例
于 2024-09-10 14:16:07 首次发布