小蚯蚓 - go基础
type: channel
link: https://xiaoqiuyinboke.cn/tag/go%E5%9F%BA%E7%A1%80/
items: Go 语言运算符详解与应用
items: Go 语言包管理 | 专业指导和最佳实践
items: GoLang 原子操作详细教程 - 探索Go语言并发编程
items: Go 语言中锁的使用 - 深度解析和实例教程
items: Go 语言中的 Goroutine - 轻松理解并掌握协程
items: Go语言复合类型 - Channel详解
items: Go指针语法详细解析 | Go语言编程指南
items: 深入理解Go语言：复合类型之结构体
items: Go 函数语法规则 | Go语言函数编程全解析
items: Go语言中的自定义类型与别名详解

Go 语言运算符详解与应用
type: item
date: 2022-07-29T09:11:00+08:00
description: 概述 同一类型的变量才可以进行运算！ 算数运算符 算数运算符和其它编程语言一样，+、-、*、/加减乘除，%求余。++（自增）和 --（自减）在 Go 语言中是单独的语句，并不是运算符 func main() { a := 1 a++ fmt.Println(a) // a是2 a++1 // 这个是不行的，因为不是运算符 fmt.Println(a++)// 这个也是不行的，因为是语句 } 关系运算符 断言左右两个值是否满足关系，是的话返回true，否的话返回false，和其它语言一样有==、!=、>、>=、<、<=。 逻辑运算符 和其它语言一样有&&、||、!。 位运算符 和其它语言一样有： &，位与 |，位或 ^，位异或 <<，左移 n 位就是乘以 2 的 n 次方 >>，右移 n 位就是除以 2 的 n 次方 赋值运算符 =，简单的赋值运算符，将一个表达式的值赋给一个左值 +=，相加后赋值 -=，相减后赋值 *=，相乘后赋值 /=，相除后赋值 %=，求余后再赋值 <<=，左移后赋值 >>=，右移后赋值 &=，按位与后赋值 |=，按位或后赋值 ^=，按位异或后赋值

Go 语言包管理 | 专业指导和最佳实践
type: item
date: 2022-07-29T08:55:00+08:00
description: 概述 包是多个源代码的集合，是一种高级的代码复用方案，一个包可以简单理解为一个存放 .go 文件的文件夹，该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码，声明该文件归属的包 // 这个包名其实就是命名空间 package 包名 注意事项： 一个文件夹下的文件只能归属一个 package，同样一个 package 的文件不能在多个文件夹下，不过实际情况也有可能一个文件夹下有多个包，例如 mysql 目录下有：mysql 包和 mysql_test 包，mysql_test 包专门用于测试 包名可以不和文件夹的名字一样（通常要一样的），包名不能包含 - 符号 包名为 main 的包为应用程序的入口包，这种包编译后会得到一个可执行文件，而编译不包含 main 包的源代码则不会得到可执行文件 测试包：名称以_test. go 结尾的文件，包含此处所述的 TestXxx 函数，_test.go 文件放在与正在测试的文件包相同的包中。该文件将从常规软件包构建中排除，但在运行“go test”命令时将包含该文件 不同文件夹下的同个包名称，应该属于不同的包！ 一个包里，不同的职责放在不同的文件里，例如 MySQL 驱动包要解析 dsn，则把 dsn 放在单独的文件里，暴露一个方法出来就可以了，包的常量也可以单独放在一个文件里，然后一般一个文件就只有一个结构体，代表一个类 一个文件夹就是一个包，包的名称可以和文件夹的名称不一样，建议写成和文件夹的名称一样。 同一个文件夹下的可以有多个源文件，不过每个源文件的包名称必须一样 同一个包里的方法可以直接调用 文件夹底下，还可以有文件夹，属于不同的包 包的分类 main包，如果是main包，则必须有一个main函数，此函数就是项目的入口。如果是main包，并且有main函数，则编译的时候会生成可执行文件。如果不是main包，编译的时候会在pkg目录，生成包名.a的包文件供其他项目使用 非main包，用来将我们的代码按功能分类，分别放在不同的包和文件中，这样好编辑管理 需要注意的是：当我们的main包里有多个文件，main函数里调用其它文件里的方法，用goland的直接执行，会报错，因为goland的默认只会编译一个文件 这个时候需要改下配置 包导入 要在代码中引用其他包的内容，需要使用 import 关键字导入使用的包 // 查找方式见依赖管理的文章 import "导入路径" // 这样也可以，不过好像不推荐 import "./项目的相对路径" 注意事项： import 导入语句通常放在文件开头包声明语句的下面 导入路径只是一个目录，通常路径的最后一段的目录名称和包的名称一样，这样方便管理，如果不一样的话，需要声明包名称，例如 import 包名称 导入路径 导入的包名需要使用双引号包裹起来 文件位置查找规则，具体可以看 go依赖管理 Go 语言中禁止循环导入包，比如： a 导入 b，b 又导入 a 导入的包会被整合到编译的可执行文件中 导入的包没有使用，会导致编译错误 每个包的依赖包是独立的，要重复导入，不能 a 包导过依赖后，b 包就不导入了 导入的包是文件级别的，所以导入后，可以在同一个文件中被使用，但是如果同一包的其他文件没有导入的话，将不能使用 小提示 go 的依赖导入自带的和第三方的应该要空行分开，方便阅读，例如： import ( "context" "database/sql" "fmt" "net/url" _ "github.com/go-sql-driver/mysql" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gutil" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gregex" ) 单行导入 import "包1" import "包2" 多行导入 import ( "包1" "包2" ) // 最好是按顺序包1，然后包2，这种先包2，再包1的idea会提示 import ( "包2" "包1" ) 包的可见性 包名标识符首字母大写，表示对外可见，对外可见通常需要写注释，以标识符开头，空格隔开 // Sum 是求两个整型的和 func Sum(x int,y int) int { return x + y } 标识符首字母小写，表示对外不可见，同一个包里多个源文件之间，是可以相互调用对方的函数、变量等 包别名 导入包名字冲突时候或者导入的包名字很长…

GoLang 原子操作详细教程 - 探索Go语言并发编程
type: item
date: 2022-07-17T11:16:00+08:00
description: 概述 atomic 提供的原子操作能够确保任一时刻只能有一个goroutine对变量进行操作，善用 atomic 能够避免程序中出现大量的锁操作，应该是方法内部实现了锁操作。atomic常见操作有： 载入 比较并交换 交换 存储 增减 增减 atomic 包中提供了如下以Add为前缀的增减操作，对变量进行增加或者减少: func AddInt32(addr *int32, delta int32) (new int32) func AddInt64(addr *int64, delta int64) (new int64) func AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) 需要注意的是，第一个参数必须是指针类型的值，通过指针变量可以获取被操作数在内存中的地址，确保同一时间只有一个goroutine能够进行操作。先来看一个例子： 分别用“锁”和原子操作来实现多个 goroutine 对同一个变量进行累加操作。 使用锁实现，可以保证每一个时刻保证只有一个协程可以拿到变量，并且对变量进行写的操作： func main() { var ( mux sync.Mutex total = 0 ) for i := 0; i < 10; i++ { go func() { for { mux.Lock() total += 1 mux.Unlock() time.Sleep(time.Millisecond) } }() } time.Sleep(time.Second) // 保证所有的协程都执行完毕 fmt.Println("The total number is", total) } 由于一直使用锁定的操作会很麻烦，所以我们可以使用 atomic 实现，也是没有问题的： func main() { var total int64 for i := 0; i < 10; i++ { go func() { for { atomic.AddInt64(&total, 1) // 应该类似事务，内部会对变量加锁 time.Sleep(time.Millisecond) } }() } time.Sleep(time.Second) fmt.Println("The total number is", total) } 载入 atomic 包中提供了如下以Load为前缀的载入操作，应该就是读的时候加读锁: func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr) 载入操作能够保证原子的读变量的值，当读取的时候，任何其他 goroutine 都无法对该变量进行读写. 比较交换 该操作简称 CAS(Compare And Swap)。 这类操作的前缀为 CompareAndSwap ，比较新的值和旧的值是否相等，相等的话就交换，返回布尔值: func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr *uintptr, old, new uintpt…

Go 语言中锁的使用 - 深度解析和实例教程
type: item
date: 2022-07-17T10:22:00+08:00
description: 共享内存 我们知道，channel 可以在在多个 goroutine 之间进行通信，其实对于并发还有一种较为常用通信方式，那就是共享内存，这是因为每个协程函数里都可以访问到全局变量。首先我们来看一个例子： var name string func printName() { log.Println("name is", name) } func main() { name = "小明" go printName() // 输出小明 go printName() // 输出小明 time.Sleep(time.Second) name = "小红" go printName() // 输出小红 go printName() // 输出小红 time.Sleep(time.Second) } 可以看到在两个 goroutine 中我们都可以访问 name 这个变量，当修改它后，在不同的 goroutine 中都可以同时获取到最新的值。这就是一个最简单的通过共享变量（内存）的方式在多个 goroutine 进行通信的方式。 如上我们知道，对变量进行并行读问题不太大，但是对变量进行并行的写的时候如果不加锁就会出现问题： func main() { var ( wg sync.WaitGroup numbers []int ) for i := 0; i < 100; i++ { wg.Add(1) go func(i int) { numbers = append(numbers, i) wg.Done() }(i) } wg.Wait() fmt.Println("The numbers is", len(numbers)) } 上诉例子运行的之后，有的时候，输出的结果是是65，有的时候输出是80等等，反正输出的结果不固定，不符合预期结果100，可以看到当我们并发对同一个切片进行写操作的时候，会出现数据不一致的问题，这就是一个典型的共享变量的问题。 sync.Mutex互斥锁 针对这个问题我们可以使用 Mutex 锁（其它语言中也叫排他锁、写锁）来修复，sync.Mutex 一旦被锁住，其它的 Lock() 操作就无法再获取它的锁，只有通过 Unlock() 释放锁之后才能通过 Lock() 继续获取锁，从而保证数据的一致性。 另外需要注意，sync.Mutex 不区分读写锁，只有 Lock() 与 Lock() 之间才会导致阻塞的情况，如果在一个地方 Lock()，在另一个地方不 Lock() 而是直接修改或访问共享数据，这对于 sync.Mutex 类型来说是允许的，因为 mutex 不会和 goroutine 进行关联。例如： func main() { var ( wg sync.WaitGroup numbers []int mux sync.Mutex ) for i := 0; i < 100; i++ { wg.Add(1) go func(i int) { mux.Lock() numbers = append(numbers, i) mux.Unlock() wg.Done() }(i) } wg.Wait() fmt.Println("The numbers is", len(numbers)) } 修改过后，我们再次运行代码，可以看到最后的 numbers 的数量始终是100个了。 可以使用 TryLock 来不阻塞判断是否被锁住 // serverAutoUnregisterLocker 防止本任务还没执行完成，下个任务又开始，拖垮系统 var serverAutoUnregisterLocker sync.Mutex func Run() { ctx := context.Background() tqtlog.Info(ctx, fmt.Sprintf("auto unregister cron job start exec")) if serverAutoUnregisterLocker.TryLock() { defer serverAutoUnregisterLocker.Unlock() err := a.service.AutoUnregisterCronJob(ctx) if err != nil { tqtlog.Error(ctx, fmt.Sprintf(`AutoUnregisterCronJob exec failed, err:%v`, err)) } } else { tqtlog.Warn(ctx, fmt.Sprintf("AutoUnregisterCronJob 获取锁失败,存在任务正在执行")) } tqtlog.Info(ctx, fmt.Sprintf("auto…

Go 语言中的 Goroutine - 轻松理解并掌握协程
type: item
date: 2022-07-13T08:57:00+08:00
description: 概述 当代计算机的多进程、多线程模型 现在计算机的 cpu 核心数是有限的，比如 8 核心 cpu，同一时刻只能并行运行 8 个进程。而同一时间操作系统上要运行的程序要远远多于 cpu 的核心数。面对这个问题，操作系统的解决办法是不停的切换进程来执行，每个进程占用一小段时间片。 由于进程拥有太多的资源（虚拟内存占用 4Gb），进程的创建、切换、销毁会占用很长的时间。线程虽然占用的资源少了一点（也要占用 4MB），但是多线程开发会比较复杂，开发者要考虑很多同步竞争问题，比如锁，资源竞争。 由于进程和线程的切换成本高，当进程和线程的数量比较多的时候，会导致 cpu 很大一部分时间都浪费在切换进程和线程上，没有真正的用在执行逻辑程序上。 引入协程模型 协程类似于线程，属于用户态的线程，系统的是线程是内核态的线程；用户态的比内核态的轻量（goroutine 只占用几 Kb），切换的执行的时候，比较快。 一个用户态线程，必须绑定一个内核态线程，内核态线程可以由协程调度创建。 线程和协程的区别是： 线程由 cpu 调度，是抢占式的 协程是用户动态调度的，是协作式的，一个协程让出 cpu 后才会执行下一个协程 GPM 调度器模型 GPM 的模型中： G 指的是 go 协程 goroutine P 是协程调度器 processer，G 是放在 P 里，然后 M 执行 P 里的 G M 是 machine，也就是线程，用于执行 G 在 go 中，线程是运行 goroutine 的实体，调度器是把 goroutine 分配到线程上执行。在 GPM 模型中，有几个重要的概念，如下图所示： 全局队列：存放等待运行的 G，可以被任意的 P 获取执行，相当于整个 GPM 模型里的共用资源，所以需要加入互斥锁 P 的本地队列：存放的也是等待执行的 G，但是存放的数量有限，最多 256 个。该队列里的 G 新建的 G，会优先放到本队列，如果本队列的 G 满了，再放到全局队列中，具体见下文 P 列表：在程序启动的时候创建，最多可以有 GOMAXPORCS 个 M 线程：线程是由 P 创建，要执行 G 的话，M 首先要绑定 P，从 P 的本地队列获取 G，如果 P 的本地队列为空，则尝试从全局队列获取一批 G，放到本地队列，全局队列再为空的话再从其他 P 的本地队列偷一半放到本地队列。 有关 p 和 m 的数量的问题 P 的个数是启动的时候，环境变量 $GOMAXPROCS 或 runtime 的 GOMAXPROCS() 方法决定的。也就是说，程序在运行的时候，只有 $GOMAXPROCS 个 goroutine 同时在运行。这个数量一般设置成内核的一半 M 的数量，由 Go 语言本身的限制决定的，Go 在启动的时候，会设置 M 的最大数量为 1 万个，但是操作系统很难支撑这么多的线程，所以这个限制忽略不计。runtime/deBug 中的 SetMaxThreads() 方法，可以设置 M 的最大数量。当一个 M 阻塞的时候，会创建新的 M。 总得来说，P 和 M 的数量没有绝对关系，一个 M 阻塞，P 会创建新的 M 或切换到其它空闲的 M 上。 GPM 的设计策略 复用线程 GMP 会对线程复用，避免频繁的创建和销毁线程，也就避免让线程闲着。 偷取机制 线程 M 绑定到 P，当一个 P 上没有可执行的 G 的时候，本 P 就会从其他 P 的本地队列上偷去一半的 G 到本队列执行。这样就不会导致空闲的 P 无事可做了。 移交机制 当 M 正在执行的 G 发生了阻塞，那么 P 就会和绑定的 M 发生分离，和一个空闲的 M 再次绑定，如果没有空闲的 M， P 新创建一个 M，这样就不会因为一个 G 阻塞，影响到队列上的其他 G 没法执行。 利用并行 通过 GOMAXPROCS 来设置 P 的数量，一般设置为核心数的一半，表示最多利用核心数的一半在并行的运行 抢占 go 中的协程，一个 goroutine 最多执行 10ms ，就会获取下一个可执行状态的 G 来执行。 全局 G 队列 当 P 去其他 P 的本地队列上偷不到 G 的时候，它可以从全局队列获取 G。 创建 goroutine 的流程 当我们在代码中执行 go func() 的时候，GPM 会进行哪些操作呢？ 当创建协程后，G 会优先保存到 P 的本地队列中，当 P 的本地队列满了，就会保存到全局队列中。执行流程如下图的序号所示： 调度器的生命周期 在 GPM 里，还有两个比较重要的角色，分别是 M0 和 G0。 M0 线程 启动程序后编号为 0 的主线程 在全局命令 runtime.m() 中，不需要在 heap 堆上分配 负责执行初始化操作，和启动第一个 G 启动 1 个 G 后，M0 就和其他 …

Go语言复合类型 - Channel详解
type: item
date: 2022-07-12T18:00:00+08:00
description: 概述 Channel 是一种特殊的引用类型，中文直译一般叫做通道，是 goroutine 执行体之间进行通信的桥梁，可以实现一个 goroutine 发生特定的值到另外一个 goroutine。 每个通道都是一个具体类型的导管，叫做通道的元素类型。使用 make 函数创建一个通道 ch := make(chan int) 和 map 一样，通道是一个使用 make 创建的数据结构的引用。所以当复制，或者作为参数传递到一个函数的时候，复制的是引用，这样调用者和被调用者都引用同一份数据结构。 通道之间可以比较吗？ 同一个类型的通道之间是可以用 == 进行比较的，当二者都是同一个底层的引用的时候，结果是 true。 通道还可以和 nil 进行比较 声明 可以通过 var 关键字进行定义 channel，由于 channel 是一种引用类型，所以 var 定义的并没有在内存种实例化，var 定义后的 channel 零值为 nil，还需要初始化化，比较麻烦，因此这种定义方式不常用。 var ch1 chan int // 声明一个传递整型的通道 var ch2 chan bool // 声明一个传递布尔型的通道 var ch3 chan []int // 声明一个传递int切片的通道 fmt.Println(ch1, ch2, ch3) // 输出<nil> <nil> <nil> 另外一种方式是通过 make 函数定义，定义之后就已经在内存中初始化了，这个方式比较常用 chInt := make(chan int) // 无缓冲 chBool := make(chan bool, 0) // 无缓冲 chStr := make(chan string, 2) // 容量为2的缓冲通道 以上两种声明 channel 的都是双向的，意味着可以向该 channel 可以发送数据并接收数据。"发送"和"接收"是 channel 的两个基本操作，统称为通信，可以通过箭头操作符书写 ch <- x // 发送 x <- ch // 接收 <-ch // 接收并且丢弃结果 通道的第三个个操作是关闭。它设置一个标志位来表明现在已经发送完毕，这个通道不会再发送新的值了。 close(ch) 关闭后的发送操作会导致宕机。 在已经关闭的通道进行接收操作，将获取已经发送的值，直到通道为空。这个时候对已关闭的空通道上进行接收，会立即返回零值。 关闭一个 nil 的通道也会导致宕机 关闭以一个已经关闭的通道也会导致宕机 通道缓冲 无缓冲通道 如下用 make 创建无缓冲通道 chInt := make(chan int) // 无缓冲 chBool := make(chan bool, 0) // 无缓冲 对无缓冲通道的发送操作，将会阻塞（相当于挂起休眠了），直到其他 goroutine 在对应的通道上执行接收操作（发送的 goroutine 再次被唤醒），这时候传值完成。相反，如果接收操作先执行，接收方的 goroutine 将阻塞，直到另一个 goroutine 在同一个通道上发送值。 因为无缓冲通道会导致发送和接收 goroutine 同步化，所以无缓冲通道也称作同步通道。这个也是它的一个重要特征。 无缓冲通道如何导致死锁？ 当在一个 goroutine 中对同一个无缓冲的通道进行收发操作，就会导致死锁，因为收发都在同一协程，就会导致互相等待，导致死锁 func main() { ch := make(chan string) ch <- "ping" fmt.Println(<-ch) } 缓冲通道 缓冲通道有一个元素队列，队列长度可以在 make 的时候指定 // 队列的长度是2 ch := make(chan int, 2) 缓冲通道的发送操作，会在队列的尾部插入元素（lpush），接收操作会在对了的头部移出（rpop）。如果通道满了，发送操作就会阻塞所在的 goroutine，直到另一个 goroutine 对它进行了接收操作来腾出空间。反过来，如果通道是空的，执行接收操作的 goroutine 阻塞，直到另一个 goroutine 在通道上发送数据。 因为缓冲空间足够的时候，不会阻塞，所以缓冲通道的一个重要特点是解耦。 在同一个 goroutine 上收发缓冲通道，会导致死锁吗？ 如果没有超过缓冲的容量，就不会，但是如果超过容量就会触发阻塞，从而导致死锁。通道一般是用于 goroutine 之间通信的，所以一般不要在同一个 goroutine 上进行收发操作。 // 利用channel 实现了一个队列的操作，不过有死锁的风险 func main() { ch := make(chan int, 2) ch <- 1 ch <- 2 // ch …

Go指针语法详细解析 | Go语言编程指南
type: item
date: 2022-07-12T15:55:00+08:00
description: 使用场景 使用指针，出于以下两个目的： 指针变量复制和拷贝的是地址，所以比较轻量，对于大的结构体变量，这样拷贝会比较快。 在生命周期的任何位置可以修改原变量的值 示例 函数内部改变外部变量值 package main import "fmt" func main() { v1 := "武沛齐" // 参数传递，是将值复制一份给形参 // 所以形参里面是个副本，修改里面的值不会改变外面的值的值 changeData(&v1) // 哈哈哈 fmt.Println(v1) } func changeData(data *string) { *data = "哈哈哈" } 获取控制台输入： package main import "fmt" func main() { var userName string fmt.Println("请输入你的名字：") fmt.Scanf("%s", &userName) // 在Scanf里改变userName的值 // 打印输入的值 fmt.Println(userName) } 指针类型声明 指针类型和其它的类型一样，也是个数据类型，任何类型（包括指针类型）都有一个对应的指针类型。 声明指针类型，只需要在该类型前面加个 *，默认零值是 nil。 普通类型的变量存储的是该类型的值，指针类型的变量存储的是该类型的内存地址！ ? var v1 string // string类型 var v2 *string // string类型的指针类型 var v1 int // int类型 var v2 *int // int类型的指针类型 举例说明，指针的指针类型，这个不常用，了解即可。 因为指针变量本身也是个变量，自己也有地址，所以可以对指针类型取地址。 name := "武沛齐" var p1 *string = &name // 两个**表示，是指针的指针类型 var p2 **string = &p1 // 3个星表示是指针的指针的指针类型 var p3 ***string = &p2 // 指针的指针的指针类型，取值要相应的加3个* ***p3 = "哈哈哈" fmt.Println(name) 取地址 所有变量都有内存地址，只需要在对应的变量前面加个 & 就可以取变量的地址。这个地址可以存在对应指针类型变量上。 var name string = "武沛齐" var pointer *string = &name 还可以用 new 函数进行取值，可以在内存中初始化一个该类型的变量，并且返回该变量的内存地址。 func main() { p := new(int) fmt.Println(p) // 0xc00000e0d8 *p = 100 fmt.Println(*p) } 空指针 因为指针类型的零值是 nil，即：空指针。如果忘记初始化，就容易 panic： 空指针并不会有语法错误，运行到该位置的时候才会 panic（panic: runtime error: invalid memory address or nil pointer dereference），所以需要特别注意。 空值代表什么都没有，使用空变量会 panic： 以下空指针会 panic 调用空指针的成员方法 package main import "fmt" type A struct { } func (a A) Hello() { fmt.Println("hello world") } type B struct { a *A } func main() { b := B{} // 初始化的时候，b的零值是nil b.a.Hello() } 直接使用空指针 func main() { var p *int fmt.Println(*p) // 直接获取空指针值 } 数组第一个元素的指针 数组中的每个元素的地址是连续的，数组的地址，等于数组的第一个元素的地址。 dataList := [3]int{11, 22, 33} // 0xc000012180 0xc000012180 fmt.Printf("%p %p", &dataList, &dataList[0]) &dataList 和 &dataList[0] 虽然地址是一样的，但是却是不同的类型 &dataList 是 *[3]int 类型 &dataList[0] 是 *int 类型 内存地址偏移 指针类型不能直接算数计算，需要依赖 unsafe.Pointer 和 uintptr 进行转换后计算 dataList := [3]int8{11, 22, 33} // 1 获取第一个元素的指针 var firstDataPtr *int8 = &dataLis…

深入理解Go语言：复合类型之结构体
type: item
date: 2022-07-12T15:22:00+08:00
description: 前言 Go 语言中，没有类的概念，使用结构实现类似类的功能。 语言内置的基础数据类型是用来描述一个值，而结构体是用来描述一组值。 数组、切片和 Map 可以用来表示 同一种数据类型 的集合，但是当我们要表示 不同数据类型 的集合时就需要用到结构体。 定义结构体 使用 type 和 struct 关键字来定义结构体 type 结构体名 struct { 字段名 字段类型 字段名 字段类型 … } // 例如 type person struct { name string city string age int8 } 结构体名：表示结构体的名称，在同一个包内不能重复，并且最好不要以包的名称开头，不然 idea 会警告 字段名：表示结构体字段名，结构体中的字段名必须唯一 字段类型：表示结构体字段的具体类型 通常结构体中一个字段占一行，但是类型相同的字段，也可以放在同一行，同样类型的字段也可以写在一行 type person struct { name, city string // name, city是同一个类型 age int8 } 一个结构体中的字段名是唯一的，例如一下代码，出现了两个 Name 字段，是错误的： type Student struct { Name string Name string } 结构体还可以省略字段名称，这种字段叫：结构体匿名字段。匿名结构体字段，默认会采用类型名作为字段名；由于结构体要求字段名称必须唯一，所以结构体中同种类型的匿名字段只能有一个，不然就会重复。 type person struct { string // 匿名字段 int8 // 匿名字段 } func main() { p1 := person{ string: "张三", int8: 18, } fmt.Println(p1.string) fmt.Println(p1.int8) } 结构体还可以定义字段的可见性，字段大写开头表示可公开访问，小写表示私有（仅在定义当前结构体的包中可访问），那么其他 package 就无法直接使用该字段 // 在包 pk1 中定义 Student 结构体 package pk1 type Student struct{ Age int name string } // 在另外一个包 pk2 中调用 Student 结构体 package pk2 func main(){ stu := Student{} stu.Age = 18 //正确 stu.name = "name" // 错误，因为`name` 字段为小写字母开头，不对外暴露 } 对结构体 json 化的时候，私有的字段将会丢失 type Student struct { Id int Name string age int // 私有 } func main() { studentInfo := Student{ Id: 1, Name: "小明", age: 35, } // json 格式化 data, err := json.Marshal(studentInfo) if err != nil { fmt.Println("json encode 错误") return } fmt.Printf("%s", data) // {"Id":1,"Name":"小明"}，年龄字段已经丢失了 } 这样对结构体进行 json 的时候，默认输出字段的名称就是结构体字段的名称，我们经常需要修改，不希望输出的字段名称是大写字母开头的，这个时候就需要 结构体标签 的功能了，结构体标签可以是多个，用空格隔开 type Student struct { Id int `json:"id"` Name string `json:"name"` age int // 私有 } func main() { studentInfo := Student{ Id: 1, Name: "小明", age: 35, } // json 格式化 data, err := json.Marshal(studentInfo) if err != nil { fmt.Println("json encode 错误") return } fmt.Printf("%s", data) // {"id":1,"name":"小明"}，这个时候字段的名称就是小写的了 } 结构体初始化 声明结构体的类型的变量的时候进行初始化，这个时候结构体的变量每个字段都有它默认类型的零值，这个和切片、map 不一样，切片和 map 定义之后，需要初始化才能使用 type person struct { name string city string age int8 } func mai…

Go 函数语法规则 | Go语言函数编程全解析
type: item
date: 2022-07-12T09:36:00+08:00
description: 函数的定义 函数的定义使用 func 关键字定义 func 函数名(参数)(返回值){ 函数体 } 函数名：由 字母、数字、下划线 组成，但函数名的第一个字母不能是数字，在同一个包内，函数名也称 不能重复； 参数由：参数变量 和 参数变量的类型 组成，例如：（amount int,name string），多个参数之间使用 , 分隔，可以没有参数；当参数比较长的时候，可以定义为结构体。 返回值：由变量和类型的形式，返回值变量可以在函数体中被使用，此时 return 语句后可为空，将定义的返回变量给返回，也就是将 return 变量值返回： func intSum(x int,y int) (result int) { result = x + y return // return语句后可为空 } 返回值参数还可以只有类型的形式，这个时候 return 语句后不能为空 // 只有int类型，没有变量 func intSum(x int,y int) int { return x + y // 这个时候return语句后不能为空 } 函数还可以返回多个值，或者没有返回值 // 返回两个int类型的值 func test () (int,int) { return 1,2 } 函数的参数 如果相邻变量的类型相同，则可以省略类型 // x类型也是int，和y一样 func intSum(x, y int) int { return x + y } 还可以声明为可变参数，在参数名后加 … 来标识，可变参数通常要作为函数的最后一个参数 func main() { ret := intSum(10, 20) fmt.Println(ret) } // 求和 func intSum(x ...int) int { fmt.Println(x) //x是一个切片 sum := 0 for _, v := range x { sum = sum + v } return sum } 声明指针类型的参数，传入的值必须是指针 // Person 是一个结构体 type Person struct { name string age int8 } func test(p *Person) { } func main() { p1 := &Person{ name: "张三", age: 30, } test(p1) } 函数参数必须传，但是参数可以没有被使用 func test(name string) { // name 没有被使用是允许的 fmt.Println("123") } func main() { test("123") // 但是参数必须传递 } 函数参数的默认值是如何设置的呢？ 方式一：使用中间对象的方式 // 默认参数的选项,中间对象 type TeaOptions struct { heat bool } // 创建默认参数对象 func NewDaulft() *TeaOptions { return &TeaOptions{ heat: false, // 默认不加热 } } type Tea struct { name string heat bool } // 创建具体对象 func NewTea(name string, ops *TeaOptions) (*Tea, error) { return &Tea{ name: name, heat: ops.heat, }, nil } func (t Tea) getTea() { fmt.Printf("name:%s , heat:%t ", t.name, t.heat) } func main() { // 使用默认参数 tea1, _ := NewTea("tea1", NewDaulft()) tea1.getTea() // 不使用默认参数,创建TeaOptions对象传参 tea2, _ := NewTea("tea2", &TeaOptions{heat: true}) tea2.getTea() } 方式二：使用 选项模式，该模式便于扩展，可以使用设计模式中的选项模式来创建对象 type config struct { logicalRegionIdKey string logicalRegionNameKey string } type Option func(cfg *config) // WithSetLogicalRegionIdKey 修改默认值 func WithSetLogicalRegionIdKey(key string) Option { return func(cfg *config) { cfg.logicalRegionIdK…

Go语言中的自定义类型与别名详解
type: item
date: 2022-07-12T09:05:00+08:00
description: 介绍 使用关键字定义类型有两种操作： 不带有 = 的是新类型定义 带有 = 的是别名定义 自定义类型 通过 type 关键字自定义类型, 是一个全新的类型，底层数据结构一模一样。主要用途是进行概念上进行区分于另外一个类型，是得业务更加清晰明了。 虽然说，它们的底层内存数据结构一样，但是它们概念上是不同。 package main // Duration 定义一个时长类型 type Duration int64 const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute ) func main() { var a int64 const b int64 = 5 var c Duration a = int64(c) c = Duration(a) * Hour // 不同类型之间，需要转换，小括号类型转换 c = 5 * Hour // 字面量会自动执行类型转 } Duration 就是一种新的类型，它底层和 int64 一样，但是代表的意思不一样。不同类型之间，需要转换，小括号类型转换，需要注意的是，字面量会自动执行类型转换。 再举个例子，下例中，Plane 类型和 Car 类型的数据结构一样，但是代表的东西不一样，拥有的方法也不一样。 package main type Car struct { Name string } func (c Car) Drive() { } // Plane 类型和Car类型的数据结构一样，成员变量一样 // 但是方法不一样，概念不一样 type Plane Car func (p Plane) Fly() { } func main() { car := Car{ Name: "车子", } car.Drive() plane := Plane{ Name: "飞机", } plane.Fly() } 类型转换 自定义的类型，是个全新的类型，因为他们底层都一样，可以进行类型转换。对于每个类型，都有一个类型转换操作 T(x)，以下两种情况可以类型之间可以相互转换 都拥有同样的底层类型的话 都是指向同一种类型的指针类型 需要注意的是：数字类型之间、字符串和一些 slice 类型也是允许转换的，例如： 浮点数转整数，小数部分丢失 字符串转为 []byte 切片，会分配一份字符串数据副本 命令类型的底层类型，决定了它支持什么操作，例如底层如果是数值类型的话，那么就支持算数运算。 option 模式 这个模式在 go 语言里到处都是，很常见。主要有两种方式： 函数模式 接口模式 方式一：将 option 定义为匿名函数模式 这个模式比较简单，主要是在传入回调函数，然后在构造函数里调用回调函数，修改默认的值。 package main import "fmt" type User struct { Name string Age int } // UserOption 函数的参数一样要是指针类型，因为要修改User结构体 // 用于修改user结构体实例 type UserOption func(*User) // WithName 用于生产UserOption func WithName(name string) UserOption { return func(u *User) { // 调用的时候，这个name会被实际值替换掉，这样的话可以灵活的赋值，例如WithName("张三") // 此时相当于u.Name="张三" u.Name = name } } // WithAge 用于生产UserOption func WithAge(age int) UserOption { return func(u *User) { // 调用的时候，这个age会被实际值替换掉，例如WithName(18) // 此时相当于u.Age=18 u.Age = age } } // opts 定义成不定长的参数 func NewUser(opts ...UserOption) *User { // 默认值 user := &User{ Age: 30, Name: "小明", } // 修改默认值 for _, opt := range opts { opt(user) } return user } func main() { // 传人参数，修改默认值 user := NewUser( WithName("张三"), WithAge(18),…