golang基础入门学习(一)

Hello World

写下你的第一个go程序

package main

import "fmt"

func main() {
   fmt.Println("Hello 世界!")
}

// 执行文件
// 第一种:生成可执行文件.exe
1.go build main.go		# -o new_name 生成新名字
2.main.exe

// 第二种:直接执行
go run main.go

  • package关键字声明了是当前 go 文件属于哪一个包,入口文件都必须声明为main包,入口函数是main函数,在自定义包和函数时命名应当尽量避免与之重复。

  • import是导入关键字,后面跟着的是被导入的包名。

  • func是函数声明关键字,用于声明一个函数。

  • fmt.Println(“Hello 世界!”)是一个语句,调用了fmt包下的Println函数进行输出。

go语言的包管理模式

定义可见性

  • 名称大写字母开头,即为公有类型/变量/常量
  • 名字小写或下划线开头,即为私有类型/变量/常量
// 公有
const MyName = "jack"

// 私有
const mySalary = 20_000

多包导入

在第一个例子中我们写出了导入单个包 go语言的多包导入如下

// 第一种
package main
import "example"
import "example1"

// 第二种 建议这样
package main
import (
  "example"
  "example1"
)

// 给导入的包起别名
package main
import (
  e "example"
  e1 "example1"
)

`go语言禁止循环导入
在Go中完全禁止循环导入,不管是直接的还是间接的。例如包A导入了包B,包B也导入了包A,这是直接循环导入,包A导入了包C,包C导入了包B,包B
又导入了包A,这就是间接的循环导入,存在循环导入的话将会无法通过编译。`

go mod

go语言的导包方式有两种一种是go path 另一种是go mod

  • 使用go path时 需要导入src目录开始的绝对路径
  • 在go1.11版本时引用了go mod 以下是使用方法
// 在项目根目录 终端执行
go mod init NewName   // NewName为自定义的go mod名称

// 引用时导入NeWName/包名即可
// 不在需要使用go path从src目录逐个导入

// 拉取没有的包
go mod tidy

注释

// 这是单行注释

/* 这是
多行
注释 */ 

标识符

标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:

  • 只能由字母,数字,下划线组成
  • 只能以字母和下划线开头
  • 严格区分大小写
  • 不能与任何已存在的标识符重复,即包内唯一的存在
  • 不能与 Go 任何内置的关键字冲突
break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

运算符

算数运算符

在这里插入图片描述

关系运算符

在这里插入图片描述

逻辑运算符

在这里插入图片描述

位运算符

在这里插入图片描述

赋值运算符

在这里插入图片描述

基本数据类型

整型

默认为0

整型分为以下两个大类: 按长度分为:int8int16int32int64对应的无符号整型:uint8uint16uint32uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

浮点型

Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32。 float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64。

复数

complex64complex128

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

布尔类型bool

truefalse

布尔类型变量的默认值为false。

Go 语言中不允许将整型强制转换为布尔型.

布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串string

  • 支持索引和切片
  • go不支持负索引
package main

import "fmt"

func main() {
	var str string
	
	// 定义字符串
	str = "Hello World"
	
	// 定义多行字符串 ``反引号
	info := `
	1.hello
	2.world
	3.hhhhh
	`

	// 打印单字符需要string()
	fmt.Println(string(str[0])) 
	
	// 支持切片[start:stop]左闭右开
	fmt.Println(str[0:5])		
}

strings函数

方法作用
len()求字符串长度
strings.ToUpper() strings.ToLower()字符串全部转大写/小写
strings.HasPrefix() strings.HasSuffix()判断字符串以什么开头/结尾
strings.Contains()判断字符串包不包含该字符串
strings.TrimSpace()仅去除两端空格
strings.Trim() strings.TrimLeft() strings.TrimRight()去除两端/左侧/右侧字符
strings.Index()找出第一个字母所在的索引
strings.Split() strings.Join()分割(本质切片)与拼接
strings.ReplaceAll()字符串替换(全部)
package main

import (
	"fmt"
	"strings"
)

func main() {
	// 字符串全部转大写/小写
	var a = "Hello"
	A := strings.ToUpper(a)
	b := strings.ToLower(a)
	fmt.Println(A)
	fmt.Println(b)

	// 判断字符串以什么开头 结尾  返回true/false
	var s = "hello world"
	fmt.Println(strings.HasPrefix(s, "hello"))
	fmt.Println(strings.HasSuffix(s, "d"))
	// 判断字符串包不包含该字符串
	fmt.Println(strings.Contains(s, "llo"))

	name := " xyh "
	fmt.Println(name)
	// 去除两端字符 去除的为最外层
	fmt.Println(strings.Trim(name, " "))
	// 只能去除两端空格
	fmt.Println(strings.TrimSpace(name))
	// 仅去除左边字符
	fmt.Println(strings.TrimLeft(name, " "))
	// 仅去除左边字符
	fmt.Println(strings.TrimRight(name, " "))

	// index : 索引 找出第一个字母所在的索引
	var s2 = "hello world"
	fmt.Println(strings.Index(s2, "world"))

	// 分割与拼接
	var s3 = "hello my world"
	s4 := strings.Split(s3, " ")
	fmt.Println(s4)
	s5 := strings.Join(s4, "-")
	fmt.Println(s5)

	var s6 = "hello"
	fmt.Println(strings.ReplaceAll(s6, "l", "p"))
}

类型转换

第一种方法

package main

import (
	"fmt"
	"reflect"
	"strconv"
)

func main() {
	// 整形之间的转换 int64转int8
	a := 10
	b := int8(a)
	fmt.Println(a, b)

	// 字符串与整数的转型
	c := "32"
	fmt.Println("c =", c, reflect.TypeOf(c))
	
	// 字符串转整型
	d, _ := strconv.Atoi(c) // 返回值是两个 需要匿名函数
	fmt.Println("d =", d, reflect.TypeOf(d))
	
	// 整形转字符串
	e := strconv.Itoa(d)
	fmt.Println("e =", e, reflect.TypeOf(e))
}

第二种方法

// strconv.Parse系列函数
package main
import (
	"fmt"
	"reflect"
	"strconv"
)
func main() {
	//  ParseInt
	//  输入:1.数字的字符串形式 2.base,数字字符串的进制,比如:2进制、10进制。
	//       3.bitSize的含义是⼤⼩限制,如果字符串转化的整形数据类型超过bitSize的最大值,那么输出的int64为bitSize的最大值,err就会显⽰数据超出范围。
	i1, _ := strconv.ParseInt("1000", 10, 8)
	println(i1) // 超出范围打印127
	i2, _ := strconv.ParseInt("1000", 10, 64)
	println(i2)

	// ParseFloat
	// 输入:1.浮点数的字符串形式
	//      2.bitSize的含义是⼤⼩限制,如果字符串转化的浮点数数据类型超过bitSize的最大值,那么输出的float为bitSize的最大值,err就会显⽰数据超出范围。
	f2, _ := strconv.ParseFloat("3.1 415926", 64)
	fmt.Println(f2, reflect.TypeOf(f2))
	f1, _ := strconv.ParseFloat("3.1415926", 32)
	fmt.Println(f1, reflect.TypeOf(f1))

	// ParseBool
	// 输入字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。
	// 返回两个信息 要转换的字符+err
	b1, _ := strconv.ParseBool("true")
	fmt.Println(b1, reflect.TypeOf(b1))
	b2, _ := strconv.ParseBool("T")
	fmt.Println(b2, reflect.TypeOf(b2))
	b3, _ := strconv.ParseBool("1")
	fmt.Println(b3, reflect.TypeOf(b3))
	b4, _ := strconv.ParseBool("100")
	fmt.Println(b4, reflect.TypeOf(b4))
}

变量和常量

常量

  • 常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值
  • 常量在定义的时候必须赋值
// 声明单个常量
const pi = 3.1415
const e = 2.7182

// 声明多个常量
const (
    pi = 3.1415
    e = 2.7182
)

// const同时声明多个常量时,如果省略了值则表示和上面一行的值相同 n1=n2=n3=100
const (
    n1 = 100
    n2
    n3
)

变量

在 go 中的类型声明是后置的,变量的声明会用到var关键字,格式为var 变量名 类型名,变量名的命名规则必须遵守标识符的命名规则。

// 声明单个
var intNum int
var str string
var char byte

// 同时声明多个
var numA, numB, numC int
// 或
var (
  name    string
  age     int
  address string
)

// 声明并赋值
var name string = "jack"
// 短变量初始化
name := "jack" // 字符串类型的变量。
// 短变量错误示例(不能对已经初始化的值使用)
var a int
a := 1

匿名变量
用下划线可以表示不需要某一个变量
比如os.Open函数有两个返回值,我们只想要第一个,不想要第二个,可以按照下面这样写

file, _ := os.Open("readme.txt")

输入输出函数

输入函数

Scan: 遇到空格和换行都可以继续扫描

Scanf: 格式化输入,从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中

Scanln:Scanln类似于Scan,但它遇到换行立即停止扫描

// Scan Scanln
package main
import "fmt"
func main() {
	var (
		name      string
		age       int
		isMarried bool
	)
	fmt.Scan(&name, &age, &isMarried) // 输入类型不一致,按默认值
	fmt.Printf("扫描结果 name:%s age:%d married:%t\t", name, age, isMarried)
}
// Scanf
package main
import "fmt"
func main() {
	var (
		name      string
		age       int
		isMarried bool
	)
	fmt.Scanf("%s %d %t", &name, &age, &isMarried)
	fmt.Printf("扫描结果 姓名:%s 年龄:%d 婚否:%t", name, age, isMarried)
}

输出函数

Print:输出到终端,不接受格式化操作,不换行

Println:输出到终端,不接受格式化操作,换行

Printf:输出到终端,只可以打印出格式化的字符串,只可以直接输出字符串类型的变量(不可以输出别的类型)不换行

Sprintf:格式化并返回一个字符串而不带任何输出

直接使用print打印会将参数输出到标准错误中仅做调试 不推荐使用 常用fmt库

// Print Println
package main

import "fmt"

func main() {
	name := "xyh"
	age := 20

	fmt.Print(age)
	fmt.Print(name)

	fmt.Println(name)
	fmt.Println(age)
}
// Printf
package main

import "fmt"

func main() {
	name := "xyh"
	age := 24
	isMarried := false
	salary := 3000.549
	fmt.Printf("姓名:%s 年龄:%d 婚否:%t 薪资:%.2f\n", name, age, isMarried, salary)
	fmt.Printf("姓名:%v 年龄:%v 婚否:%v 薪资:%v\n", name, age, isMarried, salary)
	fmt.Printf("姓名:%#v 年龄:%#v 婚否:%#v 薪资:%#v\n", name, age, isMarried, salary)

	// 整形和浮点型
	fmt.Printf("%b\n", 12)       // 二进制表示:1100
	fmt.Printf("%d\n", 12)       // 十进制表示:12
	fmt.Printf("%o\n", 12)       // 八进制表示:14
	fmt.Printf("%x\n", 12)       // 十六进制表示:c
	fmt.Printf("%X\n", 12)       // 十六进制表示:C
	fmt.Printf("%f\n", 3.1415)   // 有小数点而无指数:3.141500
	fmt.Printf("%.3f\n", 3.1415) // 3.142
	fmt.Printf("%e\n", 1000.25)  // 科学计数法:  1.000250e+03,默认精度为6

	// 设置宽度
	fmt.Printf("学号:%s 姓名:%-20s 平均成绩:%-4d\n", "1001", "alvin", 100)
	fmt.Printf("学号:%s 姓名:%-20s 平均成绩:%-4d\n", "1002", "zuibangdeyuanlaoshi", 98)
	fmt.Printf("学号:%s 姓名:%-20s 平均成绩:%-4d\n", "1003", "x", 78)
}
// Sprintf
package main

import "fmt"

func main() {
	name := "xyh"
	age := 24
	isMarried := false
	salary := 3000.549
	info := fmt.Sprintf("姓名:%s 年龄:%d 婚否:%t 薪资:%.2f\n", name, age, isMarried, salary)
	fmt.Println(info)
}

格式化输出表

在这里插入图片描述

条件控制语句

在 Go 中,条件控制语句总共有三种ifswitchselect。select相对前两者而言比较特殊,本节不会讲解,将会留到并发那一节再做介绍

if-else

// if -- else if -- else
if 布尔表达式 {  // 注意左花括号必须与表达式同行
   /* 在布尔表达式为 true 时执行 */
}else if 布尔表达式 {
    
}else{}
// 举例
func main() {
  score := 90
  var ans string
  if score >= 0 && score < 60 {
    ans = "F"
  } else if score < 70 {
    ans = "D"
  } else if score < 80 {
    ans = "C"
  } else if score < 90 {
    ans = "B"
  } else if score < 100 {
    ans = "A"
  } else if score == 100 {
    ans = "S"
    }else {
        ans = "nil"
    }
  fmt.Println(ans)
}

其他用法举例

// if判断map中是否存在该key
if v, ok := hashtable[target-num]; ok {
    // 如果target-num是hashtable中的一个键,则ok为true,v是对应的值
}

switch

switch语句也是一种多分支的判断语句,语句格式如下:

switch expr {
  case case1:
    statement1
  case case2:
    statement2
  default:
    default statement
}
// 举例
package main

import "fmt"

func main() {
    var x interface{}
    //写法一:
    switch i := x.(type) { // 带初始化语句
    case nil:
        fmt.Printf(" x 的类型 :%T\r\n", i)
    case int:
        fmt.Printf("x 是 int 型")
    case float64:
        fmt.Printf("x 是 float64 型")
    case func(int) float64:
        fmt.Printf("x 是 func(int) 型")
    case bool, string:
        fmt.Printf("x 是 bool 或 string 型")
    default:
        fmt.Printf("未知型")
    }
    //写法二
    var j = 0
    switch j {
    case 0:
    case 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法三
    var k = 0
    switch k {
    case 0:
        println("fallthrough")
        fallthrough
        /*
            Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;
            而如果switch没有表达式,它会匹配true。
            Go里面switch默认相当于每个case最后带有break,
            匹配成功后不会自动向下执行其他case,而是跳出整个switch,
            但是可以使用fallthrough强制执行后面的case代码。
        */
    case 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法三
    var m = 0
    switch m {
    case 0, 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法四
    var n = 0
    switch { //省略条件表达式,可当 if...else if...else
    case n > 0 && n < 10:
        fmt.Println("i > 0 and i < 10")
    case n > 10 && n < 20:
        fmt.Println("i > 10 and i < 20")
    default:
        fmt.Println("def")
    }
}

循环控制

在 Go 中,有仅有一种循环语句:for,Go 抛弃了while语句,for语句可以被当作while来使用

  • for循环有三种格式
for init statement; expression; post statement {
  execute statement
}

for expression {
  execute statement
}

for index, value := range iterable {
  // body
}
// 示例
for i := 0; i <= 20; i++ {
    fmt.Println(i)
}

num := 1
for num < 100 {
    num *= 2
}

func main() {
   sequence := "hello world"
   for index, value := range sequence {
      fmt.Println(index, value)
   }
}

和c语言一样 go语言循环也有break continue用法

值拷贝

// go语言处理变量赋值采用的方法

package main

import "fmt"

func main() {
	// 值拷贝 变量赋值时发生(x将地址空间复制一份 拷贝给y用 x,y对应两块不同的地址空间)
	var x = 10
	var y = x
	fmt.Println(&x, &y)
}

数组(值类型)

  • 数组是定长的数据结构,长度被指定后就不能被改变
  • 数组作为值类型,将数组作为参数传递给函数时,由于 Go 函数是传值传递,所以会将整个数组拷贝
  • 数组在声明是长度只能是一个常量,不能是变量
  1. 一致性:数组只能保存相同数据类型元素,元素的数据类型可以是任何相同的数据类型。
  2. 有序性:数组中的元素是有序的,通过下标访问。
  3. 不可变性:数组一旦初始化,则长度(数组中元素的个数)不可变。
// 属于值类型 有默认值	int-->0  string-->""
var 数组名 [元素数量]元素类型   
//type
[3]int
// 1.先声明再赋值
var names [3]string
names[0] = "张三"
names[1] = "李四"
names[2] = "王五"
fmt.Println(names)   // [张三 李四 王五]

// 2.声明并赋值
var names = [3]string{"张三","李四","王五"}
fmt.Println(names) // [张三 李四 王五]

// 3.[...]不限长度声明并赋值
var names = [...]string{"张三","李四","王五"}
fmt.Println(names,reflect.TypeOf(names))  // [张三 李四 王五] [3]string

// 4.索引设置
var names = [...]string{0:"张三",2:"王五"}
fmt.Println(names) // [张三  王五]

切片(引用类型)

  • 引用类型:不会单独分配内存 直接操作原有数组
  • 切片的底层实现依旧是数组,是引用类型,可以简单理解为是指向底层数组的指针。
  • 引用类型的零值是nil。对于引用类型的变量,我们不光要声明它,还要为它分配内容空间。

切片原理

// 切片的构造根本是对一个具体数组通过切片起始指针,切片长度以及最大容量三个参数确定下来的
type Slice struct {
      Data uintptr   // 指针,指向底层数组中切片指定的开始位置
      Len int        // 长度,即切片的长度
      Cap int        // 最大长度(容量),也就是切片开始位置到数组的最后位置的长度 
}

package main

import "fmt"

func main() {
	var arr = [5]int{10, 11, 12, 13, 14}
	s1 := arr[0:3] // 对数组切片
	s2 := arr[2:5]
	s3 := s2[0:2] // 对切片切片

	fmt.Println(s1) // [10, 11, 12]
	fmt.Println(s2) // [12, 13, 14]
	fmt.Println(s3) // [12, 13]

	// 地址是连续的
	fmt.Printf("%p\n", &arr)
	fmt.Printf("%p\n", &arr[0]) // 相差8个字节
	fmt.Printf("%p\n", &arr[1])
	fmt.Printf("%p\n", &arr[2])
	fmt.Printf("%p\n", &arr[3])
	fmt.Printf("%p\n", &arr[4])
	fmt.Println()

	// 每一个切片都有一块自己的空间地址,分别存储了对于数组的引用地址,长度和容量
	fmt.Printf("%p\n", &s1)    // s1自己的地址
	fmt.Printf("%p\n", &s1[0]) // 数组第一个元素的地址
	fmt.Println(len(s1), cap(s1))
	fmt.Println()

	fmt.Printf("%p\n", &s2)    // s2自己的地址
	fmt.Printf("%p\n", &s2[0]) // 数组下标为2的元素的地址
	fmt.Println(len(s2), cap(s2))
	fmt.Println()

	fmt.Printf("%p\n", &s3) // s3自己的地址
	fmt.Printf("%p\n", &s3[0])
	fmt.Println(len(s3), cap(s3))
}

make函数初始化

向上面那样对一个数组进行切片得到的就是一个切片类型 还有一种方法借助make函数

func make(t Type, size ...IntegerType) Type
  • 返回值是值,不是指针
  • 接收的第一个参数是类型,不定长参数根据传入类型的不同而不同
  • 专用于给切片,映射表,通道分配内存。
// 该切片类型为引用类型 没有分配地址空间 直接引用是错误的
var arr []int  // 如果是 var arr [2]int
arr[0] = 1
fmt.Println(arr) // panic: runtime error: index out of range [0] with length 0

make函数的用法

// make也是用于chan、map以及切片的内存创建,而且它返回的类型就是这三个类型本身
make([]Type, size, cap)
// Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题

示例

package main
import "fmt"
func main() {
	a := make([]int, 2)	 // 不定义cap默认cap==len
	b := make([]int, 2, 10)
	fmt.Println(a, b)			// [0 0] [0 0]	# 分配空间但未赋值 显示默认值 int->0 string->""
	fmt.Println(len(a), len(b))	// 2 2
	fmt.Println(cap(a), cap(b))	// 2 10
}

copy函数拷贝

package main

import "fmt"

func main() {

	nums1 := []int{1, 2, 3, 0, 0, 0} // 长度为m
	nums2 := []int{4, 5, 6}          // 长度为n

	copy(nums1[3:], nums2)
	fmt.Println(nums1) // [1 2 3 4 5 6]
}

append函数

基本用法

// 未开辟地址时追加
package main

import "fmt"

func main() {
		var s []int
		s1 := append(s, 1, 2)
		fmt.Println(s, s1)	// [1 2]

		var arr = []int{11, 22, 33}
		s2 := append(s1, arr...) // 只能追加切片不能追加数组 追加切片需要...打散切片
		fmt.Println(s2)	// [1 2 11 22 33]
}
// 使用make开启空间后

package main
import "fmt"
func main() {
	var x = make([]int, 3, 5)
	x1 := append(x, 10)
	fmt.Println(x1) // [0 0 0 10]
}

切片扩容机制

  • append追加时 容量不够时发生二倍扩容
  • 扩容之后产生新的切片 与原切片没关系 原切片赋值修改不会改变新切片
// go扩容机制

package main
import "fmt"
func main() {
	var emps = make([]string, 3, 5)
	emps[0] = "张三"
	emps[1] = "李四"
	emps[2] = "王五"
	fmt.Println(emps)
	emps2 := append(emps, "rain")
	fmt.Println(emps2)
	emps3 := append(emps2, "eric")
	fmt.Println(emps3)
	// 容量不够时发生二倍扩容
	emps4 := append(emps3, "yuan")
	fmt.Println(emps4) // 此时底层数组已经发生变化

	fmt.Println(len(emps4), cap(emps4)) // 6 10 容量扩大原来的二倍
}

经典面试题

package main
import "fmt"
func main() {
	arr := [4]int{10, 20, 30, 40}
	s1 := arr[0:2] // [10, 20]
	s2 := s1       // [10, 20]
	s3 := append(append(append(s1, 1), 2), 3)	// 产生新的切片s3
	s1[0] = 1000
	fmt.Println(s1)		// [1000 20]
	fmt.Println(s2)		// [1000 20]
	fmt.Println(s3)		// [10 20 1 2 3]
	fmt.Println(arr)	// [1000 20 1 2]  对s1追加的值会替换原有数组
}

开头添加元素

在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

// append接受第一个元素只能为切片
// func append(slice []Type, elems ...Type) []Type
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片

// ...的作用是解包
理解:将切片变成一个个的元素传入新切片 没有改名字也是新切片 插入元素进切片不是替换 直接当成新切片追加元素

任意位置插入元素

每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片

删除元素

要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]


// 思考题
package main
import "fmt"
func main() {
	a := [...]int{1, 2, 3}	
	b := a[:]
	b = append(b[:1], b[2:]...)
	fmt.Println(a)
	fmt.Println(b)
}

切片元素排序

package main

import (
	"fmt"
	"sort"
)

func main() {
	a := []int{10, 2, 3, 100}
	sort.Ints(a)
	fmt.Println(a) // [2 3 10 100]

	b := []string{"melon", "banana", "caomei", "apple"}
	sort.Strings(b)
	fmt.Println(b) // [apple banana caomei melon]

	c := []float64{3.14, 5.25, 1.12, 4, 78}
	sort.Float64s(c)
	fmt.Println(c) // [1.12 3.14 4 5.25 78]

	// 注意:如果是一个数组,需要先转成切片再排序  [:]
	// Reverse修改排序规则为降序
	sort.Sort(sort.Reverse(sort.IntSlice(a)))
	sort.Sort(sort.Reverse(sort.Float64Slice(c)))
	fmt.Println(a, c)
}

clear函数清空切片

package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s)
}

指针(引用类型)

  • 区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

  • 要搞明白Go语言中的指针需要先知道3个概念:指针地址指针类型指针取值

  • 关于指针有两个常用的操作符,一个是取地址符&,另一个是解引用符*。对一个变量进行取地址,会返回对应类型的指针 取址 --> & 取值 --> *

指针拷贝

// 指针之间的拷贝称为引用拷贝
var a int = 1
var b *int = &a
c := b
fmt.Println(&a, b, c)
// 测试引用拷贝和值拷贝
package main

import (
	"fmt"
)

func main() {
	var x = 10
	var y = &x
	var z = *y // 发生的是值拷贝 z不是指针类型
	x = 20
	fmt.Println(&x) // 20
	fmt.Println(y)  // 20
	fmt.Println(&z) // 10
}

双指针

package main
import (
	"fmt"
	"reflect"
)
func main() {
	var a = 100
	var b = &a // 指针类型*int
	var c = &b // 指针类型*int
	**c = 200
	fmt.Println(a)
	fmt.Println(reflect.TypeOf(c))	// c的类型为**int
}

new函数

new返回的是地址

new 和 make 是 Go 语言中用于内存分配的原语。简单来说,new 只分配内存,make 用于初始化 slice、map 和 channel。
make返回的还是引⽤类型本⾝;⽽new返回的是指向类型的指针

func new(Type) *Type
  • 返回值是类型指针
  • 接收参数是类型
  • 专用于给指针分配内存空间
// new函数分配地址空间
func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}

map类型(引用类型)

  • map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
map[KeyType]ValueType

// KeyType:表示键的类型。

// ValueType:表示键对应的值的类型。

初始化

// 第一种方法 借助make函数
info := make(map[string]string)	// map是引用类型 必须先使用make开辟地址空间
info["name"] = "xyh"
info["age"] = "23"
fmt.Println(info)

// 第二种方法 声明并赋值
info := map[string]string{"name": "xyh", "age": "23","gender":"male"}
fmt.Println(info) // map[age:18 gender:male name:yuan]

判断某个键是否存在

map 对于不存在的键其返回值是对应类型的零值,并且在访问 map 的时候其实有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在,

value, ok := map[key]

举个例子

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
    if v, ok := scoreMap["张三"]; ok{
        fmt.Println(v)
    } else {
        fmt.Println("查无此人")
    }
}

存值和修改

func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   fmt.Println(mp)
}

// 存值时使用已存在的键会覆盖原有的值
func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   if _, exist := mp["b"]; exist {
      mp["b"] = 3
   }
   fmt.Println(mp)
}

删除键值

删除一个键值对需要用到内置函数delete

func delete(m map[Type]Type1, key Type)

举个例子:

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(mp)
   delete(mp, "a")
   fmt.Println(mp)
}

清空map

在 go1.21 之前,想要清空 map,就只能对每一个 map 的 key 进行 delete
go1.21 更新了 clear 函数,就不用再进行之前的操作了,只需要一个 clear 就可以清空

func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  clear(m)
  fmt.Println(m)
}

map的遍历

Go语言中使用for range遍历map。

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k, v := range scoreMap {
        fmt.Println(k, v)
    }
}

切片与map的互相嵌套

// 元素为map类型的切片
func main() {
    var mapSlice = make([]map[string]string, 3)
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
    fmt.Println("after init")
    // 对切片中的map元素进行初始化
    mapSlice[0] = make(map[string]string, 10)
    mapSlice[0]["name"] = "王五"
    mapSlice[0]["password"] = "123456"
    mapSlice[0]["address"] = "红旗大街"
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
}



// 值为切片类型的map
func main() {
    var sliceMap = make(map[string][]string, 3)
    fmt.Println(sliceMap)
    fmt.Println("after init")
    key := "中国"
    value, ok := sliceMap[key]
    if !ok {
        value = make([]string, 0, 2)
    }
    value = append(value, "北京", "上海")
    sliceMap[key] = value
    fmt.Println(sliceMap)
}

函数

函数的声明格式如下

func 函数名(参数 参数类型) [返回值] {
  函数体
}

参数

Go 中的参数名可以不带名称,一般这种是在接口或函数类型声明时才会用到,不过为了可读性一般还是建议尽量给参数加上名称
Go 中的函数参数是传值传递,即在传递参数时会拷贝实参的值

  • 对于类型相同的参数而言,可以只需要声明一次类型,不过条件是它们必须相邻
func Log(format string, a1, a2 any) {
  ...
}
  • 变长参数可以接收 0 个或多个值,必须声明在参数列表的末尾,最典型的例子就是fmt.Printf函数。
func Printf(format string, a ...any) (n int, err error) {
  return Fprintf(os.Stdout, format, a...)
}

返回值

func Sum(a, b int) int {
   return a + b
}
  • Go 允许函数有多个返回值,此时就需要用括号将返回值围起来。
func Div(a, b float64) (float64, error) {
  if a == 0 {
    return math.NaN(), errors.New("0不能作为被除数")
  }
  return a / b, nil
}
  • Go 也支持具名返回值,不能与参数名重复,使用具名返回值时,return关键字可以不需要指定返回哪些值。
func Sum(a, b int) (ans int) {
  ans = a + b
  return
}

注意:不管具名返回值如何声明,永远都是以return关键字后的值为最高优先级。

func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
    // c,d将不会被返回
  return a + b, a * b
}

匿名函数

匿名函数就是没有签名的函数,例如下面的函数func(a, b int) int,它没有名称,所以我们只能在它的函数体后紧跟括号来进行调用。

func main() {
   func(a, b int) int {
      return a + b
   }(1, 2)
}

延迟调用

defer关键字可以使得一个函数延迟一段时间调用,在函数返回之前这些 defer 描述的函数最后都会被逐个执行

func main() {
  Do()
}

func Do() {
  defer func() {
    fmt.Println("1")
  }()
  fmt.Println("2")
}
// 输出
2
1

当有多个 defer 描述的函数时,就会像栈一样先进后出的顺序执行。

func main() {
  fmt.Println(0)
  Do()
}

func Do() {
  defer fmt.Println(1)
  fmt.Println(2)
  defer fmt.Println(3)
  defer fmt.Println(4)
  fmt.Println(5)
}
// 输出
0
2
5
4
3
1

结构体

结构体可以存储一组不同类型的数据,是一种复合类型。Go 抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go 并非是一个传统 OOP 的语言,但是 Go 依旧有着 OOP 的影子,通过结构体和方法也可以模拟出一个类

声明

type Person struct {
   name string
   age int
}

// 对于一些类型相同的相邻字段,可以不需要重复声明类型
type Rectangle struct {
  height, width, area int
  color               string
}

实例化

programmer := Programmer{
   Name:     "jack",
   Age:      19,
   Job:      "coder",
   Language: []string{"Go", "C++"},
}

可以编写一个函数来实例化结构体,类似构造函数

type Person struct {
  Name    string
  Age     int
  Address string
  Salary  float64
}

func NewPerson(name string, age int, address string, salary float64) *Person {
  return &Person{Name: name, Age: age, Address: address, Salary: salary}
}

组合

type Person struct {
   name string
   age  int
}

type Student struct {
   p      Person
   school string
}

type Employee struct {
   p   Person
   job string
}

student := Student{
   p:      Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.p.name)


// 也支持匿名组合形式 例如
type Student struct {
  Person
  school string
}
// 匿名字段的名称默认为类型名,调用者可以直接访问该类型的字段和方法

结构体指针

对于结构体指针而言,不需要解引用就可以直接访问结构体的内容

p := &Person{
   name: "jack",
   age:  18,
}
fmt.Println(p.age,p.name)

在编译的时候会转换为(*p).name ,(*p).age,其实还是需要解引用,不过在编码的时候可以省去,算是一种语法糖

结构体内存布局

type test struct {
    a int8
    b int8
    c int8
    d int8
}
n := test{
    1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)

// 输出 相差一个字节
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063

标签

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

    `key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

//Student 学生
type Student struct {
    ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
    Gender string //json序列化是默认使用字段名作为key
    name   string //私有不能被json包访问
}

func main() {
    s1 := Student{
        ID:     1,
        Gender: "女",
        name:   "pprof",
    }
    data, err := json.Marshal(s1)
    if err != nil {
        fmt.Println("json marshal failed!")
        return
    }
    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
}

方法

方法与函数的区别在于,方法拥有接收者,而函数没有,且只有自定义类型能够拥有方法。先来看一个例子。

type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())

先声明了一个类型IntSlice,其底层类型为[]int,再声明了三个方法Get,Set和Len,方法的长相与函数并无太大的区别,只是多了一小段(i IntSlice) 。i就是接收者,IntSlice就是接收者的类型,接收者就类似于其他语言中的this或self,只不过在 Go 中需要显示的指明。

接收者

接收者也分两种类型,值接收者和指针接收者
函数的参数传递过程中,是值拷贝的,如果传递的是一个整型,那就拷贝这个整型,如果是一个切片,那就拷贝这个切片,但如果是一个指针,就只需要拷贝这个指针,显然传递一个指针比起传递一个切片所消耗的资源更小,接收者也不例外,值接收者和指针接收者也是同样的道理。在大多数情况下,都推荐使用指针接收者

值接收者

接收者是值接收者时,可以简单的看成一个形参,而修改一个形参的值,并不会对方法外的值造成任何影响

type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // 修改了,但是不会造成任何影响
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

上述代码运行过后,会发现myInt的值依旧是 1,并没有被修改成 2

指针接收者

现在的接收者就是一个指针接收者,虽然myInt是一个值类型,在通过值类型调用指针接收者的方法时,Go 会将其解释为(&myint).Set(2)。所以方法的接收者为指针时,不管调用者是不是指针,都可以修改内部的值。

type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云Apprentice

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值