GO 基本语法

main 函数

image-20230128175231857

package main // 声明 main 包,表明当前是一个可执行程序

// func 为方法关键字
func main() { // main函数,是程序执行的入口
	println("Hello World!") // 在终端打印 Hello World!
}

main 函数要点

  • 无参数、无返回值
  • main 方法必须要在 main 包里面
  • go run main.go 就可以执行
  • 如果文件不叫 main.go ,则需要 go build 之后再 go run

包声明

  • 语法形式:package xxxx

  • 字母和下划线的组合

  • 可以和文件夹不同名字

  • 同一个文件夹下的声明一致

  • 引入包语法形式:import [alias] xxx

  • 如果一个包引入了但是没有使用,会报错

  • 匿名引入:前面多一个下划线

例:

package main // 声明 main 包,表明当前是一个可执行程序

import (
	"fmt"
	_ "strings"
)

func main() { // main函数,是程序执行的入口
	println("Hello World!") // 在终端打印 Hello World!
}

![image-20230128180814748](https://img2023.cnblogs.com/blog/1652001/202301/1652001-20230129150707970-1852947305.png)

string声明

  • 双引号引起来,则内部双引号需要使用 \ 转义
  • `引号引起来,则内部 ` 需要 \ 转义

例:

package main // 声明 main 包,表明当前是一个可执行程序

func main() { // main函数,是程序执行的入口
	// 一般推荐用于短的,不用换行的,不含双引号的
	print("He said: \" Hello Go  \" ")
	// 长的,复杂的。比如说放个 json 串
	print(`He said:"hello, Go"
	我还可以换个行`)
}

image-20230129102942686

string 长度

string 的长度很特殊:
字节长度:和编码无关,用 len(str)获取
字符数量:和编码有关,用编码库来计算

package main // 声明 main 包,表明当前是一个可执行程序

import "unicode/utf8"

func main() { // main函数,是程序执行的入口
	println(len("你好"))                            // 输出6
	println(utf8.RuneCountInString("你好"))         // 输出2
	println(utf8.RuneCountInString("你好 makallo")) // 输出10
}

image-20230129103812111

strings 包

中文参考文档:https://studygolang.com/pkgdoc

  • string 的拼接直接使用 + 号就可以。
  • 注意的是,某些语言支持 string 和别的类型拼接,但是golang 不可以
  • strings 主要方法(你所需要的在文档中全部都可以找到):
  • 查找和替换
  • 大小写转换
  • 子字符串相关
  • 相等

...

在这里不做赘述,直接查文档即可

rune 类型

rune (入嗯)img

  • rune,直观理解,就是字符
  • rune 不是 byte!
  • rune 本质是 int32,一个 rune 四个字节
  • rune 在很多语言里面是没有的,与之对应的是,golang 没有 char 类型。rune 不是数字,也不是 char,也不是 byte!
  • 实际中不太常用

官方解释

// 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.

//int32的别名,几乎在所有方面等同于int32
//它用来区分字符值和整数值

type rune = int32

bool, int, uint,float家族

  • bool: true, false (布尔)
  • int8, int16, int32, int64, int (整型)
  • uint8, uint16, uint32, uint64, uint (无符号整型)
  • float32, float64 (浮点型)

这个如果有其他语言基础很容易理解,不做赘述

byte 类型

  • byte,字节,本质是 uint8
  • 对应的操作包在 bytes 上

image-20230129110612262

image-20230129110400901

类型总结

  • golang 的数字类型明确标注了长度、有无符号
  • golang 不会帮你做类型转换,类型不同无法通过编译。也因此,string 只能和string 拼接, golang 有一个很特殊的 rune 类型,接近一般语言的 char 或者 character 的概念,非面试情况下,可以理解为 “rune = 字符”
  • string 遇事不决找 strings 包

变量声明 var

var,语法:

var name type = value
  • 局部变量
  • 包变量
  • 块声明
  • 驼峰命名
  • 首字符是否大写控制了访问性:大写包外可访问;
  • golang 支持类型推断

例:

package main // 声明 main 包,表明当前是一个可执行程序

// GLoba 首字母大写,全局可以访问
var Global = "全局变量"

// 首字母小写,只能在这个包里面使用
// 其子包也不能用
var local = "包变量"

var (
	First  string = "abc"
	second int32  = 16
)

func main() { // main函数,是程序执行的入口
	// 这里 int 可以省略,因为 golang 自己可以做类型推断,他觉得你可以省略
	var a int = 13
	println(a)

	// 这里省略了类型
	var b = 14
	println(b)

	// 这里uint 不可省略,因为省略之后, 不加uint类型,15会被解释为 int 类型
	var c uint = 15
	println(c)

	// 这一句无法编译通过, 因为 golang 是强类型语言,并且不会帮你做任何的转换
	println(a == c)
}

image-20230129113311497

变量声明 :=

  • 只能用于局部变量,即方法内部

  • golang 使用类型推断来推断类型。数字会被理解为 int 或者 float64。(所以要其它类型的数

    字,就得用 var 来声明)

package main // 声明 main 包,表明当前是一个可执行程序

func main() { // main函数,是程序执行的入口
	a := 13
	println(a)

	b := "你好"
	println(b)
}

变量声明易错点

  • 变量声明了没有使用
  • 类型不匹配
  • 同作用域下,变量只能声明一次
package main // 声明 main 包,表明当前是一个可执行程序

var aa = "hello"

func main() { // main函数,是程序执行的入口
	// 虽然包外面已经已经有个 aa 了,但是这里从包变成局部变量
	aa := 13
	println(aa)

	var bb = 15
	// var bb = 16 // 重复声明, 编译不通过
	println(bb)

	bb = 17 // 通过, 只是赋值了新的值
	// bb := 18 // 不行 , 因为 := 就是声明并赋值的简写,相当于是重复声明
}

常量声明const

  • 首字符是否大写控制了访问性:大写包外可访问;
  • 驼峰命名
  • 支持类型推断
  • 无法修改值
package main // 声明 main 包,表明当前是一个可执行程序

const internal = "包内可访问"
const External = "包外可访问"

func main() { // main函数,是程序执行的入口
	const a = "你好"
	println(a)
}

方法声明与调用

四个部分:

关键字 : func
方法名字:首字母是否大写决定了作用域
参数列表:[<name type>]
返回列表: [<name type>]

例:

package main // 声明 main 包,表明当前是一个可执行程序

// Fun0 只有一个返回值, 不需要用括号括起来
func Fun0(name string) string {
	return "hello, " + name
}

// Fun1 多个参数, 多个返回值。参数有名字,但是返回值没有
func Fun1(a string, b int) (int, string) {
	return 0, "你好"
}

// 推荐上面 Fun0 和 Fun1 的写法

// Fun2 的返回值具有名字, 可以在内部直接赋值,然后返回
// 也可以忽略age , name 直接返回别的
func Fun2(a string, b string) (age int, name string) {
	age = 18
	name = "makalo"
	return // 这种返回 也是返回  19, "makalo"
	// return 19, "makalo" // 这样返回也可以
}

// Fun3 多个参数具有相同类型放在一起,可以只写一次类型
func Fun3(a, b, c string, abc, bcd int, p string) (d, e int, g string) {
	d = 15
	e = 16
	g = "你好"
	return
	// return 0, 0, "你好" //这样也可以
}

func main() { // main函数,是程序执行的入口
	a := Fun0("makalo")
	println(a)

	b, c := Fun1("a", 18)
	println(b)
	println(c)

	// 使用 _ 忽略返回值, 这是匿名变量
	_, d := Fun2("a", "b")
	println(d)
}

image-20230129145845034

方法声明与调用总结

  • golang 支持多返回值,这是一个很大的不同点
  • golang 方法的作用域和变量作用域一样,通过大小写控制
  • golang 的返回值是可以有名字的,可以通过给予名字让调用方清楚知道你返回的是什么

fmt 格式化输出

fmt 包有完整的说明:

https://pkg.go.dev/fmt

掌握常用的:%s, %d, %v, %+v, %#v 即可, 更加详细的可以参考:

https://www.cnblogs.com/rickiyang/p/11074171.html

%s     字符串或切片的无解译字节
%d     十进制表示
%v     值的默认格式。
%+v   类似%v,但输出结构体时会添加字段名
%#v  相应值的Go语法表示 

不仅仅是 fmt的调用,所有格式化字符串的 API 都可以用
因为golang字符串拼接只能在 string 之间,所以这个包非常常用

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func main() { // main函数,是程序执行的入口
	name := "makalo"
	age := 18
	// 这个 API 是返回字符串的,所以大多数时候我们都是用这个
	str := fmt.Sprintf("hello, I am %s, I am %d years old n", name, age)
	println(str)
	//这个是直接输出,一般简单程序 DEBUG 会用它输出到一些信息到控制台
	fmt.Printf("hello, I am %s, I am %d years old n", name, age)
}

image-20230129153602293

数组与切片

数组

数组和别的语言的数组差不多,语法是:

[cap]type
  1. 初始化要指定长度(或者叫做容量)
  2. 直接初始化
  3. arr[i]的形式访问元素
  4. len 和 cap 操作用于获取数组长度
  5. 数组的len 和 cap 结果是一样的,就是数组的长度

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func main() { // main函数,是程序执行的入口
	// 直接初始化一个三个元素的数组。大括号里面多一个或者少一个都编译不通过
	a1 := [3]int{9, 8, 7}
	fmt.Printf("a1: %v, len: %d, cap: %d \n", a1, len(a1), cap(a1))

	// 初始化一个三个元素的数组,所有元素都是0
	var a2 [3]int
	fmt.Printf("a2: %v, len: %d, cap: %d \n", a2, len(a2), cap(a2))

	//a1 = append(a1,12) 数组不支持 append 操作

	// 按下标索引
	fmt.Printf("a1[1]: %d \n", a1[1])
	// 超出下标范围,直接崩溃,编译不通过
	//fmt.Printf("a1[99]: %d", a1[99])
}

image-20230129160832233

切片

切片,语法:

[]type
  1. 直接初始化
  2. make初始化:make([]type, length, capacity)
  3. arr[i] 的形式访问元素
  4. append 追加元素
  5. len 获取元素数量
  6. cap 获取切片容容量
  7. 推荐写法:s1 := make([]type, 0, capacity)

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func main() { // main函数,是程序执行的入口
	s1 := []int{1, 2, 3, 4} // 直接初始化了 4 个元素的切片
	fmt.Printf("s1: %v, en %d, cap: %d \n", s1, len(s1), cap(s1))

	s2 := make([]int, 3, 4) // 创建了一个包含三个元素,容量为4的切片
	fmt.Printf("s2: %v, len %d, cap: %d \n", s2, len(s2), cap(s2))

	s2 = append(s2, 7) // 后边添加一个元素,没有超出容量限制,不会发生扩容
	fmt.Printf("s2: %v, len %d, cap: %d \n", s2, len(s2), cap(s2))

	s2 = append(s2, 8) // 后边添加了一个元素,触发扩容
	fmt.Printf("s2: %v, len %d, cap: %d \n", s2, len(s2), cap(s2))

	s3 := make([]int, 4) // 只传入一个参数,表示创建一个含有四个元素,容量也为四个元素的
	fmt.Printf("s3: %v, len %d, cap: %d \n", s3, len(s3), cap(s3))

	// 按下标索引
	fmt.Printf("s3[2]: %d", s3[2])
	//超出下标范围,直接崩溃
	// runtime error: index out of range [99] with length 4
	fmt.Printf("s3[99]: %d", s3[99])
}

image-20230129162331632

上面结果显示,直接 cap 变成长度的一倍,那切片的扩容原则是什么呢?

切片的扩容原则

  • 如果切片的容量小于1024个元素,那么扩容的时候slice的cap就乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。

  • 如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。

区别

遇事不决用切片,基本不会有问题

image-20230129162446030

子切片

数组和切片都可以通过[start:end] 的形式来获取子切片:

  1. arr[start:end],获得[start, end)之间的元素
  2. arr[:end],获得[0, end) 之间的元素
  3. arr[start:],获得[start, len(arr))之间的元素

获取的子切片的值,遵循左闭右开,也就是包头不包尾

例:

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func main() { // main函数,是程序执行的入口
	s1 := []int{2, 4, 6, 8, 10}
	s2 := s1[1:3]
	fmt.Printf("s2: %v, len %d,cap: %d \n", s2, len(s2), cap(s2))

	s3 := s1[2:]
	fmt.Printf("s3: %v, len %d,cap: %d \n", s3, len(s3), cap(s3))

	s4 := s1[:3]
	fmt.Printf("s4: %v, len %d, cap: %d \n", s4, len(s4), cap(s4))
}

image-20230129163752900

如何理解切片

最直观的对比:ArrayList,即基于数组的 List 的实现,切片的底层也是数组跟 ArrayList 的区别:

  1. 切片操作是有限的,不支持随机增删(即没有 add, delete 方法,需要自己写代码)
  2. 只有 append 操作
  3. 切片支持子切片操作,和原本切片是共享底层数组

遇事不决用切片,不容易错

共享底层(optional)

核心:共享数组

子切片和切片究竟会不会互相影响,就抓住一点:它们是不是还共享数组?

什么意思?就是如果它们结构没有变化,那肯定是共享的; 但是结构变化了,就可能不是共享了

类似于引用的概念,改变了引用的值,对应的引用对象的值也会发生改变

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func main() { // main函数,是程序执行的入口
	fmt.Printf("共享底层数组 初始切片和子切片结构一样 \n")
	s1 := []int{1, 2, 3, 4}
	s2 := s1[2:]
	fmt.Printf("s1: %v, Len %d, cap: %d \n", s1, len(s1), cap(s1))
	fmt.Printf("s2: %v, len %d, cap: %d \n", s2, len(s2), cap(s2))

	fmt.Printf("共享底层数组 s2 子切片 发生改变,对应 s1 切片 也跟着改变 \n")
	s2[0] = 99
	fmt.Printf("s1: %v, len %d, cap: %d \n", s1, len(s1), cap(s1))
	fmt.Printf("s2: %v, len %d, cap: %d \n", s2, len(s2), cap(s2))

	fmt.Printf("不是共享底层 s2 子切片 发生改变,对应 s1 切片 没有改变 \n")
	s2 = append(s2, 199)
	fmt.Printf("s1: %v, len %d, cap: %d \n", s1, len(s1), cap(s1))
	fmt.Printf("s2: %v, len %d, cap: %d \n", s2, len(s2), cap(s2))

	fmt.Printf("不是共享底层 s2 子切片 发生改变,对应 s1 切片 没有改变 \n")
	s2[1] = 1999
	fmt.Printf("s1: %v, len %d, cap: %d \n", s1, len(s1), cap(s1))
	fmt.Printf("s2: %v, len %d, cap: %d \n", s2, len(s2), cap(s2))
}

image-20230129165957545

for

for 和别的语言差不多,有三种形式:

  1. for {} ,类似 while 的无限循环
  2. fori,一般的按照下标循环
  3. for range 最为特殊的 range 遍历
  4. break 和 continue 和别的语言一样
package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func main() { // main函数,是程序执行的入口
	arr := []int{9, 8, 7, 6}

	// go 里面没有where , 直接使用for
	index := 0
	for {
		if index == 3 {
			// break 跳出循环
			break
		}
		fmt.Printf("%d => %d \n", index, arr[index])
		index++
	}
	fmt.Println("for loop end1 ")

	// fori 形式 遍历下标
	for i := 0; i < len(arr); i++ {
		fmt.Printf("%d => %d \n", i, arr[i])
	}

	fmt.Println("for loop end2 ")
	// for range 形式
	// 如果只是需要 value,可以用 _ 代替 index
	// 如果只需要 index 也可以去掉 写成 for index := range arr
	for index, value := range arr {
		fmt.Printf("%d => %d \n", index, value)
	}
	fmt.Println("for loop end3 ")
}

image-20230129171735029

if - else

if-else 和别的语言也差不多, 需要注意的是

带局部变量声明的 if- else:

  1. distance 只能在 if 块,或者后边所有的 else 块里面使用
  2. 脱离了 if - else 块,则不能再使用

例:

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func IfUsingNewVariable(start int, end int) {
	if distance := end - start; distance > 100 {
		fmt.Printf("距离太远,不来了: %d\n", distance)
	} else {
		// else 分支也可以没有
		fmt.Printf("距离并不远,来一趟: %d\n", distance)
	}
	// 这里不能访问 distance
	//fmt.Printf("距离是: %d\n",distance)
}
func main() { // main函数,是程序执行的入口
	IfUsingNewVariable(20, 200)
}

switch

  • switch 和别的语言差不多
  • switch 后面可以是基础类型和字符串,或者满足特定条件的结构体
  • 最大的差别: 终于不用加 break 了!
package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

func ChooseFruit(fruit string) {
	switch fruit {
	case "苹果":
		fmt.Println("这是一个苹果")
	case "草莓", "蓝莓":
		fmt.Println("这是霉霉")
	default:
		fmt.Printf("不知道是啥: %s \n", fruit)
	}
}
func main() { // main函数,是程序执行的入口
	ChooseFruit("蓝莓")
}

type 定义

基本语法

type 名字 interface {}
type 名字 struct {}
type 名字 别的类型
type 别名 = 别的类型

type 有啥用?

我们使用一个东西或者学习一个东西,有啥用这个问题,是一开始就要搞明白的,

先看段代码

package main // 声明 main 包,表明当前是一个可执行程序

import (
	"fmt"
	"net/http"
)

func form(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "before parse form %v\n", r.Form)
	err := r.ParseForm()
	if err != nil {
		fmt.Fprintf(w, "parse form error %v\n", r.Form)
	}
	fmt.Fprintf(w, "before parse form %v\n", r.Form)
}

func getBodyIsNil(w http.ResponseWriter, r *http.Request) {
	if r.GetBody == nil {
		fmt.Fprint(w, "GetBody is nil \n")
	} else {
		fmt.Fprintf(w, "GetBody not nil  \n")
	}
}

func main() { // main函数,是程序执行的入口
	http.HandleFunc("/form", form)
	http.HandleFunc("/getBodyIsNil", getBodyIsNil)
	// 如果我想启动两个服务器,一个监听 8080 ,一个监听8081,8081 作为管理端口
	http.ListenAndServe("127.0.0.1:8080", nil)
}

就像上面的代码一样,我路由是路由,端口监听是端口监听,他们逻辑上的联系其实并不紧密,但是实际上可以统称为Server 所具备的东西,是逻辑上的抽象,type 就是 可以定义这种抽象,如果有面向对象语言基础应该理解很快

interface 接口定义

基本语法

 type 名字 interface {}
  • 里面只能有方法,方法也不需要 func 关键字
  • 啥是接口(interface):接口是一组行为的抽象
  • 尽量用接口,以实现面向接口编程
  • 当你怀疑要不要用接口的时候,加上去总是很保险的

例:

type Server interface {
	// Route 设定一个路由,命中该路由的会执行 HandlerFunc 的代码
	Route(pattern string, HandlerFunc http.HandlerFunc)

	// Start 启动我们的服务器
	Start(address string) error
}

空接口 interface{}

  • 空接口 interface{} 不包含任何方法
  • 所以任何结构体都实现了该接口,可以接收任意值
  • 类似于 Java 的 Object, 即所谓的继承树根节点

如:

image-20230131105904978

struct 结构体定义

基本语法

结构体和结构体的字段都遵循大小写控制 访问性的原则

type Name struct {
    fieldName FieldType
    // ...
}

初始化

  • Go 没有构造函数!!
  • 初始化语法:Struct{}
  • 获取指针: &Struct{}
  • 获取指针2:new(Struct)
  • new 可以理解为 Go 会为你的变量分配内存,并且把内存都置为0

初始化的几种方式:

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

type ToyDuck struct {
	Color string
	Name  string
}

func main() { // main函数,是程序执行的入口
	// 初始化的几种方式
	// 1
	duck1 := &ToyDuck{}
	fmt.Printf("ToyDuck1: %s \n", duck1.Color)

	// 2
	duck2 := ToyDuck{}
	fmt.Printf("ToyDuck2: %s \n", duck2.Color)

	// 3
	duck3 := new(ToyDuck)
	fmt.Printf("ToyDuck3: %s \n", duck3.Color)

	//4
	// 当你声明这样的时候,go 就帮你分配好内存
	// 不用担心空指针的问题,因为它压根就不是指针
	var duck4 ToyDuck
	fmt.Printf("ToyDuck4: %s \n", duck4.Color)

	// 5
	// 这样就是一个指针,但是 是个空指针
	// panic: runtime error: invalid memory address or nil pointer dereference
	var duck5 *ToyDuck
	fmt.Printf("ToyDuck5: %s \n", duck5.Color)
}

image-20230130164959835

字段赋值

字段赋值的几种方式

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

type ToyDuck struct {
	Color string
	Name  string
}

func main() { // main函数,是程序执行的入口
	// 字段赋值的几种方式
	// 1
	// 赋值,初始化按字段名赋值
	duck1 := ToyDuck{
		Color: "黄色",
		Name:  "tom",
	}
	fmt.Printf("duck1 %+v \n", duck1)

	// 2
	// 初始化按字段顺序赋值, 不建议使用
	duck2 := ToyDuck{"绿色", "武大郎"}
	fmt.Printf("duck2 %+v \n", duck2)

	// 3
	// 初始化后 再单独赋值
	duck3 := ToyDuck{}
	duck3.Color = "橙色"
	duck3.Name = "果粒橙"
	fmt.Printf("duck3 %+v \n", duck3)
}

image-20230130170137255

指针

  • 和 C,C++ 一样,*表示指针,&取地址
  • 如果声明了一个指针,但是没有赋值,那么它是 nil
  • go 的指针不能运算,但是 unsafe 包 可以绕过go类型的安全检查,直接操控内存,但是正如他的名字一样unsafe,不安全

例:

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

type ToyDuck struct {
	Color string
	Name  string
}

func main() { // main函数,是程序执行的入口
	// 指针用 * 表示
	var p *ToyDuck = &ToyDuck{}
	// 解引用,得到结构体
	var duck ToyDuck = *p
	fmt.Printf("duck %+v \n", duck)

	// 只是声明了,但是没有使用
	var nilDuck *ToyDuck
	if nilDuck == nil {
		fmt.Println("nilDuck is nil")
	}
}

image-20230130172520094

结构体自引用

  • 结构体内部引用自己,只能使用指针
  • 准确来说,在整个引用链上,如果构成循环,那就只能用指针

例:

type Node struct {
	//自引用只能使用指针
	// Teft Node
	// right Node
	TLeft *Node
	right *Node
	// 这个也会报错
	// nn NodeNode
}
type NodeNode struct {
	node Node
}

方法接收器

方法接收器(也有叫接收者的)是什么?

我们思考一个问题,如何给结构体添加方法?

方法接收器就能实现这个,而方法接收器又分为两种

  • 结构体接收器
  • 指针接收器

而这两种有什么区别,看下面代码

PS : 结构体和指针之间的方法可以互相调用

package main // 声明 main 包,表明当前是一个可执行程序

import "fmt"

type User struct {
	Name string
	Age  int
}

// 结构体接收器
func (u User) ChangeName(newNames string) {
	u.Name = newNames
}

// 指针接收器
func (u *User) ChangeAge(newAge int) {
	u.Age = newAge
}

func main() { // main函数,是程序执行的入口

	// 初始化
	u := User{
		Name: "makalo",
		Age:  18,
	}
	// 因为 ChangeName 接收器 接收的是结构体,所以方法调用的时候它数据是不会变的
	u.ChangeName("吴彦祖")
	// 因为 ChangeAge 接收器 接收的是指针,所以方法调用的时候它数据会变
	u.ChangeAge(10086)
	fmt.Printf("调用修改方法之后的数据:%+v \n", u)
}

image-20230130175512929

结构体如何实现接口

Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可

  • 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
  • 当一个结构体具备接口的所有的方法的时候,它就实现了这个接口

这也是go 实现多态的方式

基础实现
package main // 声明 main 包,表明当前是一个可执行程序

import (
	"fmt"
)

// 接口
type Person interface {
	getName() string
	getAge() int
}

// 结构体
type Student struct {
	name string
	age  int
}

// 结构体类型 也有 叫 值类型
func (stu Student) getName() string {
	return stu.name
}

// 指针类型
func (stu *Student) getAge() int {
	return stu.age
}

func main() { // main函数,是程序执行的入口
	s := &Student{
		name: "makalo",
		age:  18,
	}
	fmt.Printf("s.getName() : %s \n", s.getName())
	fmt.Printf("s.getAge() : %d \n", s.getAge())
}

image-20230130184611106

体现多态的实现

出自:https://www.runoob.com/go/go-interfaces.html

package main

import (
	"fmt"
)

type Phone interface {
	call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
	fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
	fmt.Println("I am iPhone, I can call you!")
}

func main() {
	var phone Phone

	phone = new(NokiaPhone)
	phone.call()

	phone = new(IPhone)
	phone.call()

}

image-20230130184715061

别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

    type TypeAlias = Type 

我们之前见过的rune和byte就是类型别名,他们的定义如下:

    type byte = uint8
    type rune = int32

map

  • 编译器会告诉你能不能做 key
  • 尽量用基本类型和string做key,不要和自己过不去

基础语法

基本形式:map[KeyType]ValueType
创建 make 命令,或者直接初始化
取值:val, ok := m[key]
设值:m[key]=val
key 类型:“可比较”类型

map 遍历

  • for key, val := range m {}
  • Go 一个 for 打天下
  • Go 的 map 的遍历,顺序是不定的

实例

package main

import "fmt"

func main() {
	// 创建了一个预估容量是2的 map
	m := make(map[string]string, 2)
	// 没有指定预估容量
	m1 := make(map[string]string)
	// 直接初始化
	m2 := map[string]string{
		"Tom": "Jerry",
	}

	// 赋值
	m["hello"] = "world"
	m1["hello"] = "world"
	// 赋值
	m2["hello"] = "world"
	// 取值
	val := m["hello"]
	println(val)

	// 再次取值,使用两个返回值,后面的ok会告诉你map有没有这个key
	val, ok := m["invalid_key"]
	if !ok {
		println("key not found")
	}

	for key, val := range m {
		fmt.Printf("%s => %s \n", key, val)
	}
}

image-20230131111008743

组合

  • 组合可以是接口组合
  • 可以是结构体组合
  • 结构体也可以组合接口
package main

import "fmt"

// 基础接口
type Swimming interface {
	Swim()
}

// 接口组合
type Duck interface {
	// 鸭子是会游泳的,所以这里组合了它
	Swimming
}

// 基础结构体
type Base struct {
	Name string
}

func (b *Base) SayHello() {
	fmt.Printf(" b.Name : %s \n", b.Name)
}

// 结构体组合
// 下面两种写法都可以
type Concrete1 struct {
	Base
}

type Concrete2 struct {
	*Base
}

func (c Concrete1) SayHello() {
	// c.Name 直接访问了Base的Name字段
	fmt.Printf(" c.Name : %s \n", c.Name)
	// 这样也是可以的
	fmt.Printf(" c.Base.Name : %s \n", c.Base.Name)

	// 调用了被组合的
	c.Base.SayHello()
}

func main() {
	b := &Base{Name: "基础结构体"}
	b.SayHello()

	c := &Concrete1{Base: Base{Name: "Concrete1 结构体"}}
	c.SayHello()
}

image-20230201180356485

组合与重写

  • Go 严格意义上没有重写,但是我们可以实现重写
  • 当你写下类似继承的代码的时候,千万要先试试它会调 过去哪个方法

例:

package main

import "fmt"

type Parent struct {
}

func (p Parent) SayHello() {
	fmt.Println("I am " + p.Name())
}

func (p Parent) Name() string {
	return "Parent"
}

type Son struct {
	Parent
}

// 定义了自己的 Name() 方法
func (s Son) Name() string {
	return "Son"
}

func main() {
	son := Son{
		Parent{},
	}

	son.SayHello()
}

image-20230201181608436

  • 如上面代码,main 函数会输出 I am Parent
  • 而在典型的支持重写的语言,如Java,我们可以期望它输出 I am Son

因为在java 中 如果继承子类,有同名方法,子类中的方法会重写父类的,而上面代码中有同名方法 name(),但是并没有重写,所以组合不是继承

在golang 中是提倡组合,不提倡继承的

组合的优点

  1. 可以利用面向接口编程原则的一系列优点,封装性好,耦合性低
  2. 相对于继承的编译期确定实现,组合的运行态指定实现,更加灵活
  3. 组合是非侵入式的,继承是侵入式的

如何利用组合代替继承重写

如果一定要实现类似继承的特性,只需要加上

func (s Son) SayHello() {
	fmt.Println("I am " + s.Name())
}

完整如下

package main

import "fmt"

type Parent struct {
}

func (p Parent) SayHello() {
	fmt.Println("I am " + p.Name())
}

func (p Parent) Name() string {
	return "Parent"
}

type Son struct {
	Parent
}

// 定义了自己的 Name() 方法
func (s Son) Name() string {
	return "Son"
}

func (s Son) SayHello() {
	fmt.Println("I am " + s.Name())
}

func main() {
	son := Son{
		Parent{},
	}

	son.SayHello()
}

image-20230201182355691

defer

  • 用于在方法返回之前执行某些动作
  • 像栈一样,先进后出
  • defer 语义接近 java 的 finally 块 所以我们经常使用 defer 来释放资源,例如释放 锁

例:

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("aaa")
	}()

	defer func() {
		fmt.Println("bbb")
	}()

	defer func() {
		fmt.Println("ccc")
	}()
}

image-20230202151709139

闭包

  • 函数闭包:匿名函数 + 定义它的上下文
  • 它可以访问定义之外的变量
  • Go 很强大的特性,很常用
  • 函数里面可以定义函数和调佣,方法也可以返回函数

如果有 js 基础的很好理解,几乎跟js 的闭包一样

例:

package main

import (
	"fmt"
)

func main() {

	i := 13
	a := func() {
		fmt.Printf("i is %d \n", i)
	}
	a()

	fmt.Println(ReturnClosure("Tom")())
}

func ReturnClosure(name string) func() string {
	return func() string {
		return "Hello, " + name
	}
}

image-20230202152205908

闭包延时绑定

闭包里面使用的闭包外的参数,其值是在最终调用的时 候确定下来的

ps: 跟js 里面一样

例:

package main

import (
	"fmt"
)

func main() {
	Delay()
}

func Delay() {
	fns := make([]func(), 0, 10)
	for i := 0; i < 10; i++ {
		fns = append(fns, func() {
			fmt.Printf("hello, this is : %d \n", i)
		})
	}

	for _, fn := range fns {
		fn()
	}
}

输出

image-20230202161744143

如何解决延时绑定

其实跟解决 js 的 闭包参数绑定一样的逻辑,话不多说看代码

package main

import (
	"fmt"
)

func main() {
	Delay()
}

func Delay() {
	// 创建 func 切片
	fns := make([]func(), 0, 10)
	
	// 赋值
	for i := 0; i < 10; i++ {
		// 会延时绑定
		// fns = append(fns, func() {
		// 不会,因为 闭包保存/记录了它产生时的外部函数的所有环境,
		// 这里在外面加一层 自执行函数闭包并将参数传入,就会将此时的值保存起来
		func(index int) {
			fns = append(fns, func() {
				fmt.Printf("hello, this is : %d \n", index)
			})
		}(i)
	}

	// 遍历 func 切片
	for _, fn := range fns {
		fn()
	}
}

image-20230202164724587

类型断言

用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型

语法

// T 可以是结构体或者指针,即 可以是基础类型 或者 自定义类型,也就是所有的
// X 如果是 T 类型,会被转换成T类型并赋值给value (两个类型一样也没必要转换,直接赋值) 
// ok 是两个类型是否一样,返回true/false
value, ok := x.(T) 
// 或者
value :=x.(T)

如何理解

  • 即x是不是T。
  • 类似 Java instanceOf + 强制类型转换 合体
  • 如果 x 是 nil,那么永远是 false
  • 编译器不会帮你检查,如果不接收检测结果参数,当发生断言失败(转换失败)状态,会直接panic,所以一般第二个参数是要接收的

案例

package main

import (
	"fmt"
)

func main() {
	var i interface{} = 10
	// 转换成功,返回 ok 为 true
	t1, ok := i.(int)
	fmt.Println(t1, ok)
	fmt.Println("=====分隔线=====")

	// 转换失败,返回 ok 为false
	t2, ok := i.(string)
	fmt.Println(t2, ok)

	fmt.Println("=====分隔线=====")

	// 转换失败,返回 ok  触发panic
	// panic: interface conversion: interface {} is int, not float32
	t3 := i.(float32)
	fmt.Println(t3)
}

image-20230203111903149

类型转换

语法

y := T(x)

如何理解

类似Java强制类型转换

常用和注意点

  • 数字类型转换
  • string 和 []byte 互相转
  • 编译器会进行类型检查,不能转换的会编译错误
package main

import (
	"fmt"
)

func main() {
	a := 12.0
	b := int(a)
	fmt.Println(b)

	str := "Hello"
	bytes := []byte(str)
	fmt.Printf("%s \n", bytes)

	// 对比 ASCII 码表 ,字节切片
	bytes2 := []byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}
	str2 := string(bytes2)
	fmt.Printf("%s \n", str2)

	bytes3 := []byte("Hello World str byte")
	str3 := string(bytes3)
	fmt.Printf("%s \n", str3)
}

image-20230203124411525

不定参数

语法

 name...Type 
  1. 只能作为最后一个参数
  2. 调用方可以传入任意多个,一个不传也可以
  3. 方法内部可以将不定参数看做切片处理
  4. 调用的时候可以在切片后面加 ... 来作为不定参 数的值

案例

package main

import "fmt"

func Fun(a string, b int, names ...string) {
	// 使用的时候就可以直接把 names 看做切片
	for _, name := range names {
		fmt.Printf("不定参数:%s \n", name)
	}
}

func main() {
	// 不定参数后面可以传递任意多个值
	Fun("hello", 18, "world", "makalo", "golang")
	s := []string{"帅气", "多金"}
	Fun("hello", 16, s...)
}

image-20230204230532919

init 方法

无参数

无返回值

包被引入的时候执行,只执行一次!

顺序不定(目前是按照所在文件名排序)

可以有多个(一个包,一个文件内都可以有多个)

语法

package main

func init() {
	// 第一个
	println("第一个 init 方法")
}

func init() {
	// 第二个
	println("第二个 init 方法")
}
func main() {

}

image-20230204233513706

如果我要保证 init 方法按照顺序执行,该如何做?用一个 init 方法,里面调用真实的逻辑

package main

func init() {
	// 因为我们不能确定 init 方法的执行顺序,
	// 只能曲线救国
	initBeforeSomething()
	initSomething()
	initAfterSomething()
}

func initBeforeSomething()  {
	
}

func initSomething()  {
	
}

func initAfterSomething()  {
	
}
posted @ 2023-01-29 17:56  makalo  阅读(193)  评论(0编辑  收藏  举报