✅作者简介:CSDN内容合伙人、信息安全专业在校大学生🏆
🔥系列专栏 :
📃新人博主 :欢迎点赞收藏关注,会回访!
💬舞台再大,你不上台,永远是个观众。平台再好,你不参与,永远是局外人。能力再大,你不行动,只能看别人成功!没有人会关心你付出过多少努力,撑得累不累,摔得痛不痛,他们只会看你最后站在什么位置,然后羡慕或鄙夷。
文章目录
- 进阶数据类型
- 指针
- 结构体
- 数组
- 切片
- `map`
- 方法
- 声明和调用
- 方法与指针重定向
- 选择值或者指针来作为接受者
- 接口
- 声明
- 实现接口
- 使用
- 类型断言和类型切换
- 类型断言
- 类型切换
- `Stringer`
- 错误
- io流和os库
- Reader
- `os.ReadFile()` 一次读取整个文件内容并返回,不适宜太大的文件
- `os.ReadDir()`读取目录下的所有文件名并返回
- Writer
- os库
- `os.Create()`
- `os.Open()`
- 带有缓冲区的读写:bufio
- 图像
- 泛型
- 泛型函数
- 泛型类型
- 并发
- 协程
- 信道
- range和close
- select
进阶数据类型
指针
var num int = 10
num是值 &num是地址
var p *int = &num
p是指针,指向num的底层地址,p是num的值,修改p会修改底层数据
package mainimport "fmt"func main() {var num int = 10var p *int = &numfmt.Printf("num = %d, p = %d, *p = %d\n", num, p, *p)//num = 10, p = 824633761976, *p = 10*p++fmt.Printf("num = %d, p = %d, *p = %d\n", num, p, *p)//num = 11, p = 824633761976, *p = 11}
package mainimport "fmt"func main() {i, j := 42, 2701p := &i // 指向 ifmt.Println(*p) // 通过指针读取 i 的值 42*p = 21 // 通过指针设置 i 的值 21fmt.Println(i) // 查看 i 的值p = &j // 指向 j*p = *p / 37 // 通过指针对 j 进行除法运算fmt.Println(j) // 查看 j 的值 73
}
结构体
// 定义一个结构体
type Vertex struct {X intY int
}func main() {// 声明一个结构体变量v := Vertex{1, 2} // 亦可显示赋值 Vertex{X:1, Y:2} // 字段可以通过 . 来访问v.X = 4 // 指向结构体的指针p := &vp.X = 1e9 // 按理来说应该是 *p.X 但是可以简写}
数组
数组的长度是其类型的一部分,因此数组不能改变大小。 这看起来是个限制,不过没关系,Go 拥有更加方便的使用数组的方式。
func main() {var a [2]stringa[0] = "Hello"a[1] = "World"fmt.Println(a[0], a[1]) // Hello Worldfmt.Println(a) // [Hello World]primes := [6]int{2, 3, 5, 7, 11, 13}fmt.Println(primes) //[2 3 5 7 11 13]
}
切片
- 每个数组的大小都是固定的。而切片则为数组元素提供了动态大小的、灵活的视角。 在实践中,切片比数组更常用。
func main() {primes := [6]int{2, 3, 5, 7, 11, 13}var s []int = primes[1:4] //[3 5 7]fmt.Println(s)
}
- 切片类似数组的引用,改变切片的值会影响原来数组的值
func main() {names := [4]string{"John","Paul","George","Ringo",}fmt.Println(names) //[John Paul George Ringo]a := names[0:2]b := names[1:3]fmt.Println(a, b) //[John Paul] [Paul George]b[0] = "XXX"fmt.Println(a, b) //[John XXX] [XXX George]fmt.Println(names) //[John XXX George Ringo]
}
- 切片字面量 简单来说就是允许你
直接声明并初始化一个切片,相当于底层创建了数组,然后引用数组
- 长度和容量
len(slice) cap(slice)
- 用
make
创建切片,不返回指针 , new返回指针
1. 切片可以用内置函数 make 来创建,这也是你创建动态数组的方式。make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:a := make([]int, 5) // len(a)=5要指定它的容量,需向 make 传入第三个参数:b := make([]int, 0, 5) // len(b)=0, cap(b)=5b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
- 追加元素
func main() {var s []intprintSlice(s) //len=0 cap=0 []// 可在空切片上追加s = append(s, 0)printSlice(s) //len=1 cap=1 [0]// 这个切片会按需增长s = append(s, 1)printSlice(s) //len=2 cap=2 [0 1]// 可以一次性添加多个元素s = append(s, 2, 3, 4)printSlice(s) //len=5 cap=6 [0 1 2 3 4]
}func printSlice(s []int) {fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
map
- 创建语法
1.
map1 = make(map[keyType]valueType)
map1[key1] = value1
2.
map[keyType]valueType{key1:value1,key2:value2
}
- 修改 获取 删除
func main() {m := make(map[string]int)m["答案"] = 42fmt.Println("值:", m["答案"]) // 值: 42m["答案"] = 48fmt.Println("值:", m["答案"]) // 值: 48delete(m, "答案")fmt.Println("值:", m["答案"]) // 值: 0v, ok := m["答案"]fmt.Println("值:", v, "是否存在?", ok) //值: 0 是否存在? false
}
方法
声明和调用
go没有类,方法就是一类带特殊的 接收者 参数的函数。 就是函数的参数是特定的结构体
就是接收者的类型定义和方法声明必须在同一包内
type Vertex struct {X, Y float64
}
// 1. 写法1 函数
func Abs(v Vertex) float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 2. 写法2 方法
func (v Vertex) Abs() float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)
}func main() {v := Vertex{3, 4}fmt.Println(Abs(v)) //5
}
- 指针类型的接受者
type Vertex struct {X, Y float64
}func (v Vertex) Abs() float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 若去掉下面的 * 则变成了值传递 而不是引用传递
func (v *Vertex) Scale(f float64) {v.X = v.X * fv.Y = v.Y * f
}func main() {v := Vertex{3, 4} // 等价于 p = &Vertex{3, 4} 这样p就是真正的指针了v.Scale(10)fmt.Println(v.Abs()) // 50
}
- 下面的和上面实现功能一致,只是用函数来表示: (方法重写为函数)
type Vertex struct {X, Y float64
}func Abs(v Vertex) float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)
}func Scale(v *Vertex, f float64) {v.X = v.X * fv.Y = v.Y * f
}func main() {v := Vertex{3, 4}Scale(&v, 10)fmt.Println(Abs(v))
}
方法与指针重定向
接受一个值作为参数的函数必须接受一个指定类型的值:
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // 编译错误!
而以值为接收者的方法被调用时,接收者既能为值又能为指针:
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
这种情况下,方法调用 p.Abs()
会被解释为 (*p).Abs()
。
选择值或者指针来作为接受者
使用指针接收者的原因有二:
首先,方法能够修改其接收者指向的值。
其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样会更加高效。
在本例中,Scale
和 Abs
接收者的类型为 *Vertex
,即便 Abs
并不需要修改其接收者。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。 (我们会在接下来几页中明白为什么。)
type Vertex struct {X, Y float64
}func (v *Vertex) Scale(f float64) {v.X = v.X * fv.Y = v.Y * f
}func (v *Vertex) Abs() float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)
}func main() {v := &Vertex{3, 4}fmt.Printf("缩放前:%+v,绝对值:%v\n", v, v.Abs())//缩放前:&{X:3 Y:4},绝对值:5v.Scale(5)fmt.Printf("缩放后:%+v,绝对值:%v\n", v, v.Abs())//缩放后:&{X:15 Y:20},绝对值:25
}
接口
接口类型 的定义为一组方法签名。
接口类型的变量可以持有任何实现了这些方法的值。
声明
type Shape interface { Area() float64 Perimeter() float64
}
实现接口
在Go语言中,如果一个类型拥有接口中所有的方法,那么这个类型就实现了该接口,而不需要显式声明“我实现了这个接口”。这种隐式接口的方式是Go语言的一个独特之处。
type Circle struct { radius float64
} // Circle类型实现了Shape接口
func (c Circle) Area() float64 { return math.Pi * c.radius * c.radius
} func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.radius
}
使用
- 作为函数参数
func PrintShapeInfo(s Shape) { fmt.Printf("Area: %v, Perimeter: %v\n", s.Area(), s.Perimeter())
} func main() { c := Circle{radius: 5} PrintShapeInfo(c)
}
- 作为类型断言和类型切换
var s Shape = Circle{radius: 5} if c, ok := s.(Circle); ok { fmt.Println("Circle radius:", c.radius)
} // 或者使用类型切换
switch v := s.(type) {
case Circle: fmt.Println("Circle radius:", v.radius)
case Rectangle: fmt.Println("Rectangle dimensions:", v.width, v.height)
default: fmt.Println("Unknown shape")
}
类型断言和类型切换
类型断言
类型断言 提供了访问接口值底层具体值的方式。
t := i.(T)
该语句断言接口值 i
保存了具体类型 T
,并将其底层类型为 T
的值赋予变量 t
。
若 i
并未保存 T
类型的值,该语句就会触发一个 panic。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok := i.(T)
若 i
保存了一个 T
,那么 t
将会是其底层值,而 ok
为 true
。
否则,ok
将为 false
而 t
将为 T
类型的零值,程序并不会产生 panic。
请注意这种语法和读取一个映射时的相同之处。
func main() {var i interface{} = "hello"s := i.(string)fmt.Println(s) //hellos, ok := i.(string)fmt.Println(s, ok)//hello truef, ok := i.(float64)fmt.Println(f, ok)//0 falsef = i.(float64) //panic: interface conversion: interface {} is string, not float64fmt.Println(f)
}
类型切换
func do(i interface{}) {switch v := i.(type) {case int:fmt.Printf("二倍的 %v 是 %v\n", v, v*2)case string:fmt.Printf("%q 长度为 %v 字节\n", v, len(v))default:fmt.Printf("我不知道类型 %T!\n", v)}
}func main() {do(21)do("hello")do(true)
}
Stringer
fmt 包中定义的 Stringer 是最普遍的接口之一。
type Stringer interface {String() string
}
Stringer
是一个可以用字符串描述自己的类型。fmt
包(还有很多包)都通过此接口来打印值。 类似于Java的toString
type Person struct {Name stringAge int
}
// 重写 String()方法
func (p Person) String() string {return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}func main() {a := Person{"Arthur Dent", 42}z := Person{"Zaphod Beeblebrox", 9001}fmt.Println(a, z) //Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
}
错误
Go 程序使用 error
值来表示错误状态。
与 fmt.Stringer
类似,error
类型是一个内建接口:
type error interface {Error() string
}
(与 fmt.Stringer
类似,fmt
包也会根据对 error
的实现来打印值。)
通常函数会返回一个 error
值,调用它的代码应当判断这个错误是否等于 nil
来进行错误处理。
i, err := strconv.Atoi("42")
if err != nil {fmt.Printf("couldn't convert number: %v\n", err)return
}
fmt.Println("Converted integer:", i)
error
为 nil 时表示成功;非 nil 的 error
表示失败。
io流和os库
io - Go 编程语言
io
Reader
io
包指定了 io.Reader
接口,它表示数据流的读取端。
Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。
io.Reader
接口有一个 Read
方法:
func (T) Read(b []byte) (n int, err error)
Read
用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF
错误。
示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。
package mainimport ("fmt""io""strings"
)func main() {r := strings.NewReader("Hello, Reader!")b := make([]byte, 8)for {n, err := r.Read(b)fmt.Printf("n = %v err = %v b = %v\n", n, err, b)fmt.Printf("b[:n] = %q\n", b[:n])if err == io.EOF {break}}
}
/*
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""
*/
os.ReadFile()
一次读取整个文件内容并返回,不适宜太大的文件
import ("fmt""os"
)const FILENAME = "test.txt"func TestReadFile() {file, err := os.ReadFile(FILENAME)if err != nil {return}fmt.Printf("读取文件内容: %s\n", file)
}
os.ReadDir()
读取目录下的所有文件名并返回
func TestReadeDir() {dir, err := os.ReadDir(DIRPATH)if err != nil {return}fmt.Printf("读取目录内容: %s\n", dir)
}
Writer
type Writer interface { Write(p []byte) (n int, err error)
}
一般都是使用它已经被继承的接口1. 直接写入
os.WriteFile("example.txt", []byte("Hello, Go!\n"), 0644) 2. 打开文件获取文件指针之后写入
file, err := os.Open(FILENAME)
file.Write(b []byte)
file.WriteAt(b []byte, off int64)
file.WriteString(s string)3. bufio.Writer 进行缓冲写入
writer := bufio.NewWriter(file)
_, err = writer.WriteString("Hello, Go!\n")
if err != nil { fmt.Println("Error writing to buffer:", err) return
} err = writer.Flush() // 确保所有数据都被写入到底层io.Writer中
if err != nil { fmt.Println("Error flushing buffer:", err) return
}
os库
os.Create()
func TestCreateFile() {// 创建或覆盖文件file, err := os.Create("test_create.txt")if err != nil {fmt.Printf("创建文件失败: %s\n", err)return}defer file.Close()// 写入_, err = file.WriteString("Hello, Go!\n")if err != nil {fmt.Printf("写入文件失败: %s\n", err)return}fmt.Printf("写入文件成功\n")}
os.Open()
对于更复杂的读取操作,如逐行读取,可以使用bufio.Scanner
,它封装了io.Reader
接口,提供了方便的逐行读取功能。
func TestOpen() {file, err := os.Open(FILENAME)if err != nil {fmt.Printf("打开文件失败: %s\n", err)return}defer file.Close()// 使用bufio.NewScanner读取文件scanner := bufio.NewScanner(file)for scanner.Scan() {fmt.Println(scanner.Text()) // 输出每一行}if err := scanner.Err(); err != nil {fmt.Println("Error reading file:", err)return}// 或者使用 dstByte = make([]byte, num) // 使用num来控制一次读几个字节n, err := file.Read(dstByte) // 读内容到缓冲区fmt.Println("读取文件成功")
}
带有缓冲区的读写:bufio
- bufio.Reader
bufio.Reader是bufio包中用于读取数据的结构体,它实现了io.Reader接口,并提供了缓冲功能。常用的方法有:
ReadString(delim byte) (string, error):读取直到遇到指定的分隔符delim为止的字符串。
ReadBytes(delim byte) ([]byte, error):功能与ReadString类似,但返回的是[]byte类型的数据。
ReadByte() (byte, error):读取单个字节。
ReadRune() (rune, int, error):读取一个Unicode码点(rune),返回码点、其编码长度和可能的错误。
Peek(n int) ([]byte, error):预览接下来的n个字节,但不移动读取位置。
UnreadRune() error:吐出最近一次读取操作读取的最后一个rune(只能吐出最后一个,多次调用会出错)。
Read(p []byte) (n int, err error):从输入中读取数据到p中,返回读取的字节数和可能遇到的错误。
- bufio.Writer
bufio.Writer是bufio包中用于写入数据的结构体,它实现了io.Writer接口,并提供了缓冲功能。常用的方法有:
Write(p []byte) (n int, err error):将p中的数据写入缓冲区。
WriteString(s string) (n int, err error):将字符串s写入缓冲区。
WriteByte(c byte) error:将单个字节c写入缓冲区。
Flush() error:将缓冲区中的数据写入到底层的io.Writer中,并清空缓冲区。
- bufio.Scanner
bufio.Scanner是一个用于扫描输入数据的结构体,它提供了按行、按分隔符或按其他条件扫描输入的功能。常用的方法有:
Scan() bool:扫描输入直到遇到分隔符或输入结束,返回是否成功扫描到数据。
Text() string:返回最后一次扫描到的文本数据。
Bytes() []byte:功能与Text()类似,但返回的是[]byte类型的数据。
Split(split func(data []byte, atEOF bool) (advance int, token []byte, err error)):设置扫描器的分割函数,用于定义如何分割输入数据。
图像
image 包定义了 Image
接口:
package imagetype Image interface {ColorModel() color.ModelBounds() RectangleAt(x, y int) color.Color
}
注意:Bounds
方法的返回值 Rectangle
实际上是一个 image.Rectangle,它在 image
包中声明。
(请参阅文档了解全部信息。)
color.Color
和 color.Model
类型也是接口,但是通常因为直接使用预定义的实现 image.RGBA
和 image.RGBAModel
而被忽视了。这些接口和类型由 image/color 包定义。
泛型
**泛型(Generics)**是一种编程范式,允许在编写代码时暂时不指定具体的类型,而是在使用时才确定。这样可以使代码更加通用,减少重复,并增强类型安全。Go语言从1.18版本开始正式支持泛型。
泛型语法:
- 使用方括号
[]
来定义类型参数列表,如[T any]
。 - 类型参数后面可以跟一个类型约束,用于限制类型参数可以取哪些类型。
any
表示任意类型,也可以使用接口来定义更具体的约束。
泛型函数
可以使用类型参数编写 Go 函数来处理多种类型。 函数的类型参数出现在函数参数之前的方括号之间。
func Index[T comparable](s []T, x T) int
此声明意味着 s
是满足内置约束 comparable
的任何类型 T
的切片。 x
也是相同类型的值。
comparable
是一个有用的约束,它能让我们对任意满足该类型的值使用 ==
和 !=
运算符。在此示例中,我们使用它将值与所有切片元素进行比较,直到找到匹配项。 该 Index
函数适用于任何支持比较的类型。
// 假如是整形 是不是这样表示?
func IndexInt (s []int, x int) int {pass
}
// Index 返回 x 在 s 中的下标,未找到则返回 -1。
func Index[T comparable](s []T, x T) int {for i, v := range s {// v 和 x 的类型为 T,它拥有 comparable 可比较的约束,// 因此我们可以使用 ==。if v == x {return i}}return -1
}func main() {// Index 可以在整数切片上使用si := []int{10, 20, 15, -10}fmt.Println(Index(si, 15)) //2// Index 也可以在字符串切片上使用ss := []string{"foo", "bar", "baz"}fmt.Println(Index(ss, "hello")) //-1
}
泛型类型
除了泛型函数之外,Go 还支持泛型类型。 类型可以使用类型参数进行参数化,这对于实现通用数据结构非常有用。
type Stack[T any] []T
这里,Stack
是一个泛型类型,它接受一个类型参数T
,T
可以是任意类型(因为使用了any
作为约束,any
相当于空接口interface{}
)。Stack
类型的底层是一个切片,其元素类型为T
。
并发
协程
Go 程(goroutine)是由 Go 运行时管理的轻量级线程。
go f(x, y, z)
会启动一个新的 Go 协程并执行
f(x, y, z)
f
, x
, y
和 z
的求值发生在当前的 Go 协程中,而 f
的执行发生在新的 Go 协程中。
Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法(见下一页)。
package mainimport ("fmt""time"
)func say(s string) {for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond)fmt.Println(s)}
}func main() {go say("world")say("hello")
}
信道
信道是带有类型的管道,你可以通过它用信道操作符 <-
来发送或者接收值。
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:
ch := make(chan int)
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。
func sum(s []int, c chan int) {sum := 0for _, v := range s {sum += v}c <- sum // 发送 sum 到 c
}func main() {s := []int{7, 2, 8, -9, 4, 0}c := make(chan int)go sum(s[:len(s)/2], c)go sum(s[len(s)/2:], c)x, y := <-c, <-c // 从 c 接收fmt.Println(x, y, x+y) //-5 17 12
}
- 带缓冲的信道
信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make
来初始化一个带缓冲的信道:
ch := make(chan int, 100)
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。
修改示例填满缓冲区,然后看看会发生什么。
range和close
发送者可通过 close
关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完
v, ok := <-ch
此时 ok
会被设置为 false
。
循环 for i := range c
会不断从信道接收值,直到它被关闭。
注意: 只应由发送者关闭信道,而不应油接收者关闭。向一个已经关闭的信道发送数据会引发程序 panic。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range
循环。
package mainimport ("fmt"
)func fibonacci(n int, c chan int) {x, y := 0, 1for i := 0; i < n; i++ {c <- xx, y = y, x+y}close(c)
}func main() {c := make(chan int, 10)go fibonacci(cap(c), c)for i := range c {fmt.Println(i)}
}
select
select
语句使一个 Go 程可以等待多个通信操作。
select
会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
当 select
中的其它分支都没有准备好时,default
分支就会执行。
package mainimport "fmt"func fibonacci(c, quit chan int) {x, y := 0, 1for {select {case c <- x:x, y = y, x+ycase <-quit:fmt.Println("quit")return}}
}func main() {c := make(chan int)quit := make(chan int)go func() {for i := 0; i < 10; i++ {fmt.Println(<-c)}quit <- 0}()fibonacci(c, quit)
}