什么是GO语言
- 高性能、高并发
- 语法简单、学习曲线平缓
- 丰富的标准库
- 完善的工具链
- 静态链接
- 快速编译
- 跨平台
- 垃圾回收
基础知识
Hello World
package main
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
var
package main
import (
"fmt"
"math"
)
func main() {
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64
f := float32(e)
g := a + "foo"
fmt.Println(a, b, c, d, e, f)
fmt.Println(g)
const s string = "hello"
const h = 5200000
const i = 3e20 / h
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
var关键字声明
var a int = 10
如果在声明时进行初始化,可以省略类型:
var b = 20
:=短变量同时声明和初始化变量
c := 30
Go 是强类型语言,每个变量都有一个明确的类型,常见类型包括 int, float64, string, bool 等。
零值:
- 如果变量被声明但未初始化,Go 会为其赋予“零值”:
- 整数类型的零值是
0 - 浮点类型的零值是
0.0 - 布尔类型的零值是
false - 字符串的零值是
""(空字符串)
- 整数类型的零值是
作用域:
- 变量的作用域是根据其声明的位置决定的,函数内部的变量只能在该函数内访问,而全局变量可以在整个包内访问。
const
常量使用 const 关键字声明,不能被修改:
const Pi = 3.14
常量可以是字符串、布尔值或数字:
const (
A = "Hello"
B = 42
C = true
)
常量可以不指定类型,Go 会根据上下文推断类型:
const D = 5.0 //类型为float64
常量可以参与计算,编译时计算结果可以用作其他常量的初始化:
onst E = 10
const F = E * 2 // F 的值为 20
if-else
条件表达式可以是任何返回布尔值的表达式,如比较运算符 (==, !=, <, >, <=, >=) 和逻辑运算符 (&&, ||, !)。
短声明
if x := compute(); x > 0 {
// x 大于 0 的处理
} else {
// x 小于或等于 0 的处理
}
package main
import "fmt"
func main() {
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
for
只有for循环
基础的for循环
for i:=0;i<10;i++{
fmt.Println(i)
}
省略初始化和现有条件
i := 0
for i < 10 {
fmt.Println(i)
i++
}
无限循环
for {
// 永远执行的代码
}
迭代集合
fruits := []string{"apple", "banana", "cherry"}
for i, fruit := range fruits {
fmt.Printf("%d: %s\n", i, fruit)
}
switch
**自动break:**Go的switch语句在匹配成功会自动退出,不需要显示使用break
表达式省略:switch后的表达式可以省略,省略表示true,可以用来代替多个if-else的条件判断
func main() {
value := 10
switch {
case value < 5:
fmt.Println("Less than 5")
case value >= 5 && value < 10:
fmt.Println("Between 5 and 10")
default:
fmt.Println("10 or more")
}
}
类型判断:switch语句还可以用于类型判断,这在接口类型的断言中非常有用。
func main() {
var x interface{} = 10
switch i := x.(type) {
case int:
fmt.Printf("x is an int: %d\n", i)
case string:
fmt.Printf("x is a string: %s\n", i)
default:
fmt.Println("Unknown type")
}
}
interface{}是Go语言中的一个特殊类型,表示空接口。它可以持有任何类型的值,因为所有类型都实现了空接口。这使得interface{}可以用作通用容器,能够存储任何类型的数据。
array
数组的定义: 包括元素类型和数组长度
var arr [5]int
初始化数组:
//直接初始化
arr := [5]int{1,2,3,4,5}
//使用 ... 自动推导长度
arr2 := [...]int{10,20,30}
数组遍历:
arr := [5]rune{'a', 'b', 'c', 'd', 'e'}
for i, v := range arr {
fmt.Println(i, v)
}
// 0 97
// 1 98
// 2 99
// 3 100
// 4 101
arr := [5]string{"a", "b", "c", "d", "e"}
for i, v := range arr {
fmt.Println(i, v)
}
// 0 a
// 1 b
// 2 c
// 3 d
// 4 e
数组的值传递: 在Go语言中,数组是值类型,传递数组时会复制整个数组
func modifyArray(arr [5]int) {
arr[0] = 100
}
func main() {
original := [5]int{1, 2, 3, 4, 5}
modifyArray(original)
fmt.Println(original[0]) // 仍然是 1,因为传递的是副本
}
切片: 切片是Go中对数组的更灵活的封装,允许动态大小。
slice := []int{1,2,3}
slice = append(slice,4)
slice
定义切片: 切片的定义不需要指定长度,使用[]符号表示。
var slice []int
创建切片: 切片可以通过字面量、内置的make函数或者通过数组切片创建
// 字面量创建
slice := []int{1,2,3,4,5}
// make函数创建
slice := make([]int,5) //创建一个长度5的切片,元素初始化为0
slice := make([]int,0) //创建一个空切片
**切片的长度和容量:**长度是切片元素的数量,容量是切片底层数组的总大小。
length := len(slice)
capacity := cap(slice)
动态扩展切片: 使用append函数向切片添加元素
slice = append(slice,6)
切片是引用类型: 切片本质是对底层数组的引用,多个切片可以共享一个底层数组。
slice1 := []int{1, 2, 3}
slice2 := slice1
slice2[0] = 100
fmt.Println(slice1)
map
定义和创建Map: 可以使用内置的make函数或者字面量来创建map
// 使用map函数
m := make(map[string]int) //创建一个键为字符串类型、值为整型的map
// 使用字面量
m := map[string]int{
"apple":2,
"banana":2,
}
添加更新元素: 赋值语法向map中添加或更新键值对
m["orange"] = 3 // 添加新的键值对
m["apple"] = 5 // 更新键 "apple" 的值
value := m["banana"] // 访问键 "banana" 对应的值
删除元素: 使用delete从map中删除指定的键
delete(m, "banana") // 删除键 "banana" 及其对应的值
检查键是否存在: 通过多重赋值可以检查一个键是否存在
value,exists := m["apple"]
if exists{
}else{
}
遍历Map: 使用for循环和range关键字遍历map中的键值对
for k, v := range m {
fmt.Println(k, v)
}
空map: 使用nil初始化的map无法添加元素
var emptyMap map[string]int // nil map
emptyMap = make(map[string]int) // 正确的初始化
map是引用类型: 多个变量可以引用同一个map
m1 := make(map[string]int)
m1["key"] = 1
m2 := m1
m2["key"] = 2
fmt.Println(m1["key"])
并发安全: Go的map不是线程安全的,多个goroutine同时读写同一个map可能会导致程序崩溃。在并发环境中,可以使用sync.Mutex或sync.Map来实现安全访问。
range
range是一个关键字,用于在for循环中遍历数组、切片、字符串、映射和通道。
用于数组和切片: 返回每个元素的索引和值
arr := []int{1, 2, 3, 4, 5}
for index, value := range arr {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
用于字符串: 返回字符串的索引和 Unicode码点值
str := "hello"
for index, char := range str {
fmt.Printf("Index: %d, Char: %c\n", index, char)
}
用于映射: 键和值
m := map[string]int{"a": 1, "b": 2, "c": 3}
for key, value := range m {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
用于通道: 当用于通道时,range可以接收从通道中发送的值,直到通道被关闭。
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 关闭通道
}()
for value := range ch {
fmt.Println(value) // 输出通道中的值
}
func
函数的定义:
func add(a int,b int) int{
return a+b
}
多个返回值: go语言支持多个返回值
func swap(a,b int)(int,int){ //a的类型需要是int
return b,a
}
x,y := swap(1,2)
命名返回值: 方便在函数内部直接修改返回值
func divide(a,b int)(res int,err error){
if b == {
err = fmt.Errorf("division by zero")
return
}
res = a/b
return
}
可变参数函数: 使用...声明可变参数,使函数能够接受任意数量的参数。
func sum(numbers ...int)int{
total := 0
for _,number := range numbers{
total += number
}
return total
}
res := sum(1,2,3,4,5)
闭包: Go中的函数可以嵌套,内部函数可以访问外部函数变量,这称为闭包。
func counter() func() int{
count := 0
return func() int {
count++
return count
}
}
nextCount := counter()
fmt.Println(nextCount) //输出1
fmt.Println(nextCount) //输出2
高阶函数: 函数可以作为参数传递给其他函数,也可以作为返回值返回,这称为高阶函数。
// fn函数名
func apply (fn func(int) int,value int) int{
return fn(value)
}
// double函数
double := func(x int) int{
return x*2
}
res := apply(double,5)
pointer
pointer是一种特殊的数据类型,用于存储变量的内存地址。指针允许你直接访问和修改变量的值,而不需要复制整个变量。
指针的定义: 指针的类型由*符号表示,*表示指向某种类型的指针。
var p *int; //定义一个指向整型的指针
获取变量的地址: 使用&符号获取一个变量的地址,并将其赋值给指针。
var a int = 10
p = &a
通过指针访问值: 通过*符号可以解引用指针,访问指针指向的值
fmt.Println(*p) //输出10.解引用指针p获取a的值
*p = 20 //通过指针修改a的值
fmt.Println(a) //输出20,a的值被修改
指针与函数: 指针常用于函数参数中,以避免传值带来的性能损失,尤其是当要传递大型结构体或数组时。
func increment(n *int)
{
*n++;
}
var x int = 5
increment(&x)
fmt.Println(x)
指针与切片: 切片和映射都是引用类型,它们本身并不需要使用指针,但在某些情况下,可能会使用指针来指向他们。
func modifyslice(s *[]int){
(*s)[0] = 100 //解引用并修改切片的第一个元素
}
slice := []int(1,2,3)
modifyslice(&slice)
fmt.Println(slice)
指针的零值:指针的零值nil,表示指针不指向任何有效的地址。
struct
定义**Struct**: 使用type关键字可以定义一个新的struct类型,字段有名称和类型组成。
type Person struct{
Name string
Age int
}
创建Struct实例: 可以使用字面量或new关键字创建struct实例
// 使用字面量
p1 := Person{Name:"Alice",Age:10}
// 使用new
p2 := new(Person)
P2.Name = "Bob"
p2.Age = 25
p1不是指针,而不是一个Person类型的值,它直接存储了Name和Age字段的值。
p2是一个指向Person的指针。
访问**Struct**字段: 可以使用点操作符.来访问struct的字段。
fmt.Println(p1.Name)
嵌套Struct: 在struct中嵌套其他struct,这可以用于构建更复杂的数据结构。
type Address struct {
City string
ZipCode string
}
type Employee struct {
Name string
Age int
Address Address
}
func main() {
emp := Employee{
Name: "Emma",
Age: 28,
Address: Address{
City: "New York",
ZipCode: "10001",
},
}
fmt.Println(emp.Name) // 输出:Emma
fmt.Println(emp.Address.City) // 输出:New York
}
Struct的方法: 可以为struct定义方法,以便对结构体实力进行操作。方法接收者可以是结构体的值或者指针。
func (p Person) Greet() string{
return "Hello, my name is " + p.Name
}
func (p *Person) HaveBirthday(){
p.Age++
}
p1.HaveBirthday()
fmt.Println(p1.Greet())
空值Struct: 结构体的零值是每个字段类型的零值
var p3 Person
匿名字段和嵌套结构体: Go还支持匿名字段,通常用于实现类似继承的功能。
package main
import "fmt"
type ContactInfo struct {
Email string
Phone string
}
type Manager struct {
Name string
Age int
ContactInfo
}
func main() {
m := Manager{
Name: "Lihao",
Age: 18,
ContactInfo: ContactInfo{
Email: "abc@124.com",
Phone: "13800138000",
},
}
fmt.Println(m)
}
在这个例子中,ContactInfo作为Manager的匿名字段,可以直接通过m.Email和m.Phone访问,不需要显示地写成m.ContactInfo.Email。
结构体方法: 在Go中,结构体可以定义方法,这些方法可以理解为是和某个类型绑定的函数。定义方法时,需要指定接受者(receiver),这使得方法可以作用于特定的结构体实例。
type Rectangle struct {
width, height float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
func (r *Rectangle) SetWidth(newWidth float64) {
r.width = newWidth
}
func main() {
rect := Rectangle{width: 20, height: 10}
fmt.Println(rect.width, rect.height)
rect.SetWidth(10)
fmt.Println(rect.width, rect.height)
}
error
error是一种内置的接口类型,通常用于表示错误信息。Go语言显示地处理错误,而不是使用异常机制。
错误处理是显示,设计优点:
- 错误不会被隐式地吞掉,程序的每个部分都清楚地知道哪里出了问题。
- 可以根据不同的错误类型采取不同的处理策略,保持程序的健壮性。
error类型:error类型是一个接口类型
type error interface{
Error() string
}
error接口只有一个方法:Error(),它返回一个string类型的错误描述信息。
任何实现了Error()方法的类型都可以作为error类型使用。
创建和返回错误: 在Go中,通过实现error接口的方式,返回一个错误。
import "errors"
func doSomething() error{
return errors.New("something went wrong")
}
func main(){
err := doSomething()
if err != nil{
fmt.Println("Error:",err)
}
}
自定义错误类型: Go允许定义一个结构体类型,并实现Error()方法来创建更复杂的错误类型。
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("code:%d message:%s", e.Code, e.Message)
}
func doSomething() error {
return &MyError{Code: 404, Message: "Not Found"}
}
func main() {
err := doSomething()
if err != nil {
fmt.Println("Error:", err)
}
}
检查包装的错误:error.Is来判断某个错误是否是特定类型,或是是否由某个错误引起的。
var ErrNotFound = errors.New("not found")
func doSomething() error {
return fmt.Errorf("doSomething failed: %w", ErrNotFound)
}
func main() {
err := doSomething()
if errors.Is(err, ErrNotFound) {
fmt.Println("Error: not found")
}
}
string
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "llo")) // true
fmt.Println(strings.Index(a, "ll")) // 2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(len(a)) // 5
b := "你好"
fmt.Println(len(b)) // 6
fmt
结构体格式化(带字段名):
// 带字段名
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
// 带类型信息
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
type point struct {
x, y int
}
func main() {
s := "hello"
n := 123
p := point{1, 2}
fmt.Println(s, n) // hello 123
fmt.Println(p) // {1 2}
fmt.Printf("s=%v\n", s) // s=hello
fmt.Printf("n=%v\n", n) // n=123
fmt.Printf("p=%v\n", p) // p={1 2}
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
f := 3.141592653
fmt.Println(f) // 3.141592653
fmt.Printf("%.2f\n", f) // 3.14
}
json
Go中的JSON基本操作:
json.Marshal()用于将Go的数据结构编码为JSON字符串。
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
p := Person{Name: "Alice", Age: 25}
jsonData, err := json.Marshal(p)
if err != nil {
fmt.Println("Error encoding JSON:", err)
return
}
fmt.Println(string(jsonData)) // 输出:{"name":"Alice","age":25}
}
结构体标签(tag):
- 在结构体字段定义时,我们使用了标签(如
json:"name"),这表示将结构体字段Name在 JSON 中表示为键"name"。结构体标签的作用是控制 JSON 序列化和反序列化时的字段名称。 **json.Unmarshal()**用于将JSON字符串解码为Go的数据结构。
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
jsonString := `{"name": "Bob", "age": 30}`
var p Person
err := json.Unmarshal([]byte(jsonString), &p)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
fmt.Println(p.Name, p.Age) // 输出:Bob 30
}
- 使用
json.Unmarshal([]byte(jsonString), &p)将 JSON 字符串解码为Person结构体。 - 注意:
json.Unmarshal需要传入的是[]byte类型的数据,所以需要将字符串转换为字节数组。 - 解码时需要传递指向结构体的指针(
&p),以便json.Unmarshal可以直接修改结构体内容。
处理动态JSON: JSON结构不固定,当需要处理更灵活的JSON数据时,可以使用 map[string]interface{}来处理。
func main() {
jsonString := `{"name": "Charlie", "age": 32, "skills":
["Go", "Python", "JavaScript"]}`
var data map[string]interface{}
err := json.Unmarshal([]byte(jsonString), &data)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
fmt.Println(data["name"]) // 输出:Charlie
fmt.Println(data["age"]) // 输出:32
fmt.Println(data["skills"].([]interface{})) // 输出:[Go Python JavaScript]
}
在这个例子中,data是一个map[string]interface{},可以存储任意键值对。
由于 JSON 数据的结构未知,因此字段类型是 interface{},这意味着我们需要进行类型断言(如 data["skills"].([]interface{}))来访问具体类型的数据。
** 结构体字段的导出 :** json.Marshal 和 json.Unmarshal 只能处理结构体中 导出的字段,即首字母大写的字段。首字母小写的字段是私有的,无法被编码或解码。
type Person struct {
name string `json:"name"` // 错误:字段不可导出
Age int `json:"age"`
}
func main() {
p := Person{name: "David", Age: 35}
jsonData, err := json.Marshal(p)
if err != nil {
fmt.Println("Error encoding JSON:", err)
return
}
fmt.Println(string(jsonData)) // 输出:{"age":35},字段 "name" 未被编码
}
time
time.Time类型:time.Time 是表示时间的核心类型,它表示了一个具体的时间点。可以使用 time.Now() 函数获取当前时间。
默认情况下,time.Now() 获取的是本地时间。如果你需要获取某个特定时区的时间,可以使用 time.LoadLocation 来实现。
currentTime := time.Now()
loc, err := time.LoadLocation("America/New_York")
格式化时间: time 包提供了 Format 方法来将时间格式化为特定的字符串表示形式。在 Go 中,格式化时间的方式非常特别,它使用一个参考时间:2006-01-02 15:04:05 来定义格式。这些数字是 Go 中的魔法时间,代表了 2006 年 1 月 2 日 15:04:05,开发人员通过这种方式来定义期望的格式。
func main() {
currentTime := time.Now()
// 常见的格式化示例
fmt.Println(currentTime.Format("2006-01-02 15:04:05")) // 标准格式,输出类似:2024-11-05 14:23:01
fmt.Println(currentTime.Format("2006/01/02 03:04:05 PM")) // 带 AM/PM 的 12 小时制,输出类似:2024/11/05 02:23:01 PM
fmt.Println(currentTime.Format("02-Jan-2006")) // 输出:05-Nov-2024
}
** 解析时间 :** time.Parse()从字符串中解析时间。
func main() {
timeString := "2024-11-05 14:23:01"
parsedTime, err := time.Parse("2006-01-02 15:04:05", timeString)
if err != nil {
fmt.Println("解析时间出错:", err)
return
}
fmt.Println("解析后的时间:", parsedTime)
}
time.Parse 根据传入的格式解析时间字符串,如果格式不匹配会返回错误。
时间的计算和操作:
time 包还提供了对时间进行算术操作的方法,比如增加、减少时间等。
- 增加时间:使用
Add方法。 - 减少时间:传递负的
Duration给Add方法
func main() {
currentTime := time.Now()
fmt.Println("当前时间:", currentTime)
// 增加 2 小时
twoHoursLater := currentTime.Add(2 * time.Hour)
fmt.Println("两小时后的时间:", twoHoursLater)
// 减少 30 分钟
thirtyMinutesAgo := currentTime.Add(-30 * time.Minute)
fmt.Println("30 分钟前的时间:", thirtyMinutesAgo)
}
在这里,time.Hour、time.Minute 等都是 time.Duration 类型,代表时间的长度。time.Duration 其实是 int64 类型,单位是纳秒(ns)。
时间差计算:
Sub 方法计算两个时间之间的差值,返回的是 time.Duration。Sub 方法返回两个时间点之间的差值,可以进一步转换为秒、毫秒等。
func main() {
start := time.Now()
// 模拟某些操作的延迟
time.Sleep(2 * time.Second)
end := time.Now()
duration := end.Sub(start)
fmt.Println("操作耗时:", duration) // 输出:操作耗时: 2s
}
Sub 方法返回两个时间点之间的差值,可以进一步转换为秒、毫秒等。
定时器与延迟:
- 延迟:可以使用
time.Sleep函数使程序暂停一段时间。
func main() {
fmt.Println("等待 2 秒...")
time.Sleep(2 * time.Second)
fmt.Println("2 秒后程序继续执行")
}
- 定时器:可以使用
time.NewTimer创建定时器,定时器到期时会向通道发送时间信息。
func main() {
timer := time.NewTimer(3 * time.Second)
fmt.Println("计时器开始...")
<-timer.C // 阻塞直到定时器通道发送信号
fmt.Println("计时器到期,继续执行")
}
时间戳 : Go 中也可以获取 Unix 时间戳,这个时间戳表示从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间所经过的秒数或毫秒数。
定时任务(Ticker):使用 time.Ticker 可以实现每隔一段时间执行某个任务的功能。Ticker 会周期性地向通道发送当前时间。
func main() {
ticker := time.NewTicker(1 * time.Second) // 每秒触发一次
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C
fmt.Println("Ticker 触发,当前时间:", time.Now())
}
}
在这个例子中,NewTicker(1 * time.Second) 每秒向通道发送一次信号,程序每次读取到信号后会打印当前时间。
strconv
strconv包提供了 字符串与基本数据类型之间转换 的一组函数,是处理输入输出时非常常用的工具。- 常用的函数包括:
- 整数转换:
Atoi(字符串到整数)、Itoa(整数到字符串)。 - 浮点数转换:
ParseFloat(字符串到浮点数)、FormatFloat(浮点数到字符串)。 - 布尔值转换:
ParseBool(字符串到布尔值)、FormatBool(布尔值到字符串)。 - 进制转换:
ParseInt(字符串到整数,支持指定进制)、FormatInt(整数到字符串,支持指定进制)。
env
- 读取环境变量:使用
os.Getenv()来读取指定环境变量的值,如果环境变量不存在,返回空字符串。 - 设置环境变量:使用
os.Setenv()来设置环境变量。 - 删除环境变量:使用
os.Unsetenv()来删除某个环境变量。 - 遍历环境变量:使用
os.Environ()可以获取系统中所有的环境变量。 - 提供默认值:在读取环境变量时,可以通过编写辅助函数来提供默认值,以确保在环境变量未设置的情况下程序也能正常运行。
.env文件管理:使用.env文件集中管理环境变量,并使用第三方库(如godotenv)加载环境变量,可以方便地为开发、测试和生产环境提供同的配置。