Go语言入门

Go语言入门

1. Go语言入门

1.1 helloworld示例

一个基本的helloworld示例程序如下.

go
// 这是该文件所在的包名,在同目录下,只允许有一个package语句,强制规定.
package main

// 引入fmt标准库,用于格式化I/O
import "fmt"
// 函数以func声明,后接函数名,main为程序
func main() {
    // 类似于java中的System.out.println();,同样的带有输出换行功能.
    // 在语句后,不再需要添加分号.
    fmt.Println("Hello, World!")
}

关于这个包名,真是个大坑

  • 此外,go.mod必须要放在根目录中

    可以直接使用go run main.go运行程序或使用go build构建程序.

  • main.go,一般放在根目录中,如果是多入口的程序,会放在/cmd/app下

    这种东西一般应用于一个项目会编译出多个不同的可执行文件时,如一个web服务程序,一个处理定时任务的worker程序等.

  • 需要注意,在同一级目录下,只能有一个package声明.

    同一个目录下的所有.go文件,会被视为同一个包

    在同一个目录下同时出现package A和package B,会直接报错.

    例外: go 语言允许在同一个目录下存在一个包及其对应的测试包,如

    package user和package user_test

  • 强烈建议包名与目录名相同! 原因是

    若在子目录domain中,有一个包名为package pkg2的文件,那么

    在引入时,需要写 import “GoLearn/domain”,但是在使用时,需要pkg2.xxx,这很容易导致混乱,因为包名与目录名不一致,根本不知道domain里面会有一个pkg2.

    另外,go语言官方建议 包名应全小写,单数形式.

  • 在自己写的程序中,引用自己写的模块,需要使用 <模块名>/包名 的方式引入,具体使用方法见 1.9 函数作用域

    无论当前文件位于项目的哪个层级,要引用项目内的其它包,都必须以 根模块名(项目名)作为绝对路径的起点.

    示例

    txt
    GoLearn/		(根目录, go.mod 所在处)
    ├── go.mod		(内容: module GoLearn)
    ├── main.go		(package main)
    └── domain/
    └── user.go		(package domain)
    python
    package main
    
    // 必须带上 GoLearn 这个模块名前缀
    import "GoLearn/domain"
    
    func main() {
    	// 调用时使用包名 domain
    	domain.NewUser()
    }

2 语法

2.1 数据类型

变量名使用驼峰命名法命名

变量作用域的控制是通过首字母大小写控制的,而非使用public / private 等关键字.

大写字母: 全局变量.

小写字母: 局部变量,限于本包

实际上,该规则同时适用于函数

go
var PublicVar int      // 对外可见
var privateVar int     // 仅包内可见

func PublicFunc() {}   // 对外可见
func privateFunc() {}  // 仅包内可见

在go语言中,int8,int16等,可以省略为int,此时,go会根据计算机架构,自动选择整型.

数字类型

go
package main

import "fmt"

func main() {
	// 整数类型,其未指定长度时,默认位数跟随平台.
	// 支持uint无符号数
	// 支持int8,int16,int32,int64等指定长度的类型,依次类推,uint同样支持.
	var intVar int = 1
	// 要注意这里,并非是float类型,而是float64类型,
	var floatVar float64 = 1.01

	// 其它数字类型
	// byte: 类似于uint8
	// rune: 类似于int32
	// 虚数类型
	var complex64Var complex64 = 1 + 2i   // 虚数类型,占用8字节
	var complex128Var complex128 = 1 + 2i // 虚数类型,占用16字节,若未指定类型,则默认为complex128
	// 虚数类型的另外一种声明方式,complex(<实部>,<虚部>)创建,支持complex64,complex128.
	var complex64Var2 complex64 = complex(1, 2)
	fmt.Println(complex64Var2)

	fmt.Printf("基本类型: intVar = %d, floatVar = %.2f ", intVar, floatVar)
	fmt.Printf("complex64Var = %v, complex128Var = %v\n", complex64Var, complex128Var)

	// 4. 类型转换 (Go不支持隐式转换)
	var castIntToFloat float64 = float64(intVar) + floatVar
	fmt.Printf("类型转换: castIntToFloat=%.2f\n", castIntToFloat)
}

虚数类型相关知识点

go
package main

import (
	"fmt"
)

func main() {
	// 声明方式
	// ===== 方式1:字面量直接赋值(最常用)=====
	var c1 complex128 = 1 + 2i // 默认就是 complex128
	var c2 complex64 = 3 + 4i  // 需显式指定 complex64

	// ===== 方式2:complex(<实部,虚部>) 内置函数构造 =====
	var c3 complex128 = complex(1, 2) // 实部=1, 虚部=2 → 1+2i
	var c4 complex64 = complex(3, 4)  // 3+4i

	// ===== 方式3:短变量声明(自动推导为 complex128)=====
	c5 := 5 + 6i        // c5 的类型是 complex128
	c6 := complex(7, 8) // c6 的类型也是 complex128

	// ===== 方式4:只有虚部的写法 =====
	var singlePartComplex1 complex128 = 2i    // 等价于 0 + 2i
	var singlePartComplex2 complex128 = 3.14i // 实部为 0

	// ===== 方式5:零值 =====
	var zeroComplex complex128 // 零值为 (0+0i)

	fmt.Println(c1, c2, c3, c4, c5, c6, singlePartComplex1, singlePartComplex2, zeroComplex)

	// 常用方法
	// 提取实部或提取虚部
	fmt.Println("实部:", real(c1), "虚部:", imag(c1))

	// 数学运算

	a := 1 + 2i
	b := 3 + 4i

	// 加法: (1+3) + (2+4)i = 4+6i
	fmt.Println("a + b =", a+b) // (4+6i)

	// 减法: (1-3) + (2-4)i = -2-2i
	fmt.Println("a - b =", a-b) // (-2-2i)

	// 乘法: (1*3 - 2*4) + (1*4 + 2*3)i = -5+10i
	fmt.Println("a * b =", a*b) // (-5+10i)

	// 除法: (a*b̄) / |b|²
	fmt.Println("a / b =", a/b) // (0.44+0.08i)
	/* 了解即可,这是虚数的四则运算规则.
	设 a = x₁ + y₁i,  b = x₂ + y₂i

	加法: a + b = (x₁+x₂) + (y₁+y₂)i
	减法: a - b = (x₁-x₂) + (y₁-y₂)i
	乘法: a * b = (x₁x₂ - y₁y₂) + (x₁y₂ + x₂y₁)i
	除法: a / b = [(x₁x₂ + y₁y₂) + (y₁x₂ - x₁y₂)i] / (x₂² + y₂²)
	*/

	complexCompare1 := 1 + 2i
	complexCompare2 := 1 + 2i
	complexCompare3 := 1 + 3i

	// == 和 != 可以比较(实部和虚部分别相等)
	fmt.Println(complexCompare1 == complexCompare2) // true
	fmt.Println(complexCompare1 != complexCompare3) // true
	// 如果实部或虚部包含NaN,则==会固定返回false,因为NaN != NaN

	// 不支持 < > <= >= 比较(复数没有大小概念)
	// fmt.Println(a1 < b1) // 编译错误!

	// 同样的,complex是不支持隐式转换的,必须显式转换
	var c64 complex64 = 1 + 2i
	var c128 complex128

	c128 = complex128(c64) // complex64 → complex128(安全)
	c64 = complex64(c128)  // complex128 → complex64(可能丢失精度)
}

精度丢失问题及其解决方案

go
package main

import "fmt"

// 解决方案
// 1. 引入高精度decimal包
import "github.com/shopspring/decimal"

func main() {
	var floatVar1 float64 = 0.1
	var floatVar2 float64 = 0.2
	var floatVar3 float64 = floatVar1 + floatVar2 // 在这里出现了精度丢失问题.
	fmt.Println(floatVar3)
	// 2. 使用decimal.Decimal类型进行计算
	var decimalVar1 decimal.Decimal = decimal.NewFromFloat(0.1)
	var decimalVar2 decimal.Decimal = decimal.NewFromFloat(0.2)
	// 3. 使用类似这样的方法进行数学运算避免精度丢失问题.
	var decimalVar3 decimal.Decimal = decimalVar1.Add(decimalVar2)
	fmt.Println(decimalVar3)
}

布尔类型(bool)

go
package main

import "fmt"

func main() {
	// 声明布尔变量
	var boolVar bool = true

	fmt.Println(boolVar)

	if boolVar {
		fmt.Println("boolVar是真,1")
	} else {
		fmt.Println("boolVar是假")
	}
	if boolVar == true {
		fmt.Print("boolVar是真,2")
	}
	// 逻辑运算

	var x bool = true
	var y bool = false
	fmt.Println("x && y: ", x && y)
	fmt.Println("x || y: ", x || y)
	fmt.Println("!x :", !x)
}

其它派生类型

指针类型 *

使用 & 取出地址

使用 *<类型> 声明一个指针类型的变量

go
package main

import "fmt"

func main() {
	var intVar int = 10
	// 声明一个指针,指向intVar
	var ptrVar *int = &intVar
	fmt.Println("intVar的值@地址: ", intVar, "@", ptrVar)
	fmt.Println("通过指针访问intVar,*ptrVar: ", *ptrVar)

	var nullPtrVar *int
	fmt.Print("定义了空指针nullPtrVar: ", nullPtrVar)
	// 对空指针解引用会产生NPE.

	// 定义一个结构体
	type Person struct {
		name string
		age  int
	}
	// 声明一个标准变量
	var person1 Person = Person{
		name: "Alice",
		age:  18,
	}
	// 声明一个结构体指针
	var ptrPerson1 *Person = &Person{
		name: "Alice",
		age:  18,
	}
	fmt.Println("person1姓名: ", person1.name)

	fmt.Println("ptrPerson1姓名: ", (*ptrPerson1).name) // 这是标准写法,需要先解引用指针去访问结构体,再访问属性
	fmt.Println("ptrPerson1姓名: ", ptrPerson1.name)    // 但是呢,在Go语言中,提供了语法糖,可以不必解引用就可以直接访问属性.
	
}

字符串类型

声明方式

go
	// 声明字符串,共四种方式.
	var strVar string = "Hello, \tKulve\n"  // 使用双引号创建的字符串,是支持转义字符的.
	var strVar2 string = `Hello, \tkulve\n` // 使用反引号创建的字符串,是不支持转义字符的.
	var formatStrVar string = fmt.Sprintf("Hello, %s", "Kulve")
	var stringBuilder strings.Builder
	stringBuilder.WriteString("Hello, ")
	stringBuilder.WriteString("Kulve")

	fmt.Println("strVar: ", strVar)
	fmt.Println("strVar2: ", strVar2)
	fmt.Println("formatStrVar: ", formatStrVar)
	fmt.Println("stringBuilder: ", stringBuilder.String())

常用属性

go
	// 常用的属性
	strVar = "Hello, Kulve"
	strVar2 = "你好, 圐圙" // 其它字符还是一字节
	// 获取字节长度,中文UTF8每字符占用3字节
	fmt.Printf("len(strVar): %d \n", len(strVar))
	fmt.Printf("len(strVar2): %d \n", len(strVar2))
	// 获取字符数量
	fmt.Printf("utf8.RuneCountInString(strVar): %d \n", utf8.RuneCountInString(strVar))
	fmt.Printf("utf8.RuneCountInString(strVar2): %d \n", utf8.RuneCountInString(strVar2))
	// 使用索引访问字符串,返回的是byte而不是字符
	fmt.Printf("strVar[0]: %d \n", strVar[0])
	// 遍历字符串
	for index, char := range strVar2 {
		fmt.Printf("%d,%c \t", index, char)
	}
	fmt.Printf("\n")

常用方法

go
	// 常用的方法(strings包)
	strVar = "Hello, Kulve"
	fmt.Println("包含Kulve: ", strings.Contains(strVar, "Kulve"))
	fmt.Println("以Hello开头: ", strings.HasPrefix(strVar, "Hello"))
	fmt.Println("以Kulve结尾: ", strings.HasSuffix(strVar, "Kulve"))
	fmt.Println("返回,首次出现的索引: ", strings.Index(strVar, ","))
	fmt.Println("返回e最后一次出现的索引: ", strings.LastIndex(strVar, "e"))
	fmt.Println("字符e出现次数: ", strings.Count(strVar, "e"))
	fmt.Println("转换大写: ", strings.ToUpper(strVar))
	fmt.Println("转换小写: ", strings.ToLower(strVar))
	fmt.Println("移除两边空白字符: ", strings.TrimSpace("  Hello, Kulve  "))
	fmt.Println("移除两端的指定字符: ", strings.Trim("||Hello, Kulve||", "|"))
	fmt.Println("只移除左边的指定字符: ", strings.TrimLeft("||Hello, Kulve||", "|"))
	fmt.Println("只移除右边的指定字符: ", strings.TrimRight("||Hello, Kulve||", "|"))
	fmt.Println("替换第1个找到的kulve为圐圙: ", strings.Replace(strVar, "Kulve", "圐圙", 1))
	fmt.Println("替换所有的kulve为圐圙: ", strings.ReplaceAll(strVar, "Kulve", "圐圙"))
	fmt.Println("分割字符串: ", strings.Split("Hello, Kulve, 圐圙", ",")) // 返回值是一个列表/数组
	fmt.Println("分割字符串为指定长度的列表: ", strings.SplitN("Hello, Kulve, 圐圙", ",", 2))
	fmt.Println("将指定的字符串列表以指定分隔符连接为字符串: ", strings.Join([]string{"Hello", "Kulve", "圐圙"}, "#"))
	fmt.Println("重复字符串: ", strings.Repeat("Hello, Kulve. \n", 3))

类型转换

go
package main

import "fmt"

func main() {
	var strVar string = "Hello"
	// 字符串和切片类型转换
	// string -> []byte
	var byteSlice []byte = []byte(strVar)
	fmt.Println(byteSlice)

	// string -> []rune,注意,这样才是正确的处理UTF8的正确方式.
	var runeSlice []rune = []rune(strVar)
	fmt.Println(runeSlice)

	// []byte -> string
	var strResult1 string = string(byteSlice)
	fmt.Println(strResult1)
	// []rune -> string
	var strResult2 string = string(runeSlice)
	fmt.Println(strResult2)
}

类型转换2

go
package main

import (
	"fmt"
	"strconv"
)

func main() {

	var strNum string = "12345"

	// string -> int 方式一: strconv.AtoI(<字符串>)
	var intNum1, err1 = strconv.Atoi(strNum)

	if err1 != nil {
		fmt.Println("转换失败: ", err1)
	} else {
		fmt.Println("strconv.Atoi(strNum): ", intNum1)
	}

	// string -> int 方式二: strconv.ParseInt(<字符串>,<进制>,<位数>)
	// 这种方式更灵活
	var intNum2, err2 = strconv.ParseInt(strNum, 10, 64)
	if err2 != nil {
		fmt.Println("转换失败: ", err2)
	} else {
		fmt.Println("strconv.ParseInt(strNum,10,64): ", intNum2)
	}

	// int -> string 方式一: strconv.Itoa(<整数>)
	var intVar int = 100
	var strResult = strconv.Itoa(intVar)
	fmt.Println("strconv.Itoa(intVar): ", strResult)

	// int -> string 方式二: strconv.FormatInt(<int64值>,<进制>)
	strResult = strconv.FormatInt(int64(intVar), 10)
	fmt.Println("strconv.FormatInt(int64(intVar), 10): ", strResult)

	// int -> string 方式三: strconv
	strResult = fmt.Sprintf("%d", intVar)
	fmt.Println("fmt.Sprintf(\"%d\", intVar): ", strResult)

	var floatStr string = "123.456"
	// string -> float
	var floatNum, err3 = strconv.ParseFloat(floatStr, 64)
	if err3 != nil {
		fmt.Println("转换失败: ", err3)
	} else {
		fmt.Println("strconv.ParseFloat(floatStr,64): ", floatNum)
	}

	// float -> string 方式一: strconv.FormatFloat(<float64>,'f',)
	var floatVar float64 = 100.01
	// 形参解释:
	// fmt: 'f' 普通十进制小数,无指数
	// fmt: 'e' 科学计数法,小写e
	// fmt: 'E' 科学计数法,大写E
	// fmt: 'g' 根据情况自动选择f或e
	// fmt: 'G' 根据情况自动选择f或E

	// prec: <精度>, 保留几位小数,若不足则补0. 若为-1,则为最小必要位数,不再强制补0.
	// bitSize: <32/64>, 指定float的长度.
	strResult = strconv.FormatFloat(floatVar, 'f', 2, 64)
	fmt.Println("strconv.FormatFloat(floatVar, 'f', 2, 64): ", strResult)

	strResult = strconv.FormatFloat(floatVar, 'e', -1, 64)
	fmt.Println("strconv.FormatFloat(floatVar, 'e', -1, 64): ", strResult)

}

数组类型 []

声明

go
package main

import "fmt"

func main() {
	// 1. 声明时指定长度,会自动初始化为默认值.
	var arrVar1 [5]string
	fmt.Println(arrVar1)

	// 2. 在声明时指定长度并手动初始化.
	var arrVar2 = [3]string{"Hello, ", "Kulve", "."}
	fmt.Println(arrVar2)

	// 3. 在声明时由编译器自动确定长度.
	var arrVar3 = [...]string{"Hello, ", "Kulve", "."}
	fmt.Println(arrVar3)

	// 4. 邪门玩法: 在声明时指定下标,其它元素初始化为默认值.
	var arrVar4 = [5]string{1: "Hello, ", 3: "Kulve", 4: "."}
	fmt.Println(arrVar4)
	
}

遍历与数组的传递方式

go
package main

import "fmt"

func main() {
	// 1. 遍历数组
	var arrVar = [...]int{1, 2, 3, 4, 2, 4, 5}
	for index, value := range arrVar {
		fmt.Printf("arrVar[%d]: %d\n", index, value)
	}
	// 2. 获取数组的长度和容量,在数组中,长度与容量是相等的.
	fmt.Printf("长度: %d, 容量: %d\n", len(arrVar), cap(arrVar))

	// 3. 数组是值类型,这与其它大部分语言不同.
	arrCopy := arrVar
	arrCopy[0] = 999
	fmt.Println("原数组: ", arrVar[0])  // 1 (未改变)
	fmt.Println("新数组: ", arrCopy[0]) // 999
}
go
func main() {
	// 声明一个包含指定元素数量的数组
	var arr [5]int = [5]int{1, 2, 3, 4, 5}

	// 简写形式
	arr2 := [3]string{"Go", "Python", "Java"}

	// 让编译器推断长度,将[<长度>]换成[...]就可以
	arr3 := [...]int{10, 20, 30}

	fmt.Println(arr[0])   // 输出: 1
	fmt.Println(len(arr)) // 输出: 5
	fmt.Println(arr2)     // 输出: [Go Python Java]
	fmt.Println(arr3)     // 输出: [10 20 30]
}

结构体 type struct

go
// 声明结构体
// 这个与java中的class不同,它不能存储方法.
type Person struct {
	Name  string
	Age   int
	Email string
}

func main() {
	// 为结构体字段赋值
	// 方式1: 按字段顺序
	p1 := Person{"Alice", 30, "alice@example.com"}

	// 方式2: 指定字段名(推荐,也就是命名方式)
	p2 := Person{
		Name: "Bob",
		Age:  25,
	}

	// 方式3: 先声明再赋值,有点类似于class
	var p3 Person
	p3.Name = "Charlie"

	fmt.Println(p1.Name) // 输出: Alice
	fmt.Println(p2)      // 输出: {Bob 25 }
}

通道 channel

需要注意的是,若缓冲区满了或者创建的是无缓冲区的通道,那么在接收方或发送方无法接收或发送时,程序将会阻塞.

go
func main() {
	// channel: 通道
	// 用于在协程之间通信使用
	// 创建缓冲为 2 的 int 类型 channel
	bufferedChannel := make(chan int, 2)

	// 发送数据到 channel
	bufferedChannel <- 100
	bufferedChannel <- 200

	// 从 channel 接收数据
	// 先进先出,类似一个队列
	x := <-bufferedChannel
	y := <-bufferedChannel

	fmt.Println(x, y) // 输出: 100 200

	// 无缓冲 channel
	unbufferedChannel := make(chan string)
	// 异步发送数据
	go func() {
		unbufferedChannel <- "Hello from goroutine"
	}()
	msg := <-unbufferedChannel
	fmt.Println(msg)
}

函数 func

go
// 示例1: 定义一个函数类型的接口
type AddFunc func(int, int) int

func main() {
    // 函数作为变量
    // 声明函数,并赋值给变量保存起来
    var add AddFunc = func(a, b int) int {
       return a + b
    }
    // 调用时,使用变量名作为函数名使用.
    result := add(3, 5)
    fmt.Println(result) // 输出: 8

    // 示例2: 函数作为参数
    // 参数1: a
    // 参数2: b:int
    // 参数3: op:func(int, int) int,是一个函数类型
    operate := func(a, b int, op func(int, int) int) int {
       return op(a, b)
    }

    // 使用,这里先声明一个函数,作用是将a,b相乘,并返回结果.
    multiply := func(a, b int) int { return a * b }
    // 使用operate函数,传入值a,b,以及函数multiply
    fmt.Println(operate(4, 5, multiply)) // 输出: 20

    // 示例3: 函数作为返回值
    // 函数名: makeMultiplier
    // 接收参数: factor int
    // 函数返回值类型: func(int) int
    makeMultiplier := func(factor int) func(int) int {
       // 在这里,返回了一个函数,用于计算x*factor.
       return func(x int) int {
          return x * factor
       }
    }

    double := makeMultiplier(2)
    fmt.Println(double(7)) // 输出: 14
}

切片类型 slice

go
package main

import "fmt"

func main() {
	// 1. 通过数组创建切片
	var arrVar = [...]int{1, 2, 3, 4, 2, 4, 5}
	var sliceVar = arrVar[1:4]

	fmt.Println(sliceVar)

	// 2. 直接创建切片
	var sliceVar2 = []int{10, 20, 30}
	fmt.Println(sliceVar2)

	// 3. 使用make创建切片: make([]类型,<长度>,<容量>)
	var sliceVar3 = make([]int, 10, 20)
	sliceVar3[1] = 10
	fmt.Println(sliceVar3)
	// 相关方法
	// 追加元素
	sliceVar3 = append(sliceVar3, 1, 2)
	fmt.Println(sliceVar3) // 输出: [a b c d e]

	// 切片复制
	s4 := make([]int, len(sliceVar3))
	copy(s4, sliceVar3)
}

一个示例: 在切片中搜索指定内容

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	// 示例: 在切片中搜索指定内容.方式一: 遍历切片
	var sliceVar = []string{"Hello", ", ", "Kulve", "."}
	var targetString string = "Kulve"
	var found bool = false
	var index int = 0

	for i, v := range sliceVar {
		if v == targetString {
			found = true
			index = i
			break
		}
	}
	if found {
		fmt.Printf("搜索%s,下标%d\n", targetString, index)
	} else {
		fmt.Printf("搜索%s,不存在\n", targetString)
	}

	// 在切片中搜索指定内容.方式二: 使用Go1.21+提供的slices包.
	found = slices.Contains(sliceVar, targetString)
	if found {
		index = slices.Index(sliceVar, targetString)
		fmt.Printf("搜索%s,下标%d\n", targetString, index)
	} else {
		fmt.Printf("搜索%s,不存在\n", targetString)
	}
}

常用方法

go
package main

import "fmt"

func main() {
	// 在切片中添加一个元素
	var sliceVar = []string{"Hello", ", ", "Kulve", "."}
	sliceVar = append(sliceVar, "圐圙")
	fmt.Println(sliceVar)
	// 在切片中添加多个元素
	sliceVar = append(sliceVar, "村", "委", "会")
	fmt.Println(sliceVar)
	// 在切片中添加另外一个切片,需要使用...对切片解包
	var sliceVar2 = []string{"大", "队", "."}
	sliceVar = append(sliceVar, sliceVar2...)
	fmt.Println(sliceVar)
	// 切片没有内置的删除方法,删除只能通过对切片进行切片后拼接删除.
}

切片的传递

go
package main

import "fmt"

func main() {
	// 在切片中添加一个元素
	var sliceVar1 = []string{"Hello", ", ", "Kulve", "."}
	var sliceVar2 = sliceVar1
	sliceVar1 = append(sliceVar1, "圐圙") // 当注释掉这行后,下一行的修改将会被传递到sliceVar2上.
	// 其原因是因为切片扩容,导致地址改变,但sliceVar2仍然持有原地址
	sliceVar1[1] = "西西米托"

	fmt.Println("sliceVar1: ", sliceVar1)
	fmt.Println("sliceVar2: ", sliceVar2)

}

接口类型

go
// 定义接口
type Animal interface {
	Speak() string
	Move() string
}

// Dog 实现 Animal 接口
// Go语言的接口,是隐式实现的,不需要显式声明实现的接口
// 只需要同时实现接口内的所有函数,就可以将实现赋值到接口类型的对象上,见下面的ShowAnimal函数,传入值是一个animal接口类型对象.
type Dog struct {
	Name string
}

func (d Dog) Speak() string {
	return "汪汪!"
}

func (d Dog) Move() string {
	return "四条腿跑"
}

// Cat 实现 Animal 接口
type Cat struct {
	Name string
}

func (c Cat) Speak() string {
	return "喵喵~"
}

func (c Cat) Move() string {
	return "悄无声息地走"
}

// 多态函数
func ShowAnimal(a Animal) {
	fmt.Printf("%s, %s\n", a.Speak(), a.Move())
}

func main() {
	// 声明两个实体类
	dog := Dog{"旺财"}
	cat := Cat{"咪咪"}
	// 这里调用了多态函数,传入了两个实体类,由多态函数自动处理.
	ShowAnimal(dog) // 输出: 汪汪!, 四条腿跑
	ShowAnimal(cat) // 输出: 喵喵~, 悄无声息地走

	// 空接口: 可以存储任意类型,限一个,如基本类型,函数类型,切片类型,结构体等.
	// 在Go中,所有的方法都实现了空接口
	var anything interface{} = "hello"
	anything = 42
	anything = 3.14
	fmt.Println(anything)
}

map类型

这是一个非常重要的类型

基本使用

go
func main() {
	// 方式1: make 创建
	scores := make(map[string]int)
	scores["Alice"] = 95
	scores["Bob"] = 87

	// 方式2: 字面量
	ages := map[string]int{
		"Tom":   20,
		"Jerry": 18,
	}

	// 访问
	fmt.Println(scores["Alice"]) // 输出: 95

	// 判断键是否存在
	if score, ok := scores["Charlie"]; ok {
		fmt.Println("存在:", score)
	} else {
		fmt.Println("Charlie 不存在")
	}

	// 删除
	delete(ages, "Tom")

	// 遍历
	for name, score := range scores {
		fmt.Printf("%s: %d\n", name, score)
	}
}

新增Map元素的几种方式

没有单独的新增函数,若要新增函数,需要在循环中实现,利用map中键不存在,会自动新增的特性实现添加.

go
func main() {
	// 创建一个空的map
	scores := make(map[string]int)

	// 新增元素

	// 方式1: 直接赋值新增
	// 当键不存在时,会自动新增元素
	scores["Alice"] = 95
	scores["Bob"] = 87
	fmt.Println("初始map:", scores) // map[Alice:95 Bob:87]

	// 方式2: 在循环中动态新增
	names := []string{"Charlie", "David", "Eve"}
	// i: 值
	// name: 键
	// 循环遍历namesmap,利用键不存在时,自动新增元素的特性,将names中的元素添加到scores中
	// 若已存在,那么值将会被覆盖

	for i, name := range names {
		scores[name] = 90 + i*5 // Charlie:90, David:95, Eve:100
	}
	fmt.Println("新增后:", scores) // map[Alice:95 Bob:87 Charlie:90 David:95 Eve:100]

	// 方式3: 从另一个map复制新增
	// 原理与上一个差不多
	extraScores := map[string]int{
		"Frank": 88,
		"Grace": 92,
	}
	for k, v := range extraScores {
		scores[k] = v // 如果key不存在就是新增
	}
	fmt.Println("合并后:", scores) // map[Alice:95 Bob:87 ... Frank:88 Grace:92]

	// 方式4: 条件新增(只有key不存在时才添加)
	if _, exists := scores["Henry"]; !exists {
		scores["Henry"] = 85
		fmt.Println("新增Henry:", scores["Henry"]) // 85
	}
}

链式运算

Go的链式运算,遵循运算符的运算规则,从左到右.

2.3 变量声明

变量声明方式

go
// 声明方式一: 若活力类型,编译器会自动推断类型
var <变量名> [类型] = <>
// 声明方式二: 此时编译器会自动推断类型
<变量名> := <>
// 声明方式三: 一次性声明多个变量
var <变量名列表> [类型] = <值列表>

2.4 值类型和引用类型

int,float,string,bool类型为值引用类型,即在使用=时,会在内存中复制一份

其它复杂类型为引用类型.

2.5 常量声明

常量推荐使用大驼峰或全大写+下划线

go官方推荐使用大驼峰.

基础声明

go
package main

import "fmt"

func main() {
	const LENGTH int = 10
	const WIDTH int = 5
	var area int
	const a, b, c = 1, false, "str" // 多重赋值

	area = LENGTH * WIDTH
	fmt.Printf("面积为 : %d", area) // 面积为 : 50
	fmt.Println()
	fmt.Println(a, b, c) // 1 false str
}

使用常量组

go
const (
	a = 1
	b
	c
	d
)

func main() {
	fmt.Println(a)
	// b、c、d没有初始化,使用上一行(即a)的值
	fmt.Println(b) // 输出1
	fmt.Println(c) // 输出1
	fmt.Println(d) // 输出1
}

一个特殊的iota 常量计数器

iota是一个特殊常量,用于生成递增的序列

基础使用

go
// iota 从 0 开始,每行加 1
const (
	a = iota   // 0
	b          // 1,将会使用上一行的iota递增序列.
	c          // 2
)

func main() {
	fmt.Printf("a=%d, b=%d, c=%d\n", a, b, c) // a=0, b=1, c=2
}

进阶使用

iota在常量声明时,若在中间有显式赋值的变量时,将会停止使用iota,显式赋值的变量之后声明的新变量,将会使用上一行的值,也就是使用显式赋值的变量的值,等到下次使用显式赋值或使用iota时,变量才会改变值.

实际上,这是go语言的机制,参见常量组的声明

go
package main

import "fmt"

func main() {
	const (
		a = iota // 0
		b        // 1
		c        // 2
		d = "ha" // 独立值,iota += 1,实际值为给定的值而不是iota
		e        // "ha",iota += 1,实际值为上一行的值而不是iota
		f = 100  // 独立值,iota += 1,同样的,实际值为上一行的值而不是iota
		g        // 100,iota += 1,同上.
		h = iota // 7,恢复使用 iota 值,iota经过中间几个变量后,值为7
		i        // 8
	)
	fmt.Printf("a=%d, b=%d, c=%d, d=%s, e=%s, f=%d, g=%d, h=%d, i=%d\n", a, b, c, d, e, f, g, h, i)
}

1.6 运算符

同其它语言

1.7 程序结构

if分支结构

go
if <condition> {
    <if block>
} [else {
    <else block>
}]

switch分支结构

go
switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

select 分支结构

这个结构只能用于通道操作,每个case上的表达式,必须是一个通道操作,要么是发送,要么是接收

不带default子句的select,一般用于同时监听多个通道,只要任意一个通道有数据,那么就可以执行下去.

go
select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码

    // 你可以定义任意数量的 case

  default:
    // 所有通道都没有准备好,执行的代码
}
  • 每个case都是一个通道
  • 所有的channel在执行到select时,都将会被求值,此时,在本次执行select时,值已经被确定,不会再修改.
  • 如果有多个case可以运行,那么select将会随机选出一个执行,其它的不会执行
  • 在找不到可执行的case时,如果有default,那么将执行default,如果没有default,那么将会阻塞,此时将不再对channel或值进行求值,此时即使将通道替换为nil,也不会改变select正在监听的通道内的对象

for 循环结构

go语言中只有for循环,但有三种形式可选,可以用其中一种代替while循环.

go
// 方式一: 类似于其它语言的for
for init; condition; post {}

// 方式二: 类似于其它语言的while
for condition {}

// 方式三: 类似于其它语言的for(;;)
for {}

示例

go
func main() {
	numbers := [6]int{1, 2, 3, 5}
	// 这里的key, value 类似于js的解构赋值,可以同时取出键和值.
	for key, value := range numbers {
		fmt.Printf("第 %d 位 x 的值 = %d\n", key, value)
	}
}
go
// 控制循环语句
break: 中断本层循环,跳出本层循环
continue: 中断本次循环,重新开始本层循环.
go
// 无条件跳转语句,非常不建议使用该语句
goto <label>
...
label: <标签名>

1.8 函数

函数使用大驼峰命名

大驼峰命名的函数会导出,也就是全局可用.

小驼峰命名的函数不会导出,也就是仅限于本包.

结构

go
func <函数名>( [参数列表] ) [返回值类型] {
   函数体
}

函数是允许返回多个值的,如

go
func swap(x, y string) (string, string) {
   return y, x
}

1.9 函数作用域

  • 在函数内部声明的变量,包括形式参数与返回值,都是局部变量
  • 在函数外声明的变量,可以在本包内使用
  • 若使用导出函数,那么可以在外部包使用

如何确定是否导出?

  • 导出: 首字母大写,如 var ExportedVar int = 100

  • 不导出: 首字母小写,如 var unexportedVar int = 100;

使用外部包的变量,需要注意的就是文章刚开头说的,关于包,项目文件的组织安排的问题.

go
// GoLearn/main.go
package main

import (
	"GoLearn/domain" // 这里的导入,需要从项目的模块名开始写.
	"fmt"
)

func main() {
	// 可以访问导出的内容
	fmt.Println(domain.ExportedVar)    // 正常访问
	fmt.Println(domain.ExportedFunc()) // 正常访问

	obj := domain.ExportedStruct{
		Name: "张三", // 大写字段可访问
		// age:  25,      // 编译错误!age 未导出
	}

	// 以下全部编译错误
	// pack2.unexportedVar     // 未导出
	// pack2.unexportedFunc()  // 未导出

	fmt.Println(obj.Name)
	fmt.Println(obj.Address)
}
go
// GoLearn/domain/pack2.go
package domain

// 导出(大写开头)—— 外部包可用
var ExportedVar = "我是导出的变量"

func ExportedFunc() string { return "导出函数" }

type ExportedStruct struct {
	Name    string // 导出字段
	age     int    // 未导出字段(包外不可见)
	Address string // 导出字段
}

// 未导出(小写开头)—— 仅限包内
var unexportedVar = "私有变量"

func unexportedFunc() string { return "私有函数" }

Go 拓展使用

2.1 指针 pointer

啊,指针,危险又美丽的东西

基本声明与使用

go
func main() {
	var a int
	var ptr *int
	var pptr **int // Go支持指向指针的指针,使用**就可以了

	a = 3000

	// 指针 ptr 地址
	ptr = &a

	// 指向指针 ptr 地址
	pptr = &ptr

	// 获取 pptr 的值
	fmt.Printf("变量 a = %d\n", a)
	fmt.Printf("指针变量 *ptr = %d\n", *ptr)
	fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}

指针在数组中的应用

go
const max = 3

func main() {
	number := [max]int{5, 6, 7}
	var ptrs [max]*int // 声明int类型的指针数组
	// 将number数组的值的地址赋给ptrs
    // &number: 指向了number数组,在这块内存区域中,使用for遍历
    // 注意,这里使用number也是可以的,number与&number的区别在于,number是将数组复制了一份,页&number是指向数组的一个指针,使用数组复制时,是有性能开销的,但适用于单线程环境或者多线程不修改数组的环境.
    // i: 键
    // x: 值
	for i, x := range &number {
        // 取出x的内存地址,赋给指针数组的对应元素
		ptrs[i] = &x
	}
    // 将
	for i, x := range ptrs {
		fmt.Printf("指针数组:索引:%d 值:%d 值的内存地址:%d\n", i, *&*x, x)
	}
}

2.2 结构体 struct

基本声明与使用

go
// 定义
type <结构体名> struct {
   <成员名1> <类型>
   <成员名2> <类型>
   ...
}

// 声明
var <变量名> <结构体类型名>

// 使用
<变量名>.<成员名>

结构体同样支持指针.

2.3 切片 slice

基本声明与使用

切片与

go
func main() {
	// 声明一个未指定大小的数组,这就是一个切片了
	// 声明方法一: 直接声明一个空切片
	var identfier []int
	fmt.Println(identfier)
	// 声明方法二: 使用make()函数
	// make(<类型>,<长度>[,<最大容量>])
	//
	identfier2 := make([]int, 0)
	fmt.Println(identfier2)

	var numbers = make([]int, 3, 5)
	fmt.Println(numbers)

	// 切片的初始化
	var initArray = []int{1, 2, 3} // 实际上,这里省略了[]int类型,initSlice1是由编译器自动推导的,最最完整的定义是这样的 var initSlice1 []int = []int{1, 2, 3}
	// 切片的截取/使用
	// 可以通过设置下限及上限来设置截取切片[lower:upper]
	// 数组名[起始索引:结束索引],包含起始索引,不包含结束索引
	// 若省略结束索引,则到数组末尾
	// 若省略起始索引,则从0开始
	var initSlice1 = initArray[0:2]

	fmt.Println(initArray)
	fmt.Println(initSlice1)
	// 这一行的意思是,将initSlice切片,增加了一个元素,值为4.
	// append(<切片>[,<元素列表>])
	initSlice1 = append(initSlice1, 0)
	fmt.Println(initSlice1)
	// 获取切片长度
	initSlice1Len := len(initSlice1)
	fmt.Println(initSlice1Len)
	// 获取切片容量
	initSlice1Cap := cap(initSlice1)
	fmt.Println(initSlice1Cap)
	// 复制一份切片
	// copy(<目标切片>,<源切片>)
	var initSlice2 = make([]int, 66)
	copy(initSlice2, initSlice1)
	fmt.Println(initSlice2)
}
  • 切片的数据是存放在原数组中的,多个切片可以共享同一数组
  • 但,若切片超出的容量,那么将会复制一份原数据,再将新数据添加进来.

2.4 范围 range

范围主要用于迭代数组,切片,通道或集合中的元素. 用于for循环中. 其返回值根据迭代对象决定

go
func main() {
	// 声明一个包含 2 的幂次方的切片
	var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
	// 遍历数组,会返回两个值,第一个是索引,第二个是值
	for index, value := range pow {
		// 打印 2 的 i 次方等于 v
		fmt.Printf("2^%d = %d\n", index, value)
	}

	// 遍历字符串,会返回两个值,一个是索引,一个是字符(类似于Char)
	for i, c := range "hello我0" {
		fmt.Printf("index: %d, char: %c\n", i, c)
	}

	// 创建一个空的 map,key 是 int 类型,value 是 float32 类型
	floatMap := make(map[int]float32)

	// 向 map1 中添加 key-value 对
	// float类型的map
	floatMap[1] = 1.0
	floatMap[2] = 2.0
	floatMap[3] = 3.0
	floatMap[4] = 4.0
	// 遍历map时,会返回两个值,一个是键,一个是值
	for key, value := range floatMap {
		// 打印 key 和 value
		fmt.Printf("key is: %d - value is: %.2f\n", key, value)
	}
	
	// range 也可以用在 map 的键值对上。
	// 字符串类型的map
	stringMap := map[string]string{"a": "apple", "b": "banana"}
	for k, v := range stringMap {
		fmt.Printf("%s -> %s\n", k, v)
	}

	// 遍历通道中的元素,直到通道关闭为止
	ch := make(chan int, 2)
	ch <- 100
	ch <- 2
	close(ch)
	// 遍历通道,以获取通道中的元素
	for v := range ch {
		fmt.Println(v)
	}
}

2.5 集合 map

Map是一种无序键值对的集合,可通过key快速检索value的值,当然,也可以使用切片迭代map.

此外,map是引用类型,将map的值,赋值给另外一个变量后,若在另外一个变量中修改了map,则原map也会被修改.

map必须先声明,再初始化后才能使用,否则直接报错.

go
func main() {
	var siteMap map[string]string // 创建集合,但此时siteMap为nil,需要使用make初始化后才能使用.

	fmt.Println(siteMap)        // map[]
	fmt.Println(siteMap == nil) // true

	// 懒人写法,一步到位
	//siteMap := make(map[string]string)

	siteMap = make(map[string]string) // 初始化map,分配空间

	// 插入新元素时,只要键不存在,那么就是新增,若键已存在,那么就是修改
	siteMap["Google"] = "谷歌"
	siteMap["Baidu"] = "百度"
	siteMap["KuLve"] = "圐圙"
	siteMap["Wiki"] = "维基百科"

	// 使用键输出网站集合
	for site := range siteMap {
		fmt.Println(site, "->", siteMap[site])
	}

	// 查看元素在集合中是否存在
	// ok的值为true或false
	// name为值
	value, ok := siteMap["Facebook"]
	if ok {
		fmt.Println("Facebook 的 站点是", value)
	} else {
		fmt.Println("Facebook 站点不存在")
	}
}

2.6 强制类型转换

基本语法

go
<类型>(<表达式>)