我是一名 C++开发者,我其次写过 java、web 前端开发、也对 nodejs 有些了解,但是都没在工作中用过,没有在生产环境下使用过,但是入职 C++开发游戏服务端岗位后,发现公司有些业务已经用 Go 写了。而且同事大佬们许多也都会 Go,所以不学就要落后喽,但是对于 C++仍旧持续学习新特性,深入深入再深入学习,虽然工作中写的 C++代码像 C++98,但挡不住对技术的热爱。
先看一下 Go By Example 毕竟咱也是代码经验不错的开发者了,此笔记不适用于没有开发背景的人学习。我想更是从 C++视角看 Go。
main.go,像 java 一样上来就是模块化
package main
import "fmt"
func main(){
.Println("hello world")
fmt}
直接运行
go run main.go
编译
go build main.go
对于 hello world 对于 C++开发应该一点压力都没有
package main
import "fmt"
func main(){
.Println("go"+"lang")
fmt//golang
.Println("1+1=",1+1)
fmt//1+1= 2
.Println("7.0/3.0=",7.0/3.0)
fmt//7.0/3.0= 2.3333333333333335
.Println(true&&false)
fmt//false
.Println(true||false)
fmt//true
.Println(!true)
fmt//false
}
这些对于科班的老鸟还不是小意思吗
值类型(Value Types)是指变量直接存储值本身,它们在赋值或传递给函数时会被复制。以下是一些 Go 语言中的值类型:
引用类型(Reference Types)是指变量存储的是值的内存地址,而不是值本身。它们在赋值或传递给函数时,复制的是指向底层数据的指针。以下是一些 Go 语言中的引用类型:
需要注意的是,虽然切片和映射看起来像是引用类型,但它们实际上是引用类型的包装器,底层仍然是值类型。
在 Go 中变量是需要显示声明的,其实和 C++差不多,Go 总之不是弱类型语言
package main
import "fmt"
func main(){
//自动推断有初始值的变量像C++的auto
var a="initial"
.Println(a)//initial
fmt//var可以声明多个变量 int b,c不更优雅,有点无语
var b,c int = 1,2;
.Println(b,c)//1 2
fmtvar d = true
.Println(d)//true
fmt//默认初始化 整形的话一般为0,但是这不是规范的写法,开发中声明的变量理应都初始化
var e int8
.Println(e)//0
fmt//:=为声明初始化的简写
:= "short"
f var f_str string = "short"
.Println(f," ",f_str)//short short
fmt}
在此还真没感觉 Go 很香,对于写 C/C++或者 js 的,我感觉很难接收这种写法和 Python 一样写起来别扭
常量 const,支持字符、字符串、布尔、数值
package main
import(
"fmt"
"math"
)
const s string = "constant string"
func main(){
.Println(s)//打印全局变量
fmt//constant string
const n = 500000000;
const d = 3e20 / n;
.Println(d)
fmt//6e+11
.Println(int64(d))//强制转换
fmt//600000000000
.Println(math.Sin(n))
fmt//-0.28470407323754404
}
Go 中的常量表达式,常量表达式是指在编译期间就可以确定结果的表达式。常量表达式可以包括数字、字符串、布尔值和枚举类型等。
const Pi = 3.14159
const MaxSize int = 100
const Str = "Hello,World"
const MaxInt = 1 << 32 - 1
const MinInt = -MaxInt - 1
const IsTrue = true && false
常熟表达式可以执行任意精度的运算,应该编译时就已经算好了
先看代码,真服了还真像 Python 那种狗屎写法
package main
import(
"fmt"
)
func main(){
var i int = 1
for i<=3{
.Println(i)//1 2 3
fmt= i + 1
i }
for j:=7;j<=9;j++{
.Println(j)//7 8 9
fmt}
for{
.Println("For Loop")
fmtbreak;//不进行break则是死循环
}
for n:=0;n<=5;n++{
if n%2 == 0{
continue
}
.Println(n)//1 3 5
fmt}
}
Go 中没有三目运算符,if 支持定义语句作用域变量和 for 一样
package main
import "fmt"
func main() {
if 0 == 7%2 {
.Println("7 is even")
fmt} else {
.Println("8 is odd")
fmt}
if num := 9; num < 0 {
.Println(num, "is negative")
fmt} else if num < 10 {
.Println(num, "has 1 digit")
fmt} else {
.Println(num, "has multiple digits")
fmt}
}
/*
8 is odd
9 has 1 digit
*/
go 的 switch 不支持使用 break,但是支持用 fallthrough 执行所在 case 后执行下一个 case。go 的 switch 支持定义变量
go 的 switch 语句可以用于对以下类型匹配
package main
import (
"fmt"
"time"
)
func main() {
:= 2
i .Print("write ", i, " as ")
fmtswitch i {
case 1:
.Println("one")
fmtcase 2:
.Println("two")
fmtcase 3:
.Println("three")
fmt}
//write 2 as two
switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
.Println("It's the weekend")
fmtdefault:
.Println("It's a weekday")
fmt}
//It's the weekend
:= time.Now()
t .Println(t) //2023-08-05 17:31:51.7226392 +0800 CST m=+0.011514601
fmtswitch {
case t.Hour() < 12:
.Println("It's before noon")
fmtdefault:
.Println("It's after noon")
fmt}
//It's after noon
//看是那种接口类型的实现
:= func(i interface{}) {
whatAmI switch t := i.(type) {
case bool:
.Println("I'm a bool")
fmtcase int:
.Println("I'm an int")
fmtdefault:
.Printf("Don't know type %T\n", t)
fmt}
}
(true) //I'm a bool
whatAmI:= 100
num (num) //I'm an int
whatAmI("hello") //Don't know type string
whatAmI}
在 go 中数组是一个具有编号且长度固定的元素序列,起始但从数组这来看,go 是有点香的对吧,比起 C 的要方便不少
package main
import "fmt"
func main() {
var arrInt [5]int
.Println("arrInt:", arrInt)
fmt//emp: [0 0 0 0 0]
[4] = 444
arrInt.Println("arrInt:", arrInt)
fmt//arrInt: [0 0 0 0 444]
.Println("ele:", arrInt[4])
fmt//ele: 444
//获取数组长度
.Println("len:", len(arrInt))
fmt//5
//初始化
:= [5]int{1, 2, 3, 4, 5}
b .Println(b)
fmt//[1 2 3 4 5]
//多维数组
var twoD [2][3]int
for i := 0; i < len(twoD); i++ {
for j := 0; j < len(twoD[i]); j++ {
[i][j] = i + j
twoD}
}
.Println("2d:", twoD)
fmt//2d: [[0 1 2] [1 2 3]]
:= [2][2][2]int{{{1, 2}, {1, 2}}, {{1, 2}, {1, 2}}}
threeD .Println("3d", threeD)
fmt//3d [[[1 2] [1 2]] [[1 2] [1 2]]]
}
Go 语言中的切片(slice)是一种动态数组,它提供了对数组的部分或全部元素的访问和操作。切片是引用类型,它底层依赖于数组,但具有更灵活的长度和容量。 有点像 C++的 std::vector 喽
其实 Go 中没有真的数组,下面我们来看下,出下列外还可以进行 copy 与多维切片之类的,在此先不展开阐述,仅为本章知识总揽全局。我们 C++程序员先了解了解就好。
package main
import "fmt"
func main() {
//1.声明一个切片类型
var s []int
= []int{1, 2, 3} //初始化切片
s := []int{1, 2, 3} //简写
s1 //2.可以使用内置函数len与cap获取切片的长度和容量
.Println(len(s)) //3
fmt.Println(cap(s)) //3
fmt.Println(len(s1)) //3
fmt//3.可以使用索引访问与修改元素
[1] = 666
s.Println(s[1]) //666
fmt.Println(s) //[1 666 3]
fmt//4.支持切片表达式获取子切片
:= []int{1, 2, 3, 4, 5}
s2 := s2[0:2] //下标0到下标1
subS2 .Println(subS2) //[1 2]
fmt.Println(s2[:]) //[1 2 3 4 5]
fmt.Println(s2[:3]) //[1 2 3]
fmt.Println(s2[1:]) //[2 3 4 5]
fmt//这种切片骚操作有点像Python了其实
//5.可以用append函数向切片末尾追加元素
:= []int{1, 2, 3}
s3 = append(s3, 4)
s3 .Println(s3) //[1 2 3 4]
fmt//6.切片可以用make函数创建指定长度和容量的切片
:= make([]int, 3, 5) //长度3容量5
s4 .Println(len(s4)) //3
fmt.Println(cap(s4)) //5
fmt.Println(s4) //[0 0 0]
fmt= append(s4, 9)
s4 .Println(s4) //[0 0 0 9]
fmt= append(s4, 999)
s4 = append(s4, 888)
s4 .Println(s4) //[0 0 0 9 999 888]
fmt}
map 对于 C++程序员用的 std::map 熟悉不过了。如 std::map 基于红黑树按照键的顺序排列,属于有序的关联容器。std::unordered_map 属于无序的关联容器,基于哈希表。
在 Go 语言中,map 是一种键值对的集合,也被称为字典或关联数组。它提供了一种快速查找和访问数据的方式。
package main
import "fmt"
func main() {
//1.声明与初始化
:= make(map[string]int)
m ["k1"] = 12
m["k2"] = 23
m.Println(m) //map[k1:12 k2:23]
fmt:= m["k1"]
v1 .Println(v1) //12
fmt.Println(len(m)) //2
fmt:= map[string]int{"a": 1, "b": 2}
n .Println(n) //map[a:1 b:2]
fmt//2.删除键值对
delete(m, "k2")
.Println(m) //map[k1:12]
fmt//3.判断有没有某个键
, prs := m["k2"]
_.Println(prs) //false
fmt/*
当从一个 map 中取值时,还有可以选择是否接收的第二个返回值,
该值表明了 map 中是否存在这个键。 这可以用来消除 键不存在
和 键的值为零值 产生的歧义, 例如 0 和 ""。这里我们不需
要值,所以用 空白标识符(blank identifier) _ 将其忽略。
*/
//4.遍历map所有元素
for name, age := range m {
.Println(name, age)
fmt} // k1 12
}
range 用于迭代各种各样的数据结构。 让我们来看看如何在我们已经学过的数据结构上使用 range。
package main
import "fmt"
func main() {
//1.例如遍历切片
:= []int{1, 2, 3, 4}
nums := 0
sum for _, num := range nums {
+= num
sum }
.Println(sum) //10
fmtfor i, num := range nums {
if num == 3 {
.Println(i, " ", num)
fmt//2 3
}
}
//2.例如遍历map
:= map[string]string{"a": "apple", "b": "banana"}
kvs for k, v := range kvs {
.Println(k, "=", v)
fmt} //a = apple b = banana
//3.遍历字符串
for i, c := range "go" {
.Printf("%d %c\n", i, c)
fmt} //0 g 1 o
}
go 中有一点说的就是,函数是不支持重载的,说实话骚操作没有 C++多
当多个连续的参数为同样类型时, 可以仅声明最后一个参数的类型,忽略之前相同类型参数的类型声明。
package main
import "fmt"
func plus(a int, b int) int {
return a + b
}
func plusplus(a, b, c int) int {
return a + b + c
}
func plusf(a float32, b float32) float32 {
return a + b
}
func main() {
.Println(plus(1, 2)) //3
fmt.Println(plusplus(1, 2, 3)) //6
fmt.Println(plusf(1.2, 1.4)) //2.6
fmt}
挺骚操作的,C++起码用数组和 std::tuple
package main
import (
"fmt"
)
() (int, int) {
func valsreturn 3, 7
}
() {
func main, b := vals()
a.Println(a, b) // 输出:3 7
fmt, c := vals()
_, _ := vals()
d.Println(c) //7
fmt.Println(d) //3
fmt}
变参函数早已不是什么骚操作,C 中和 C++11 变参模板都可以
C 风格
#include <iostream>
#include <cstdarg>
// 接受可变数量参数的函数
double average(int count, ...) {
va_list args;
(args, count);
va_start
double sum = 0;
for (int i = 0; i < count; i++) {
+= va_arg(args, int);
sum }
(args);
va_end
return sum / count;
}
int main() {
double result = average(4, 2, 4, 6, 8);
::cout << "Average: " << result << std::endl;
std
return 0;
}
C++11 变参模板
<iostream>
#include
// 递归终止函数
() {
double averagereturn 0;
}
// 变参模板函数
<typename T, typename... Args>
template(T arg, Args... args) {
double averagereturn arg + average(args...) / (sizeof...(args) + 1);
}
int main() {
= average(2, 4, 6, 8);
double result ::cout << "Average: " << result << std::endl;
std
return 0;
}
go 语言,语法而言确实挺香
package main
import "fmt"
func sum(nums ...int) {
.Println(nums, " ")
fmt:= 0
total for _, num := range nums {
+= num
total }
.Println(total)
fmt}
func main() {
(1, 2)
sum//[1 2]
//3
(1, 2, 3)
sum//[1 2 3]
//6
:= []int{1, 2, 3, 4}
nums (nums...) //解构
sum//[1 2 3 4]
//10
}
闭包这东西,对于写 js 的其实很熟悉,go 中也有
C++除此之外还有像写可调用对象之类的形式
#include <iostream>
using namespace std;
auto createClosure(int x)
{
return [x]()
{
return x * x;
};
}
int main(int argc, char **argv)
{
auto closure = createClosure(43);
std::cout << closure() << std::endl;
// 1849
return 0;
}
GO 中像 JavaScript 一样是支持延长变量声明周期的,毕竟 go 有垃圾回收,C++就要使用动态内存实现这种骚操作了,而且很难实现动态内存管理
package main
import "fmt"
func intSeq() func() int {
:= 0
i return func() int {
++
ireturn i
}
}
func main() {
:= intSeq()
mfunc .Println(mfunc()) //1
fmt.Println(mfunc()) //2
fmt.Println(mfunc()) //3
fmt.Println(mfunc()) //4
fmt.Println(mfunc()) //5
fmt}
函数递归
package main
import "fmt"
func fact(n int) int {
.Println(n)
fmtif n == 0 {
return 1
}
return n * fact(n-1)
}
func main() {
.Println(fact(10))
fmt}
//10 9 8 7 6 5 4 3 2 1 0 3628800
闭包递归,必须先显式声明
package main
import "fmt"
func main() {
var fib func(n int) int
= func(n int) int {
fib .Println(n)
fmtif n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
.Println(fib(7)) //13
fmt}
指针对于一名 C++程序员再熟悉不过了
package main
import "fmt"
func zeroval(ival int64) {
= 0
ival }
func zeroptr(iptr *int64) {
*iptr = 0
}
func main() {
var a int64 = 2
var b int64 = 3
(a)
zeroval(&b)
zeroptr.Println(a) //2
fmt.Println(b) //0
fmt}
Go 语言中的字符串是一个只读的 byte 类型的切片。 Go 语言和标准库特别对待字符串 - 作为以 UTF-8 为编码的文本容器。 在其他语言当中, 字符串由”字符”组成。 在 Go 语言当中,字符的概念被称为 rune - 它是一个表示 Unicode 编码的整数。
package main
import (
"fmt"
"unicode/utf8"
)
func isGao(r rune) bool {
return r == '高'
}
func main() {
var s string = "高万禄abcd"
.Println(len(s)) //13
fmtfor i := 0; i < len(s); i++ {
.Printf("%x ", s[i])
fmt}
.Println()
fmt//e9 ab 98 e4 b8 87 e7 a6 84 61 62 63 64
// a b c d
//可见"高万禄"占据了9个字节
.Println(utf8.RuneCountInString(s)) //7
fmtfor idx, runeVal := range s {
.Println(idx, runeVal)
fmt}
//0 39640 3 19975 6 31108 9 97 10 98 11 99 12 100
.Println(utf8.DecodeRuneInString(s)) //39640 3 ,编码39640 3bytes
fmtfor i, bytes := 0, 0; i < len(s); i += bytes {
, width := utf8.DecodeRuneInString(s[i:])
runeValue.Printf("%#U at %d\n", runeValue, i)
fmt= width
bytes .Println(isGao(runeValue))
fmt}
//U+9AD8 '高' at 0 true|U+4E07 '万' at 3 false|U+7984 '禄' at 6 false
//U+0061 'a' at 9 false|U+0062 'b' at 10 false|U+0063 'c' at 11 false
//U+0064 'd' at 12 false
}
可见从兼容 utf8 这种设计而言,go 比 C++强太多了,而且不需要考虑语言版本问题
这对于来鸟不是小意思?
package main
import "fmt"
type Person struct {
string
name int
age }
func newPerson(name string) *Person {
:= Person{name: name}
p .age = 21
preturn &p
}
func main() {
.Println(Person{"gwl", 21}) //{gwl 21}
fmt.Println(Person{name: "gwl", age: 30}) //{gwl 30}
fmt.Println(Person{name: "fred"}) //{fred 0}
fmt.Println(&Person{name: "", age: 0}) //&{ 0}
fmtvar s Person = Person{name: "gaowanlu", age: 21}
.Println(s.name, s.age) //gaowanlu 21
fmt:= &s
sptr .Println(sptr.name, sptr.age) //gaowanlu 21
fmt:= newPerson("gaowanlu")
personPtr .Println(*personPtr) //{gaowanlu 21}
fmt}
GO 支持为结构体类型定义方法
调用方法时,Go 会自动处理值和指针之间的转换。 想要避免在调用方法时产生一个拷贝,或者想让方法可以修改接受结构体的值, 你都可以使用指针来调用方法。
ptrChange 方法接收一个指向 Person 结构体的指针作为接收者。这意味着在调用该方法时,可以直接修改原始结构体的值。在方法内部,通过指针来访问结构体的字段,并对其进行更改。因此,通过调用 personPtr.ptrChange() , person 的值将被修改为 name: “a” 和 age: 1 。
valChange 方法接收一个 Person 结构体的值作为接收者。当调用该方法时,会创建一个接收者的副本,并在副本上进行操作,而不会影响原始结构体的值。因此,通过调用 person.valChange() , person 的值不会改变。
package main
import "fmt"
type Person struct {
string
name int
age }
func (p *Person) print() {
.Println("name: ", p.name, " age: ", p.age)
fmt}
func (p *Person) ptrChange() {
.name = "a"
p.age = 1
p}
func (v Person) valChange() {
.name = "b"
v.age = 2
v}
func main() {
:= Person{name: "w", age: 3}
person .print() //name: w age: 3
person.Println(person) //{w 3}
fmt.valChange()
person.print() //name: w age: 3
person.Println(person) //{w 3}
fmt:= &person
personPtr .ptrChange()
personPtr.print() //name: a age: 1
personPtr.Println(*personPtr) //{a 1}
fmt}
方法签名的集合叫做:接口(Interfaces)。
package main
import (
"fmt"
"math"
)
type geometry interface {
() float64
area() float64
perim}
type rect struct {
, height float64
width}
type circle struct {
float64
radius }
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perim() float64 {
return 2*r.width + 2*r.height
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}
func measure(g geometry) {
.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
fmt}
func main() {
:= rect{width: 3, height: 4}
r := circle{radius: 5}
c (r)
measure// {3 4}
// 12
// 14
(c)
measure// {5}
// 78.53981633974483
// 31.41592653589793
}
Go 支持对于结构体(struct)和接口(interfaces)的 嵌入(embedding) 以表达一种更加无缝的 组合(composition) 类型
Go 语言中没有像其他一些面向对象编程语言那样的继承机制。相反,Go 语言使用组合来实现代码的重用和扩展。
在 Go 语言中,可以通过在结构体中嵌入其他结构体来实现组合。这样,嵌入的结构体可以访问其字段和方法,就好像它们是在当前结构体中定义的一样。这种方式可以实现代码的重用,而无需显式地继承。
另外,Go 语言还提供了接口(interface)的概念,通过接口可以实现多态性。结构体可以实现一个或多个接口,并按照接口定义的方法来调用结构体的行为。这样,可以在不同的结构体上使用相同的接口,实现代码的灵活性和可扩展性。
总而言之,尽管 Go 语言没有继承的概念,但通过组合和接口的使用,可以实现代码的重用和扩展。
package main
import "fmt"
type base struct {
int
num }
func (b base) describe() string {
return fmt.Sprintf("base with num=%v", b.num)
}
type container struct {
basestring
str }
func main() {
:= container{
co : base{
base: 1,
num},
: "some name",
str}
.Printf("co={num: %v, str: %v}\n", co.num, co.str)
fmt.Println("also num:", co.base.num)
fmt.Println("describe:", co.describe())
fmttype describer interface {
() string
describe}
var d describer = co
.Println("describer:", d.describe())
fmt}
/*
co={num: 1, str: some name}
also num: 1
describe: base with num=1
describer: base with num=1
*/
C++的泛型是基于模板的,Go 中也有泛型的特性
package main
import "fmt"
func MapKeys[K comparable, V any](m map[K]V) []K {
:= make([]K, 0, len(m))
r for k, _ := range m {
= append(r, k)
r }
return r
}
func main() {
var m = map[int]string{1: "2", 2: "4", 4: "8"}
//自动推断
.Println("keys :", MapKeys(m)) //keys : [1 2 4]
fmt//显式指定
.Println(MapKeys[int, string](m)) //[1 2 4]
fmt}
用泛型写链表 C++
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
class Element
{
public:
<T> *next{nullptr};
Element;
T val};
template <typename T>
class List
{
public:
<T> *head{nullptr};
Element<T> *tail{nullptr};
Element};
// 尾插法
template <typename T>
void Push(List<T> *lst, T v)
{
if (lst->tail == nullptr)
{
->head = new Element<T>;
lst->head->val = v;
lst->tail = lst->head;
lst}
else
{
->tail->next = new Element<T>;
lst->tail->next->val = v;
lst->tail = lst->tail->next;
lst}
}
template <typename T>
<T> GetAll(List<T> *lst)
vector{
<T> vec;
vectorfor (auto ptr = lst->head; ptr != nullptr; ptr = ptr->next)
{
.push_back(ptr->val);
vec}
return vec;
}
int main(int argc, char **argv)
{
<int> lst;
List(&lst, 1);
Push(&lst, 2);
Push(&lst, 3);
Pushfor (auto n : GetAll(&lst))
{
<< n << " ";
cout }
<< std::endl;
cout // 1 2 3
return 0;
}
使用 GO 编写,对比上面的 C++样例很容易学习
package main
import "fmt"
type List[T any] struct {
, tail *element[T]
head}
type element[T any] struct {
*element[T]
next
val T}
// 尾插法
func (lst *List[T]) Push(v T) {
if lst.tail == nil {
.head = &element[T]{val: v}
lst.tail = lst.head
lst} else {
.tail.next = &element[T]{val: v}
lst.tail = lst.tail.next
lst}
}
func (lst *List[T]) GetAll() []T {
var elems []T
for e := lst.head; e != nil; e = e.next {
= append(elems, e.val)
elems }
return elems
}
func main() {
:= List[int]{}
lst .Push(1)
lst.Push(2)
lst.Push(3)
lst.Println(lst.GetAll()) //[1 2 3]
fmt}
符合 Go 语言习惯的做法是使用一个独立、明确的返回值来传递错误信息。 这与 Java、Ruby 使用的异常(exception) 以及在 C 语言中有时用到的重载 (overloaded) 的单返回/错误值有着明显的不同。 Go 语言的处理方式能清楚的知道哪个函数返回了错误,并使用跟其他(无异常处理的)语言类似的方式来处理错误。
package main
import (
"errors"
"fmt"
)
func f1(arg int) (int, error) {
if arg == 42 {
return -1, errors.New("can't work with 42")
}
return arg + 3, nil
}
// 自定义Error类型
type argError struct {
int
arg string
prob }
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
func f2(arg int) (int, error) {
if arg == 42 {
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}
func main() {
for _, i := range []int{7, 32} {
if r, e := f1(i); e != nil {
.Println("f1 failed:", e)
fmt} else {
.Println("f1 worked:", r)
fmt}
}
//f1 worked: 10
//f1 worked: 35
for _, i := range []int{7, 42} {
if r, e := f2(i); e != nil {
.Println("f2 failed:", e)
fmt} else {
.Println("f2 worked", r)
fmt}
}
//f2 worked 10
//f2 failed: 42 - can't work with it
, e := f2(42)
_if ae, ok := e.(*argError); ok {
.Println(ae.arg)
fmt.Println(ae.prob)
fmt}
//42
//can't work with it
}
关于以下部分是用于检查错误类型的代码段。首先,我们调用函数 f2(42)
并将返回的结果赋值给变量 e 。然后,我们使用类型断言将 e 转换为
*argError
类型的变量 ae 。如果类型断言成功,即 e 的类型是
*argError
,则条件 ok 为 true ,我们可以访问 ae
的字段。在这种情况下,我们打印出 ae.arg 和 ae.prob
的值,分别对应错误的参数和问题描述。
因为 error 类型是个接口,argError 实际上实现了 error
type error interface {
() string
Error}
e 是接口 error 类型,go 中可用
interface.(*T)转换为具体类型,还会进行断言 ok
为真则转换成功,非接口类型转换直接
T(v)如i:=64 f:=float64(i)
, e := f2(42)
_if ae, ok := e.(*argError); ok {
.Println(ae.arg)
fmt.Println(ae.prob)
fmt}
其实 Go 中这种异常处理方式,其实也挺优雅的,但是说不上是真正的异常机制
协程(goroutine)是轻量级得执行线程
import 语句导入了两个包: fmt 和 time 。 fmt 包用于格式化输出, time 包用于时间相关操作。
f 函数是一个普通函数,它接受一个字符串参数 from 。在函数体内部,使用 for 循环打印出三次 from 的值。
main 函数是程序的入口函数。在函数体内部,首先调用 f(“direct”) ,这是直接调用函数的方式。然后使用 go 关键字调用 f(“goroutin”) ,这是使用协程(goroutine)并发运行函数的方式。接下来,使用协程运行一个匿名函数,该函数接受一个字符串参数 msg ,并打印出该参数的值。最后,使用 time.Sleep(time.Second) 让程序休眠一秒钟,以确保协程有足够的时间执行。最后,打印出”done”表示程序执行完毕。
package main
import (
"fmt"
"time"
)
func f(from string) {
for i := 0; i < 3; i++ {
.Println(from, ":", i)
fmt}
}
func main() {
("direct") //直接运行
fgo f("goroutin") //使用协程运行
//协程运行匿名函数
go func(msg string) {
.Println(msg)
fmt}("going")
.Sleep(time.Second)
time.Println("done")
fmt}
/*
direct : 0
direct : 1
direct : 2
going
goroutin : 0
goroutin : 1
goroutin : 2
done
*/
通道(channels) 是连接多个协程的管道。
你可以从一个协程将值发送到通道,然后在另一个协程中接收。
使用 make(chan val-type) 创建一个新的通道。
通道类型就是他们需要传递值的类型。
使用 channel <- 语法 发送 一个新的值到通道中。使用 <-channel
语法从通道中 接收 一个值。
默认发送和接收操作是阻塞的,直到发送方和接收方都就绪。 这个特性允许我们,不使用任何其它的同步操作, 就可以在程序结尾处等待消息 “ping”。
package main
import (
"fmt"
"time"
)
type Message struct {
string
name int
age }
func main() {
:= make(chan Message)
message go func() {
:= time.Now().Unix()
timestamp for {
:= time.Now().Unix()
now if now-timestamp > 5 {
<- Message{name: "end", age: 21}
message break
}
<- Message{name: "gaowanlu", age: 21}
message }
}()
go func() {
:= time.Now().Unix()
timestamp for {
:= time.Now().Unix()
now if now-timestamp > 7 {
break
}
:= <-message
msg .Println(msg)
fmtif msg.name == "end" {
break
}
}
}()
.Sleep(time.Second * 10)
time}
//{gaowanlu 21}......{end 21}
在默认情况下,通道是无缓冲的,意味着只有对应的接收(<-chan
)通道准备好接收时,才允许进行发送(chan<-
)。有缓冲通道允许在没有对应接收者的情况下,缓存一定数量的值。
package main
import "fmt"
func main() {
:= make(chan string, 2) //2个缓冲
messages <- "message1"
messages <- "message2"
messages .Println(<-messages) //message1
fmt.Println(<-messages) //message2
fmt}
C++程序员肯定直到线程同步,协程同步理解起来当然也很简单
package main
import (
"fmt"
"time"
)
// 接收通道bool型,形参命名为done
func worker(done chan bool) {
.Println("working...")
fmt.Sleep(time.Second)
time.Println("done")
fmt<- true
done }
func main() {
:= make(chan bool, 1)
done go worker(done)
<-done //阻塞
}
//working...
//等三秒
//done
当使用通道作为函数的参数时,可以指定通道是否为只读或只写,该特性可以提升程序的类型安全。
package main
import "fmt"
//只写
func ping(pings chan<- string, msg string) {
<- msg
pings }
//pings只读,pongs只写
func pong(pings <-chan string, pongs chan<- string) {
:= <-pings
msg <- msg
pongs }
func main() {
:= make(chan string, 1)
pings := make(chan string, 1)
pongs (pings, "passed message")
ping(pings, pongs)
pong.Println(<-pongs) //passed message
fmt}
Go 的选择器(select)可以同时等待多个通道操作。有点像 C++的 select IO 多路复用。
package main
import (
"fmt"
"time"
)
func main() {
:= make(chan string)
c1 := make(chan string)
c2 go func() {
.Sleep(1 * time.Second)
time<- "one"
c1 }()
go func() {
.Sleep(2 * time.Second)
time<- "two"
c2 }()
for i := 0; i < 2; i++ {
.Println("select ", i)
fmtselect {
case msg1 := <-c1:
.Println("received", msg1)
fmtcase msg2 := <-c2:
.Println("received", msg2)
fmt}
}
}
/*
select 0
received one
select 1
received two
*/
超时 对于一个需要连接外部资源, 或者有耗时较长的操作的程序而言是很重要的。 得益于通道和 select,在 Go 中实现超时操作是简洁而优雅的。更像 C++ IO 多路复用超时处理了。
select 是阻塞的。
package main
import (
"fmt"
"time"
)
func main() {
.Println("select 1")
fmt:= make(chan string, 1)
c1 go func() {
.Sleep(2 * time.Second)
time<- "result 1"
c1 }()
select {
case res := <-c1:
.Println(res)
fmtcase <-time.After(1 * time.Second):
.Println("timeout 1")
fmt}
.Println("select 2")
fmt:= make(chan string, 1)
c2 go func() {
.Sleep(2 * time.Second)
time<- "result 2"
c2 }()
select {
case res := <-c2:
.Println(res)
fmtcase <-time.After(3 * time.Second):
.Println("timeout 2")
fmt}
}
/*
select 1
timeout 1
select 2
result 2
*/
有非阻塞 IO 肯定有非阻塞通道喽。常规的通过通道发送和接收数据是阻塞的。 然而,我们可以使用带一个 default 子句的 select 来实现 非阻塞 的发送、接收,甚至是非阻塞的多路 select。
package main
import "fmt"
func main() {
:= make(chan string)
messages := make(chan bool)
signals select {
case msg := <-messages:
.Println("received message", msg)
fmtdefault:
.Println("no message received")
fmt}
//会直接输出 no message received
:= "hi"
msg select {
case messages <- msg:
.Println("sent message", msg)
fmtdefault:
.Println("no message sent")
fmt}
//会直接输出 no message sent
//因为没有人接收
select {
case msg := <-messages:
.Println("received message", msg)
fmtcase sig := <-signals:
.Println("received signal", sig)
fmtdefault:
.Println("no activity")
fmt}
//直接输出no activity
}
关闭 一个通道意味着不能再向这个通道发送值了。 该特性可以向通道的接收方传达工作已经完成的信息。
package main
import "fmt"
func main() {
:= make(chan int, 3)
jobs := make(chan bool)
done go func() {
//死循环
for {
, more := <-jobs
jif more {
.Println("received job", j)
fmt} else {
.Println("received all jobs")
fmt<- true
done break
}
}
}()
for j := 1; j <= 3; j++ {
<- j
jobs .Println("sent job", j)
fmt}
close(jobs)
.Println("sent all jobs")
fmt.Println(<-done)
fmt}
// received job 1
// sent job 1
// sent job 2
// sent job 3
// sent all jobs
// received job 2
// received job 3
// received all jobs
// true
通道是支持 for 和 range 迭代遍历的
遍历的话用这种方式也不是不行
for {
, more := <-jobs
jif more {
//
} else {
//
break
}
}
但是 for-range 更像语法糖,里应当只遍历 close 过的通道,一个非空的通道也是可以关闭的,通道中剩下的值仍然可以被接收到。遍历没有 close 的通道会报错。
package main
import "fmt"
func main() {
:= make(chan string, 2)
queue <- "one"
queue <- "two"
queue close(queue)
for elem := range queue {
.Println(elem)
fmt}
}
// one
// two
Go 有内置的定时器
timer1 是从构造开始计时的。当 timer1.C 的通道接收到数据时,表示定时器已经触发,而不是在接收到数据时开始计时。timer.C 中的 C 是 Channel(通道)的缩写。
package main
import (
"fmt"
"time"
)
func main() {
:= time.NewTimer(2 * time.Second)
timer1 <-timer1.C
.Println("Timer 1 fired")
fmt}
在 main 函数中,我们创建了一个名为 timer2 的定时器,它将在 1 秒后触发。使用了一个匿名函数来作为一个 goroutine(并发执行的函数)。在这个 goroutine 中,我们使用 <-timer2.C 语句从 timer2 的通道中接收数据。当定时器触发时,这个语句会解除阻塞,从而执行后续的代码。
调用了 timer2.Stop() 方法来停止定时器。如果定时器成功停止,返回值
stop2 会为 true,我们将打印出 “Timer 2
stopped”。协程中的<-timer2.C
并不会返回。
package main
import (
"fmt"
"time"
)
func main() {
:= time.NewTimer(time.Second)
timer2 go func() {
<-timer2.C
.Println("Timer 2 fired")
fmt}()
:= timer2.Stop() //停止定时器
stop2 //停止timer2成功
if stop2 {
.Println("Timer 2 stopped")
fmt}
.Sleep(3 * time.Second)
time}
//Timer 2 stopped
打点器是以固定的时间间隔重复执行而准备的。死循环,加定时器,select
package main
import (
"fmt"
"time"
)
func main() {
:= time.NewTicker(500 * time.Millisecond)
ticker := make(chan bool)
done go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
.Println("Tick at", t)
fmt}
}
}()
.Sleep(8000 * time.Millisecond)
time.Stop()
ticker<- true
done .Println("Ticker stopped")
fmt}
/*
Tick at 2023-08-19 15:22:39.2576045 +0800 CST m=+0.513205701
Tick at 2023-08-19 15:22:39.7555552 +0800 CST m=+1.011156401
Tick at 2023-08-19 15:22:40.2530881 +0800 CST m=+1.508689301
Tick at 2023-08-19 15:22:40.753203 +0800 CST m=+2.008804201
Tick at 2023-08-19 15:22:41.2539281 +0800 CST m=+2.509529301
Tick at 2023-08-19 15:22:41.7518864 +0800 CST m=+3.007487601
Tick at 2023-08-19 15:22:42.2498314 +0800 CST m=+3.505432601
Tick at 2023-08-19 15:22:42.7499687 +0800 CST m=+4.005569901
Tick at 2023-08-19 15:22:43.2495641 +0800 CST m=+4.505165301
Tick at 2023-08-19 15:22:43.749537 +0800 CST m=+5.005138201
Tick at 2023-08-19 15:22:44.2538916 +0800 CST m=+5.509492801
Tick at 2023-08-19 15:22:44.7520632 +0800 CST m=+6.007664401
Tick at 2023-08-19 15:22:45.2503554 +0800 CST m=+6.505956601
Tick at 2023-08-19 15:22:45.7527025 +0800 CST m=+7.008303701
Tick at 2023-08-19 15:22:46.2523764 +0800 CST m=+7.507977601
Tick at 2023-08-19 15:22:46.7510895 +0800 CST m=+8.006690701
Ticker stopped
*/
C++通常用线程池来进行 Task 处理,但在 go 中使用协程池会非常便捷
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
.Println("worker", id, "started job")
fmt.Sleep(time.Second) //模拟费时人物
time.Println("worker", id, "finished job", j)
fmt<- j * 2
results }
}
func main() {
const numJobs = 10
:= make(chan int, numJobs)
jobs := make(chan int, numJobs)
results //建3个worker协程
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
<- j
jobs }
close(jobs)
for i := 1; i <= 5; i++ {
.Println(<-results)
fmt}
}
/*
worker 3 started job
worker 1 started job
worker 2 started job
worker 2 finished job 3
worker 2 started job
worker 1 finished job 2
worker 1 started job
6
4
worker 3 finished job 1
2
worker 1 finished job 5
worker 2 finished job 4
10
8
*/
想要等待多个协程完成,我们可以使用 wait group。像 C++多线程编程里的屏障。
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int) {
.Println("Worker %d starting", id)
fmt.Sleep(time.Second)
time.Println("Worker %d done", id)
fmt}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
//在等待组中添加一个等待任务,表示有一个任务需要等待。
.Add(1)
wg//这一行的目的是为了避免在闭包中捕获到循环变量 i 的引用。由于闭包是在稍后异步执行的,如果不保存 i 的当前值,所有的闭包都会捕获相同的 i 值,导致打印的 id 不正确。
:= i
i go func() {
//使用 defer 延迟执行,当协程执行完成时,会调用 wg.Done() 来通知等待组,表示一个任务已经完成。
defer wg.Done()
(i)
worker}()
}
//等待等待组中的所有任务完成。这个函数会一直阻塞,直到等待组中的计数器降为零,也就是所有工作任务都完成了。
.Wait()
wg}
/*
Worker %d starting 5
Worker %d starting 2
Worker %d starting 3
Worker %d starting 4
Worker %d starting 1
Worker %d done 1
Worker %d done 4
Worker %d done 3
Worker %d done 2
Worker %d done 5
*/
速率限制是控制服务资源利用和质量的重要机制,基于协程、通道和打点器
package main
import (
"fmt"
"time"
)
func main() {
:= make(chan int, 5)
requests for i := 1; i <= 5; i++ {
<- i
requests }
close(requests)
:= time.Tick(200 * time.Millisecond)
limiter
//没200ms才会loop一次
for req := range requests {
<-limiter
.Println("request", req, time.Now())
fmt}
:= make(chan time.Time, 3)
burstyLimiter for i := 0; i < 3; i++ {
<- time.Now()
burstyLimiter }
//协程定时发出信号
go func() {
for t := range time.Tick(200 * time.Millisecond) {
<- t
burstyLimiter }
}()
:= make(chan int, 5)
burstyRequests for i := 1; i <= 5; i++ {
<- i
burstyRequests }
close(burstyRequests)
for req := range burstyRequests {
<-burstyLimiter
.Println("request", req, time.Now())
fmt}
}
/*
request 1 2023-09-02 02:13:21.4947625 +0800 CST m=+0.206486401
request 2 2023-09-02 02:13:21.7054319 +0800 CST m=+0.417155801
request 3 2023-09-02 02:13:21.8956622 +0800 CST m=+0.607386101
request 4 2023-09-02 02:13:22.094804 +0800 CST m=+0.806527901
request 5 2023-09-02 02:13:22.2934502 +0800 CST m=+1.005174101
request 1 2023-09-02 02:13:22.2934502 +0800 CST m=+1.005174101
request 2 2023-09-02 02:13:22.2939621 +0800 CST m=+1.005686001
request 3 2023-09-02 02:13:22.2939621 +0800 CST m=+1.005686001
request 4 2023-09-02 02:13:22.5088651 +0800 CST m=+1.220589001
request 5 2023-09-02 02:13:22.6968576 +0800 CST m=+1.408581501
*/
C++新版本中其实也有了原子整型,GO 中也不例外
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var ops uint64
= 0
ops var wg sync.WaitGroup
for i := 0; i < 50; i++ {
.Add(1)
wggo func() {
defer wg.Done()
for c := 0; c < 1000; c++ {
.AddUint64(&ops, 1)
atomic}
}()
}
.Wait()
wg.Println("ops:", ops)
fmt}
//ops:50000
写 C++的肯定知道 posix 的 mutex 与 C++并发标准库的 mutex,Go 中当然也会有。
package main
import (
"fmt"
"sync"
)
type Container struct {
.Mutex
mu syncmap[string]int
counters }
func (c *Container) inc(name string) {
.mu.Lock()
cdefer c.mu.Unlock()
.counters[name]++
c}
func main() {
:= Container{
c : map[string]int{"a": 0, "b": 0},
counters}
var wg sync.WaitGroup
:= func(name string, n int) {
doIncrement for i := 0; i < n; i++ {
.inc(name)
c}
.Done()
wg}
.Add(3)
wggo doIncrement("a", 10000)
go doIncrement("a", 10000)
go doIncrement("b", 10000)
.Wait()
wg.Println(c.counters)
fmt}
//map[a:20000 b:10000]
除了使用互斥量,其实还可以利用通道和协程来实现对某些数据的互斥访问,在游戏开发中常常用这种思想,来实现并发,就像使用消息队列一样,多个线程发出操作请求,但处理请求的服务是单线程的。
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
type readOp struct {
int
key chan int
resp }
type writeOp struct {
int
key int
val chan bool
resp }
func main() {
var readOps uint64
var writeOps uint64
:= make(chan readOp)
reads := make(chan writeOp)
writes //消费者
go func() {
var state = make(map[int]int)
for {
select {
case read := <-reads:
.AddUint64(&readOps, 1)
atomic.resp <- state[read.key]
readcase write := <-writes:
.AddUint64(&writeOps, 1)
atomic[write.key] = write.val
state.resp <- true
write}
}
}()
//并发读
for r := 0; r < 100; r++ {
go func() {
for {
:= readOp{
read : rand.Intn(5),
key: make(chan int)}
resp<- read
reads <-read.resp
.Sleep(time.Millisecond)
time}
}()
}
//并发写
for w := 0; w < 10; w++ {
go func() {
for {
:= writeOp{
write : rand.Intn(5),
key: rand.Intn(100),
val: make(chan bool)}
resp<- write
writes <-write.resp
.Sleep(time.Millisecond)
time}
}()
}
.Sleep(time.Second)
time:= atomic.LoadUint64(&readOps)
readOpsFinal .Println("readOps:", readOpsFinal)
fmt:= atomic.LoadUint64(&writeOps)
writeOpsFinal .Println("writeOps:", writeOpsFinal)
fmt}
Go 的 sort 包实现了内建及用户自定义数据类型的排序功能。
package main
import (
"fmt"
"sort"
)
func main() {
:= []string{"c", "a", "b"}
strs .Strings(strs)
sort.Println("Strings:", strs)
fmt:= []int{7, 2, 4}
ints .Ints(ints)
sort.Println("Ints:", ints)
fmt:= sort.IntsAreSorted(ints)
s .Println("Sorted:", s)
fmt}
/*
Strings: [a b c]
Ints: [2 4 7]
Sorted: true
*/
数据类型需要实现 Len、Swap、Less 方法
package main
import (
"fmt"
"sort"
)
// byLength如果像支持sort则需要
// 实现sort.Interface接口
// Len Less Swap方法
type byLength []string
func (s byLength) Len() int {
return len(s)
}
func (s byLength) Swap(i, j int) {
[i], s[j] = s[j], s[i]
s}
func (s byLength) Less(i, j int) bool {
return len(s[i]) < len(s[j])
}
func main() {
:= []string{"peach", "banana", "kiwi"}
fruits .Sort(byLength(fruits))
sort.Println(fruits)
fmt}
//[kiwi peach banana]
,panic 是一种运行时异常,用于表示程序发生了一个不可恢复的错误或紧急情况。
package main
import "fmt"
func test1() {
panic("This is a panic")
}
func test2() {
()
test1.Println("test 2")
fmt}
func main() {
()
test2.Println("hello world")
fmt}
/*
panic: This is a panic
goroutine 1 [running]:
main.test1(...)
c:/Users/gaowanlu/Desktop/MyProject/note/testcode/go/main.go:6
main.test2()
c:/Users/gaowanlu/Desktop/MyProject/note/testcode/go/main.go:10 +0x27
main.main()
c:/Users/gaowanlu/Desktop/MyProject/note/testcode/go/main.go:15 +0x19
exit status 2
*/
panic 会立即停止当前函数的执行,并开始执行调用栈上的延迟(deferred)函数
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
.Println("Recovered from panic:", r)
fmt}
}()
panic("This is a panic")
}
//Recovered from panic: This is a panic
Defer 用于确保程序在执行完成后,会调用某个函数,一般是执行清理工作。 Defer 的用途跟其他语言的 ensure 或 finally 类似。
package main
import (
"fmt"
"os"
)
func main() {
:= createFile("./tmp/defer.txt")
f defer closeFile(f)
(f)
writeFile}
func createFile(p string) *os.File {
.Println("creating")
fmt, err := os.Create(p)
fif err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
.Println("writing")
fmt.Fprintln(f, "data")
fmt}
func closeFile(f *os.File) {
.Println("closing")
fmt:= f.Close()
err if err != nil {
.Fprintf(os.Stderr, "error:%v\n", err)
fmt.Exit(1)
os}
}
/*
creating
writing
closing
*/
Go 通过使用 recover 内置函数,可以从 panic 中 恢复 recover 。 recover 可以阻止 panic 中止程序,并让它继续执行。panic->defer->end
package main
import "fmt"
func mayPanic() {
panic("a problem")
}
func main() {
defer func() {
if r := recover(); r != nil {
.Println("Recovered,Error:\n", r)
fmt}
}()
()
mayPanic.Println("After mayPanic()")
fmt//这行代码不会执行,因为 mayPanic 函数会调用 panic。 main 程序的执行在 panic 点停止,并在继续处理完 defer 后结束。
}
/*
Recovered,Error:
a problem
*/
下面的代码,就会输出 After mayPanic,因为所有 panic 已经在 test 函数内处理过了。
package main
import "fmt"
func mayPanic() {
panic("a problem")
}
func test() {
defer func() {
if r := recover(); r != nil {
.Println("Recovered,Error:\n", r)
fmt}
}()
()
mayPanic}
func main() {
()
test.Println("After mayPanic()")
fmt}
/*
Recovered,Error:
a problem
After mayPanic()
*/
标准库的 strings 包提供了很多有用的字符串相关的函数。
package main
import (
"fmt"
"strings"
s )
var p = fmt.Println
func main() {
//包含Contains: true
("Contains: ", s.Contains("test", "es"))
p//子串数 Count: 2
("Count: ", s.Count("test", "t"))
p//前缀 HasPrefix: true
("HasPrefix: ", s.HasPrefix("test", "tes"))
p//后缀 HasSuffix: true
("HasSuffix: ", s.HasSuffix("test", "st"))
p//Index: 1
("Index: ", s.Index("test", "es"))
p//Join: a-b
("Join: ", s.Join([]string{"a", "b"}, "-"))
p//Repeat: abcabcabcabcabc
("Repeat: ", s.Repeat("abc", 5))
p//Replace: abcdkkfff
("Replace: ", s.Replace("abcdeeeefff", "ee", "k", -1))
p//Replace: abcdeekkk
("Replace: ", s.Replace("abcdeefff", "f", "k", 3))
p//Replace: abcdeekkf
("Replace: ", s.Replace("abcdeefff", "f", "k", 2))
p//Split: [a b c d e]
("Split: ", s.Split("a-b-c-d-e", "-"))
p//ToLower: test
("ToLower: ", s.ToLower("TEST"))
p//ToUpper: TEST
("ToUpper: ", s.ToUpper("test"))
p//Len: 5
("Len: ", len("hello"))
p//Char: a
.Printf("Char: %c\n", "abcde"[0])
fmt//Char: b
.Printf("Char: %c\n", "abcde"[1])
fmt}
Go 在传统的 printf 中对字符串格式化提供了优异的支持。
package main
import (
"fmt"
"os"
)
type point struct {
, y int
x}
func main() {
:= point{1, 2}
p .x = 4
p//对象打印
//struct1: {4 2}
.Printf("struct1: %v\n", p)
fmt//struct2: {x:4 y:2}
.Printf("struct2: %+v\n", p)
fmt//带有类型 struct3: main.point{x:4, y:2}
.Printf("struct3: %#v\n", p)
fmt//type: main.point
.Printf("type: %T\n", p)
fmt//格式化布尔值 bool: true
.Printf("bool: %t\n", true)
fmt//整数 int: 123
.Printf("int: %d\n", 123)
fmt//二进制 bin: 1110
.Printf("bin: %b\n", 14)
fmt//字符 char: !
.Printf("char: %c\n", 33)
fmt//十六进制 hex: 1c8
.Printf("hex: %x\n", 456)
fmt//浮点 float1: 78.900000
.Printf("float1: %f\n", 78.9)
fmt//科学计数法
//float2: 1.234000e+08
.Printf("float2: %e\n", 123400000.0)
fmt//float3: 1.234000E+08
.Printf("float3: %E\n", 123400000.0)
fmt//字符串转义 str1: "string"
.Printf("str1: %s\n", "\"string\"")
fmt//字符串不转义 str2: "\"string\""
.Printf("str2: %q\n", "\"string\"")
fmt//%x 输出使用 base-16 编码的字符串, 每个字节使用 2 个字符表示
.Printf("str3: %x\n", "hex this")
fmt//打印指针 pointer: 0xc00001a0c0
.Printf("pointer: %p\n", &p)
fmt//6位数字空间 右对齐
//width1: | 12| 345|
.Printf("width1: |%6d|%6d|\n", 12, 345)
fmt//6位整数部分, . 占用一个 小数部分占2个
//width2: | 1.20| 3.45|
.Printf("width2: |%6.2f|%6.2f|\n", 1.2, 3.45)
fmt//和width2一样,只不过为左对齐
//width3: |1.20 |3.45 |
.Printf("width3: |%-6.2f|%-6.2f|\n", 1.2, 3.45)
fmt
//6个字符空间,右对齐
//width4: | foo| b|
.Printf("width4: |%6s|%6s|\n", "foo", "b")
fmt//左对齐
//width5: |foo |b |
.Printf("width5: |%-6s|%-6s|\n", "foo", "b")
fmt
//格式化到字符串
:= fmt.Sprintf("sprintf: a %s", "string")
s .Println(s) //sprintf: a string
fmt
//格式化到文件描述符,os.Stderr其实就是文件指针
//io: an sprintf: a string
.Fprintf(os.Stderr, "io: an %s\n", s)
fmt
}
Go 使用 text/template 包为创建动态内容或向用户显示自定义输出提供了内置支持。 一个名为 html/template 的兄弟软件包提供了相同的 API,但具有额外的安全功能,被用于生成 HTML。
C++有个第三方库挺好用,https://github.com/pantor/inja
package main
import (
"html/template"
"os"
)
func main() {
//创建名为t1的模板
:= template.New("t1")
t1 //解析模板字符串
, err := t1.Parse("Value is {{.}}\n")
t1if err != nil {
panic(err)
}
/*
template.Must函数是一个实用函数,它用于将模板解析过程和错误检查结合在一起。如果t1.Parse成功,template.Must返回t1,否则会引发panic并显示错误消息。这样可以确保模板的解析过程不会失败,否则程序会崩溃。
*/
= template.Must(t1.Parse("Value: {{.}}\n"))
t1 /*
Value: some text
Value: 5
Value: [Go Rust C++ C#]
*/
.Execute(os.Stdout, "some text")
t1.Execute(os.Stdout, 5)
t1.Execute(os.Stdout, []string{
t1"Go",
"Rust",
"C++",
"C#",
})
//封装模板构造
:= func(name, t string) *template.Template {
Create return template.Must(template.New(name).Parse(t))
}
//如果数据是一个结构体,我们可以使用 {{.FieldName}} 动作来访问其字段。 这些字段应该是导出的,以便在模板执行时可访问。
:= Create("t2", "Name: {{.Name}}\n")
t2 //Name: Jane Doe
.Execute(os.Stdout, struct {
t2string
Name }{"Jane Doe"})
//Name: Mickey Mouse
.Execute(os.Stdout, map[string]string{
t2"Name": "Mickey Mouse",
})
//这同样适用于 map;在 map 中没有限制键名的大小写。
//if/else 提供了条件执行模板。如果一个值是类型的默认值,例如 0、空字符串、空指针等, 则该值被认为是 false。
//这个示例演示了另一个模板特性:使用 - 在动作中去除空格。
:= Create("t3",
t3 "{{if . -}} yes {{else -}} no {{end}}\n")
//yes
.Execute(os.Stdout, "not empty")
t3//no
.Execute(os.Stdout, "")
t3
//切片 在模板中使用Range
:= Create("t4", "Range:{{range .}}{{.}} {{end}}\n")
t4 .Execute(os.Stdout, []string{
t4"GO",
"Rust",
"C++",
"C#",
})
//Range:GO Rust C++ C#
}
Go 提供了内建的正则表达式支持。C++当然也有支持的。
package main
import (
"bytes"
"fmt"
"regexp"
)
func main() {
//正则匹配
, _ := regexp.MatchString("p([a-z]+)ch", "peach")
match.Println(match) //true
fmt
//构造正则表达式
, _ := regexp.Compile("p([a-z]+)ch")
r.Println(r.MatchString("peach")) //true
fmt
//搜索 返回第一个
//peach
.Println(r.FindString("peach punch"))
fmt//[2 7] 下标[2,7)
.Println(r.FindStringIndex("a peach punch"))
fmt
//[peach ea]
.Println(r.FindStringSubmatch("peach punch"))
fmt//[0 5 1 3]
.Println(r.FindStringSubmatchIndex("peach punch"))
fmt
//搜索多个
//[peach punch]
.Println(r.FindAllString("peach punch pinch", 2))
fmt//搜索所有
//[peach punch pinch]
.Println(r.FindAllString("peach punch pinch", -1))
fmt//all: [[0 5 1 3] [6 11 7 9] [12 17 13 15]]
.Println("all:", r.FindAllStringSubmatchIndex(
fmt"peach punch pinch", -1))
//true
.Println(r.Match([]byte("peach")))
fmt
//MustCompile失败则会panic
= regexp.MustCompile("p([a-z]+)ch")
r .Println("regexp:", r) //regexp: p([a-z]+)ch
fmt
//正则替换
//a <fruit>
.Println(r.ReplaceAllString("a peach", "<fruit>"))
fmt
//正则回调替换
//a PEACH
:= []byte("a peach")
in := r.ReplaceAllFunc(in, bytes.ToUpper)
out .Println(string(out))
fmt}
Go 提供内建的 JSON 编码解码(序列化反序列化)支持, 包括内建及自定义类型与 JSON 数据之间的转化。Awesome!!!
package main
import (
"encoding/json"
"fmt"
"os"
)
type response1 struct {
int
Page []string
Fruits }
type resposne2 struct {
int `json:"page"`
Page []string `json:"fruits"`
Fruits }
func main() {
//序列化json.Marshal
//bool
, _ := json.Marshal(true)
bolB.Println(string(bolB)) //true
fmt
//int
, _ := json.Marshal(1)
intB.Println(string(intB)) //1
fmt
//float
, _ := json.Marshal(2.34)
fltB.Println(string(fltB)) //2.34
fmt
//string
, _ := json.Marshal("gopher")
strB.Println(string(strB)) //"gopher"
fmt
//array
:= []string{"apple", "peach", "pear"}
slcD , _ := json.Marshal(slcD)
slcB.Println(string(slcB)) //["apple","peach","pear"]
fmt
//map
:= map[string]int{"apple": 5, "lettuce": 7}
mapD , _ := json.Marshal(mapD)
mapB.Println(string(mapB)) //{"apple":5,"lettuce":7}
fmt
//object
:= &response1{
res1D : 1,
Page: []string{"apple", "peach", "pear"}}
Fruits, _ := json.Marshal(res1D)
res1B//{"Page":1,"Fruits":["apple","peach","pear"]}
.Println(string(res1B))
fmt
:= &resposne2{
res2D : 1,
Page: []string{"apple", "peach", "pear"}}
Fruits, _ := json.Marshal(res2D)
res2B//{"page":1,"fruits":["apple","peach","pear"]}
.Println(string(res2B))
fmt
:= []byte(`{"num":6.13,"strs":["a","b"]}`)
byt var dat map[string]interface{}
if err := json.Unmarshal(byt, &dat); err != nil {
panic(err)
}
//map[num:6.13 strs:[a b]]
.Println(dat)
fmt
:= dat["num"].(float64)
num .Println(num) //6.13
fmt
:= dat["strs"].([]interface{})
strs := strs[0].(string)
str1 .Println(str1) //a
fmt
:= `{"page": 1, "fruits": ["apple", "peach"]}`
str := resposne2{}
res .Unmarshal([]byte(str), &res)
json.Println(res) //{1 [apple peach]}
fmt.Println(res.Fruits[0]) //apple
fmt
:= json.NewEncoder(os.Stdout)
enc := map[string]int{"apple": 5, "lettuce": 7}
d .Encode(d) //{"apple":5,"lettuce":7}
enc}
Go 通过 encoding.xml 包为 XML 和 类-XML 格式提供了内建支持。总之很鸡肋。
package main
import (
"encoding/xml"
"fmt"
)
type Plant struct {
.Name `xml:"plant"`
XMLName xmlint `xml:"id,attr"`
Id string `xml:"name"`
Name []string `xml:"Origin"`
Origin }
func (p Plant) String() string {
return fmt.Sprintf("Plant id=%v,name=%v,origin=%v", p.Id, p.Name, p.Origin)
}
func main() {
:= &Plant{Id: 27, Name: "Coffee"}
coffee .Origin = []string{"AAA", "BBB"}
coffee, _ := xml.MarshalIndent(coffee, " ", " ")
out.Println(string(out))
fmt/*
<plant id="27">
<name>Coffee</name>
<Origin>AAA</Origin>
<Origin>BBB</Origin>
</plant>
*/
.Println(xml.Header + string(out))
fmt/*
<?xml version="1.0" encoding="UTF-8"?>
<plant id="27">
<name>Coffee</name>
<Origin>AAA</Origin>
<Origin>BBB</Origin>
</plant>
*/
var p Plant
if err := xml.Unmarshal(out, &p); err != nil {
panic(err)
}
.Println(p)
fmt//Plant id=27,name=Coffee,origin=[AAA BBB]
:= &Plant{Id: 81, Name: "Tomato"}
tomato .Origin = []string{"Mexico", "California"}
tomato
type Nesting struct {
.Name `xml:"nesting"`
XMLName xml[]*Plant `xml:"parent>child>plant"`
Plants }
:= &Nesting{}
nesting .Plants = []*Plant{coffee, tomato}
nesting, _ = xml.MarshalIndent(nesting, " ", " ")
out.Println(string(out))
fmt/*
<nesting>
<parent>
<child>
<plant id="27">
<name>Coffee</name>
<Origin>AAA</Origin>
<Origin>BBB</Origin>
</plant>
<plant id="81">
<name>Tomato</name>
<Origin>Mexico</Origin>
<Origin>California</Origin>
</plant>
</child>
</parent>
</nesting>
*/
}
Go 为时间(time)和时间段(duration)提供了大量的支持;
package main
import (
"fmt"
"time"
)
func main() {
:= fmt.Println
p := time.Now()
now (now) //2023-09-06 23:17:04.2315883 +0800 CST m=+0.003479201
p:= time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
then (then) //2009-11-17 20:34:58.651387237 +0000 UTC
p
(then.Year()) //2009
p(then.Month()) //November
p(then.Day()) //17
p(then.Hour()) //20
p(then.Minute()) //34
p(then.Second()) //58
p(then.Nanosecond()) //651387237
p(then.Location()) //UTC
p(then.Weekday()) //Tuesday
p
(then.Before(now)) //true
p(then.After(now)) //false
p(then.Equal(then)) //true
p
//时间间隔
:= now.Sub(then)
diff (diff) //120978h46m45.054955063s
p(diff.Hours()) //120978.79400789388
p(diff.Minutes()) //7.258727640473633e+06
p(diff.Seconds()) //4.35523658428418e+08
p(diff.Nanoseconds()) //435523658428417963
p
(then.Add(diff)) //2023-09-06 15:23:31.7993186 +0000 UTC
p(then.Add(-diff)) //1996-01-30 01:46:25.503455874 +0000 UTC
p
}
一般程序会有获取 Unix 时间 的秒数,毫秒数,或者微秒数的需求。
分别使用 time.Now 的 Unix 和 UnixNano, 来获取从 Unix
纪元起,到现在经过的秒数和纳秒数。
注意 UnixMillis 是不存在的,所以要得到毫秒数的话,
你需要手动的从纳秒转化一下。
package main
import (
"fmt"
"time"
)
func main() {
:= time.Now()
now := now.Unix()
secs := now.UnixNano()
nanos .Println(now)
fmt
:= nanos / 1000000
millis .Println(secs) //1694014017
fmt.Println(millis) //1694014017306
fmt.Println(nanos) //1694014017306408400
fmt
//你也可以将 Unix 纪元起的整数秒或者纳秒转化到相应的时间。
.Println(time.Unix(secs, 0)) //2023-09-06 23:28:28 +0800 CST
fmt.Println(time.Unix(0, nanos)) //2023-09-06 23:28:28.5384755 +0800 CST
fmt}
Go 支持通过基于描述模板的时间格式化与解析。
package main
import (
"fmt"
"time"
)
func main() {
:= fmt.Println
p
:= time.Now()
t //2023-09-13T22:48:00+08:00
(t.Format(time.RFC3339))
p
, e := time.Parse(
t1.RFC3339,
time"2012-11-01T22:08:41+00:00")
if e == nil {
(t1) //2012-11-01 22:08:41 +0000 +0000
p}
//10:50PM
(t.Format("3:04PM"))
p//Wed Sep 13 22:52:01 2023
(t.Format("Mon Jan _2 15:04:05 2006"))
p//2023-09-13T22:56:01.126711+08:00
(t.Format("2006-01-02T15:04:05.999999-07:00"))
p:= "3 04 PM"
form , e := time.Parse(form, "8 41 PM")
t2if e == nil {
(t2) //0000-01-01 20:41:00 +0000 UTC
p}
//2023-09-13T22:57:56-00:00
.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n",
fmt.Year(), t.Month(), t.Day(),
t.Hour(), t.Minute(), t.Second())
t
:= "Mon Jan _2 15:04:05 2006"
ansic , e = time.Parse(ansic, "8:41PM")
t(e) //parsing time "8:41PM" as "Mon Jan _2 15:04:05 2006": cannot parse "8:41PM" as "Mon"
p(t) //0001-01-01 00:00:00 +0000 UTC
p}
Go 的 math/rand 包提供了伪随机数生成器。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
//rand.Intn 返回一个随机的整数 n,且 0 <= n < 100。
.Println(rand.Intn(100)) //85
fmt.Println(rand.Intn(100)) //30
fmt
//rand.Float64 返回一个64位浮点数 f,且 0.0 <= f < 1.0。
.Println(rand.Float64()) //0.5017169387431317
fmt.Println((rand.Float64() * 5) + 5) //[5.0,10.0]
fmt
//种种子
:= rand.NewSource(time.Now().UnixNano())
s1 := rand.New(s1)
r1
.Println(r1.Intn(100)) //[0,100]
fmt.Println(r1.Intn(100)) //[0,100]
fmt
:= rand.NewSource(42)
s2 := rand.New(s2)
r2 .Println(r2.Intn(100)) //5 每次都是5
fmt
:= rand.NewSource(42)
s3 := rand.New(s3)
r3 .Println(r3.Intn(100)) //5 每次都是5
fmt
.Println(r3.Intn(100)) //87 每次都是87
fmt}
从字符串中解析数字在很多程序中是一个基础常见的任务,内建的 strconv 包提供了数字解析能力。
package main
import (
"fmt"
"strconv"
)
func main() {
, _ := strconv.ParseFloat("1.234", 64)
f.Println(f) //1.234
fmt
, _ := strconv.ParseInt("123", 0, 64)
i.Println(i) //123
fmt
, _ := strconv.ParseInt("0x1c8", 0, 64)
d.Println(d) //456
fmt
, _ := strconv.ParseUint("789", 0, 64)
u.Println(u) //789
fmt
, _ := strconv.Atoi("135")
k.Println(k) //135
fmt
, e := strconv.Atoi("wat")
_.Println(e) //strconv.Atoi: parsing "wat": invalid syntax
fmt}
URL 提供了统一资源定位方式。
package main
import (
"fmt"
"net"
"net/url"
)
func main() {
:= "postgres://user:pass@host.com:5432/path?k=v&k=o#f"
s , err := url.Parse(s)
uif err != nil {
.Println(err)
fmt} else {
//postgres
.Println(u.Scheme)
fmt.Println(u.User) //pass
fmt.Println(u.User.Username()) //user
fmt, _ := u.User.Password()
p.Println(p) //pass
fmt
.Println(u.Host) //host.com:5432
fmt, port, _ := net.SplitHostPort(u.Host)
host.Println(host) //host.com
fmt.Println(port) //5432
fmt
.Println(u.Path) // /path
fmt.Println(u.Fragment) // f
fmt.Println(u.RawQuery) // k=v&k=o
fmt
, _ := url.ParseQuery(u.RawQuery)
m.Println(m) //map[k:[v o]]
fmt.Println(m["k"][0]) //v
fmt}
}
SHA256 散列(hash) 经常用于生成二进制文件或者文本块的短标识。 例如,TLS/SSL 证书使用 SHA256 来计算一个证书的签名。
SHA-256,全称为”Secure Hash Algorithm 256-bit”,是一种密码学散列函数,它接受输入数据并将其转换为固定长度的 256 位(32 字节)散列值。SHA-256 属于 SHA-2(Secure Hash Algorithm 2)家族,是 SHA-2 家族中的一员。
SHA-256 散列函数有以下特点:
固定长度输出: 无论输入数据的大小如何,SHA-256 始终生成一个 256 位的散列值,这意味着输出长度始终相同。
不可逆性: SHA-256 是一个单向散列函数,即不能从散列值反推出原始输入数据。这是密码学安全性的基本要求之一。
碰撞抵抗性: SHA-256 被设计为具有很高的碰撞抵抗性,这意味着找到两个不同的输入,它们产生相同的散列值的难度非常大。
困难性: 计算 SHA-256 散列值的逆过程(从散列值到原始输入)应该在当前计算技术下是困难的,以确保散列值的安全性。
SHA-256 常常用于密码学应用、数字签名、数据完整性验证和密码哈希存储。例如,在密码学中,它可以用于确保数据传输的完整性,或者用于存储用户密码的散列值,以增加安全性。
需要注意的是,虽然 SHA-256 在许多用例中非常有用,但在某些情况下,可能需要更长的散列(如 SHA-512)或其他加密算法,以满足特定的安全要求。此外,密码学领域不断发展,对于安全性要求不断提高,因此也会出现更安全的散列算法。因此,在选择散列算法时,需要考虑具体的用例和安全需求。
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
:= "sha256 this string"
s := sha256.New()
h .Write([]byte(s))
h:= h.Sum(nil)
bs .Println(s)
fmt.Printf("%x", bs)
fmt}
//sha256 this string
//1af1dfa857bf1d8814fe1af8983c18080019922e557f15a8a0d3db739d77aacb
Go 提供了对 base64 编解码的内建支持。
Base64 编码是一种用于将二进制数据转换为文本字符的编码方法。它将二进制数据(例如图片、音频、文件等)转换为由 64 个不同字符组成的 ASCII 字符串,这些字符包括大小写字母、数字和一些特殊符号。Base64 编码的主要目的是将二进制数据表示为文本,以便在文本协议(如电子邮件、XML、JSON)中传输或存储,因为文本协议通常不支持二进制数据的直接传输。
Base64 编码的特点和用途包括:
Base64 编码的原理是将每 3 个字节的二进制数据编码为 4 个 Base64 字符。如果原始数据的字节数不是 3 的倍数,编码后会使用填充字符(通常是等号 =)来保持输出长度的完整性。解码过程是 Base64 编码的逆过程,将 Base64 字符串还原为原始的二进制数据。
以下是一个示例,展示了如何使用 Base64 编码和解码数据,假设有一个二进制数据流:
原始数据:Hello, World!
Base64 编码后的数据:SGVsbG8sIFdvcmxkIQ==
Base64 解码后的数据:Hello, World!
Base64 编码通常由各种编程语言的标准库提供支持,因此在实际开发中,你可以方便地进行编码和解码操作。
package main
import (
"encoding/base64"
b64 "fmt"
)
func main() {
:= "Hello, World!"
data
//使用 标准 base64 格式进行编解码。
:= b64.StdEncoding.EncodeToString([]byte(data))
sEnc .Println(sEnc) //SGVsbG8sIFdvcmxkIQ==
fmt
, _ := b64.StdEncoding.DecodeString(sEnc)
sDec.Println(string(sDec)) //Hello, World!
fmt
//使用 URL base64 格式进行编解码。
:= "abc123!?$*&()'-=@~ '"
data1 := b64.URLEncoding.EncodeToString([]byte(data1))
uEnc .Println(uEnc) //YWJjMTIzIT8kKiYoKSctPUB-
fmt, _ := b64.URLEncoding.DecodeString(uEnc)
uDec.Println(string(uDec)) //abc123!?$*&()'-=@~
fmt}
读文件,任何语言都差不多嘛。但是 io 毕竟是重头戏,肯定有很多的语法糖或者语言特性。下面的只是冰山一角。
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
//ReadFile
, err := os.ReadFile("./mypack.go")
dat(err)
check.Print(string(dat)) //输出文件内全部内容
fmt
, err := os.Open("./mypack.go")
f(err)
check
//Read
:= make([]byte, 5)
b1 , err := f.Read(b1)
n1(err)
check.Printf("%d bytes: %s\n", n1, string(b1[:n1]))
fmt//5 bytes: packa
//Seek
, err := f.Seek(6, io.SeekStart)
o2//io.SeekCurrent io.SeekEnd
(err)
check:= make([]byte, 2)
b2 , err := f.Read(b2)
n2(err)
check.Printf("%d bytes @ %d: %v", n2, o2, string(b2[:n2]))
fmt
//io.ReadAtLeast 用于从输入源中读取至少指定数量的字节数据
, err := f.Seek(6, io.SeekStart)
o3(err)
check:= make([]byte, 2)
b3
, err := io.ReadAtLeast(f, b3, 2)
n3(err)
check.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))
fmt
, err = f.Seek(0, io.SeekStart)
_(err)
check
//bufio.NewReader 创建一个带有缓冲的读取器,它可以用于提高文件或其他输入源的读取性能
:= bufio.NewReader(f) //缓冲区默认的大小是 4096 字节
r4 , err := r4.Peek(5) //取前5字节
b4(err)
check.Printf("5 bytes: %s\n", string(b4))
fmt.Close()
f}
//func NewReader(rd io.Reader) *Reader
//rd:实现了 io.Reader 接口的输入源,可以是文件、网络连接、字符串、字节数组等。
写文件和读文件类似,二者操作基本呈现对称。
package main
import (
"bufio"
"fmt"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
:= []byte("hello\ngo\n")
d1 //覆盖写
:= os.WriteFile("./tmp.txt", d1, 0644)
err (err)
check
//创建新文件
, err := os.Create("./tmp1.txt")
f(err)
check
defer f.Close() //推迟
:= []byte{115, 111, 109, 101, 10}
d2 , err := f.Write(d2)
n2(err)
check.Printf("wrote %d bytes\n", n2)
fmt//wrote 5 bytes
, err := f.WriteString("writes\n")
n3(err)
check.Printf("wrote %d bytes\n", n3)
fmt//wrote 7 bytes
.Sync() //同步,用于将文件的内存缓冲区的内容刷新到磁盘上的文件中
f
:= bufio.NewWriter(f)
w , err := w.WriteString("buffered\n")
n4(err)
check.Printf("wrote %d bytes\n", n4)
fmt//wrote 9 bytes
.Flush() //用于手动刷新缓冲区,将缓冲区中的数据写入底层的输出流(如文件、网络连接等
w}
行过滤器(line filter) 是一种常见的程序类型, 它读取 stdin 上的输入,对其进行处理,然后将处理结果打印到 stdout。 grep 和 sed 就是常见的行过滤器。
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
:= bufio.NewScanner(os.Stdin)
scanner //scanner.Scan将在回车时返回
for scanner.Scan() {
:= strings.ToUpper(scanner.Text())
ucl .Println(ucl)
fmt}
if err := scanner.Err(); err != nil {
.Fprintln(os.Stderr, "error: ", err)
fmt.Exit(1)
os}
}
检查 Scan 的错误。 文件结束符(EOF)是可以接受的,它不会被 Scan 当作一个错误。
filepath 包为 文件路径 ,提供了方便的跨操作系统的解析和构建函数; 比如:Linux 下的 dir/file 和 Windows 下的 dir。
其实 C++ filesystem 库现在也有这套东西了。
package main
import (
"fmt"
"path/filepath"
"strings"
)
func main() {
:= filepath.Join("dir1", "dir2", "filename")
p .Println("p:", p)
fmt//p: dir1\dir2\filename
.Println(filepath.Join("dir1//", "filename"))
fmt//dir1\filename
.Println(filepath.Join("dir1/../dir1", "filename"))
fmt//dir1\filename
// Dir 和 Base 可以被用于分割路径中的目录和文件。 此外,Split 可以一次调用返回上面两个函数的结果。
.Println("Dir(p):", filepath.Dir(p))
fmt//Dir(p): dir1\dir2
.Println("Base(p):", filepath.Base(p))
fmt//Base(p): filename
//判断是否为绝对路径
.Println(filepath.IsAbs("dir/file")) //false
fmt.Println(filepath.IsAbs("C:\\Users\\gaowanlu\\Desktop\\MyProject\\note\\testcode\\go"))
fmt//true
//文件扩展名
:= "config.json"
filename := filepath.Ext(filename)
ext .Println(ext) //.json
fmt.Println(strings.TrimSuffix(filename, ext)) //config
fmt
//Rel 寻找 basepath 与 targpath 之间的相对路径。 如果相对路径不存在,则返回错误。
, err := filepath.Rel("./tmp", "../build/")
relif err != nil {
panic(err)
}
.Println(rel) //..\..\build
fmt}
对于操作文件系统中的 目录 ,Go 提供了几个非常有用的函数。
package main
import (
"fmt"
"os"
"path/filepath"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
:= os.Mkdir("subdir", 0755) //在当前工作目录下,创建一个子目录。
err (err)
checkdefer os.RemoveAll("subdir") //类似于 rm -rf
// 一个用于创建临时文件的帮助函数
:= func(name string) {
createEmptyFile := []byte("")
d (os.WriteFile(name, d, 0644))
check}
("subdir/file1")
createEmptyFile
//类似于命令 mkdir -p
= os.MkdirAll("subdir/parent/child", 0755)
err (err)
check
//ReadDir 列出目录的内容,返回一个 os.DirEntry 类型的切片对象。
, err := os.ReadDir("subdir/parent")
c(err)
checkfor _, entry := range c {
.Println(" ", entry.Name(), entry.IsDir()) // child true
fmt}
//Chdir 可以修改当前工作目录,类似于 cd。
= os.Chdir("subdir/")
err (err)
check, err = os.ReadDir(".")
c(err)
checkfor _, entry := range c {
.Println(" ", entry.Name(), entry.IsDir()) // child true
fmt}
//遍历一个目录及其所有子目录。 Walk 接受一个路径和回调函数,用于处理访问到的每个目录和文件。
= filepath.Walk("../", visit)
err (err)
check}
func visit(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
.Println(" ", p, info.IsDir())
fmtreturn nil
}
在程序运行时,我们经常创建一些运行时用到,程序结束后就不再使用的数据。 临时目录和文件 对于上面的情况很有用,因为它不会随着时间的推移而污染文件系统。
package main
import (
"fmt"
"os"
"path/filepath"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
//默认临时文件目录创建
, err := os.CreateTemp("", "sample")
f(err)
check
.Println("Temp file name:", f.Name())
fmt//Temp file name: C:\Users\gaowanlu\AppData\Local\Temp\sample47593345
defer os.Remove(f.Name())
, err = f.Write([]byte{1, 2, 3, 4})
_(err)
check
, err := os.MkdirTemp("", "sampledir")
dname.Println("Temp dir name:", dname)
fmt//Temp dir name: C:\Users\gaowanlu\AppData\Local\Temp\sampledir3959527615
defer os.RemoveAll(dname)
:= filepath.Join(dname, "file1")
fname = os.WriteFile(fname, []byte{1, 2}, 0666)
err (err)
check}
单元测试是很重要的一部分。 testing 包为提供了编写单元测试所需的工具,写好单元测试后,我们可以通过 go test 命令运行测试。
实际上,单元测试的代码可以位于任何包下。 测试代码通常与需要被测试的代码位于同一个包下。
单元测试和基准测试,也算是专门的测试知识,尽量还是去单独学一下软件测试。不会也没关系,在服务端编程中有时写代码进行单元测试不常见,进行黑盒挺多的。
package main
import (
"fmt"
"testing"
)
func IntMin(a, b int) int {
if a < b {
return a
}
return b
}
func TestIntMinBasic(t *testing.T) {
:= IntMin(2, -2)
ans if ans != -2 {
//t.Error*会报告测试失败的信息,然后继续运行测试
.Errorf("IntMin(2, -2) = %d; want -2", ans)
t}
}
// 单元测试可以重复,所以会经常使用表驱动风格编写单元测试,
// 表中列出了输入数据,预期输出,使用循环,遍历并执行测试逻辑
func TestIntMinTableDriven(t *testing.T) {
var tests = []struct {
, b int
aint
want }{
{0, 1, 0},
{1, 0, 0},
{2, -2, -2},
{0, -1, -1},
{-1, 0, -1},
}
for _, tt := range tests {
:= fmt.Sprintf("%d,%d", tt.a, tt.b)
testname .Run(testname, func(t *testing.T) {
t:= IntMin(tt.a, tt.b)
ans if ans != tt.want {
.Errorf("got %d, want %d", ans, tt.want)
t}
})
}
}
// 基准测试通常在_test.go文件中,以Benchmark开头命名
// testing 运行器多次执行每个基准测试函数,并在每次运行时增加 b.N, 直到它收集到精确的测量值。
// 通常,基准测试运行一个函数,我们在一个 b.N 次的循环内进行基准测试。
func BenchmarkIntMin(b *testing.B) {
for i := 0; i < b.N; i++ {
(1, 2)
IntMin}
}
//$go test -v
//运行当前项目中的所有基准测试。所有测试都在基准测试之前运行。 bench 标志使用正则表达式过滤基准函数名称
//$go test -bench=.
命令行参数 是指定程序运行参数的一个常见方式。例如,go run hello.go, 程序 go 使用了 run 和 hello.go 两个参数。
package main
import (
"fmt"
"os"
)
func main() {
:= os.Args
argsWithProg := os.Args[1:]
argsWithoutProg .Println(argsWithProg)
fmt.Println(argsWithoutProg)
fmt}
/*
./main.exe a b c
[C:\Users\gaowanlu\Desktop\MyProject\note\testcode\go\main.exe a b c]
[a b c]
*/
命令行标志 是命令行程序指定选项的常用方式。例如,在 wc -l 中, 这个 -l 就是一个命令行标志。Go 提供了一个 flag 包,支持基本的命令行标志解析。
package main
import (
"flag"
"fmt"
)
func main() {
:= flag.String("word", "foo", "a string")
wordPtr
:= flag.Int("numb", 42, "an int")
numbPtr := flag.Bool("fork", false, "a bool")
forkPtr
var svar string
.StringVar(&svar, "svar", "bar", "a string var")
flag
.Parse()
flag
.Println("word:", *wordPtr)
fmt.Println("numb:", *numbPtr)
fmt.Println("fork:", *forkPtr)
fmt.Println("svar:", svar)
fmt.Println("tail:", flag.Args())
fmt}
/*
PS C:\Users\gaowanlu\Desktop\MyProject\note\testcode\go> ./main.exe --help
Usage of C:\Users\gaowanlu\Desktop\MyProject\note\testcode\go\main.exe:
-fork
a bool
-numb int
an int (default 42)
-svar string
a string var (default "bar")
-word string
a string (default "foo")
PS C:\Users\gaowanlu\Desktop\MyProject\note\testcode\go> ./main.exe -fork false -numb 666 -svar love -word two
word: foo
numb: 42
fork: true
svar: bar
tail: [false -numb 666 -svar love -word two]
*/
go 和 git 这种命令行工具,都有很多的 子命令 。 并且每个工具都有一套自己的 flag,比如: go build 和 go get 是 go 里面的两个不同的子命令。 flag 包让我们可以轻松的为工具定义简单的子命令。
package main
import (
"flag"
"fmt"
"os"
)
func main() {
//子命令 foo 可选参数 enable name
:= flag.NewFlagSet("foo", flag.ExitOnError)
fooCmd := fooCmd.Bool("enable", false, "enable")
fooEnable := fooCmd.String("name", "", "name")
fooName
//子命令 bar 可选参数 level
:= flag.NewFlagSet("bar", flag.ExitOnError)
barCmd := barCmd.Int("level", 0, "level")
barLevel
if len(os.Args) < 2 {
.Println("expected 'foo' or 'bar' subcommands")
fmt.Exit(1)
os}
switch os.Args[1] {
case "foo":
.Parse(os.Args[2:])
fooCmd.Println("subcommand 'foo'")
fmt.Println(" enable:", *fooEnable)
fmt.Println(" name:", *fooName)
fmt.Println(" tail:", fooCmd.Args())
fmtcase "bar":
.Parse(os.Args[2:])
barCmd.Println("subcommand 'bar'")
fmt.Println(" level:", *barLevel)
fmt.Println(" tail:", barCmd.Args())
fmtdefault:
.Println("expected 'foo' or 'bar' subcommands")
fmt.Exit(1)
os}
}
/*
PS C:\Users\gaowanlu\Desktop\MyProject\note\testcode\go> ./main.exe foo
subcommand 'foo'
enable: false
name:
tail: []
PS C:\Users\gaowanlu\Desktop\MyProject\note\testcode\go> ./main.exe bar -level 12
subcommand 'bar'
level: 12
tail: []
*/
环境变量 是一种向 Unix 程序传递配置信息的常见方式。 让我们来看看如何设置、获取以及列出环境变量。
package main
import (
"fmt"
"os"
"strings"
)
// 默认只在本程序运行期间有效
func main() {
.Setenv("FOO", "1")
os.Println("FOO:", os.Getenv("FOO")) //FOO: 1
fmt.Println("BAR", os.Getenv("BAR")) //BAR
fmtfor _, e := range os.Environ() {
:= strings.SplitN(e, "=", 2)
pair .Println(pair[0], "=", pair[1])
fmt}
}
Go 标准库的 net/http 包为 HTTP 客户端和服务端提供了出色的支持。
package main
import (
"bufio"
"fmt"
"net/http"
)
func main() {
, err := http.Get("http://gobyexample.com")
respif err != nil {
panic(err)
}
defer resp.Body.Close()
//Response status: 200 OK
.Println("Response status:", resp.Status)
fmt:= bufio.NewScanner(resp.Body)
scanner for scanner.Scan() {
.Println(scanner.Text())
fmt}
if err := scanner.Err(); err != nil {
panic(err)
}
}
/*
Response status: 200 OK
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go by Example</title>
...
*/
使用 net/http 包,我们可以轻松实现一个简单的 HTTP 服务器。
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, req *http.Request) {
.Fprintf(w, "hello\n")
fmt}
func headers(w http.ResponseWriter, req *http.Request) {
for name, headers := range req.Header {
for _, h := range headers {
.Fprintf(w, "%v: %v\n", name, h)
fmt}
}
}
func main() {
.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
http.ListenAndServe(":8090", nil)
http}
Go 语言的 context 包是用于在 Go 应用程序中传递取消信号、截止时间和跟踪请求范围的重要工具。context 包提供了一种可用于协调多个 goroutine 的方法,以便它们能够协同工作并共享信息,例如取消信号、超时、截止时间以及上下文值等。
context 包中最常用的类型是 context.Context,它是一个接口,用于包含与请求相关的信息。context.Context 类型可以通过 context.Background()或 context.TODO()来创建,通常用作根上下文。
上下文可以在 goroutine 之间传递,以便它们能够共享上下文的信息,包括取消信号、截止时间和上下文值。
通过 context.WithCancel()、context.WithTimeout()和 context.WithDeadline()等方法,可以创建一个带有取消信号的新上下文。当调用上下文的 cancel()函数时,所有依赖于该上下文的 goroutine 都会收到取消信号。
, cancel := context.WithCancel(context.Background())
ctxdefer cancel() // 始终确保在不再需要时调用cancel
可以使用 context.WithTimeout()和 context.WithDeadline()来为上下文设置截止时间。当截止时间到达时,上下文将被自动取消。
, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctxdefer cancel()
可以使用 context.WithValue()来为上下文添加键值对形式的上下文值。这些值可以在 goroutine 之间传递,但应谨慎使用,以避免滥用上下文来传递状态。
:= context.WithValue(context.Background(), key, value) ctx
在 goroutine 内,可以使用 ctx.Done()通道来检查取消信号,以确定是否应该退出。
select {
case <-ctx.Done():
// 上下文已取消,执行清理工作并退出
}
当启动新的 goroutine 时,通常将上下文作为参数传递给该 goroutine,以确保它们可以访问相同的上下文信息。
go func(ctx context.Context) {
// 在这里使用ctx
}(ctx)
context 包的主要目的是协调多个 goroutine 之间的行为,特别是在取消操作和截止时间方面。使用 context 可以帮助管理资源、避免资源泄漏以及实现更可靠的并发控制。在编写 Go 应用程序时,特别是涉及网络请求、并发任务或长时间运行的任务时,context 包是非常有用的工具。
package main
import (
"fmt"
"net/http"
"time"
)
func hello(w http.ResponseWriter, req *http.Request) {
:= req.Context()
ctx .Println("server: hello handler started")
fmtdefer fmt.Println("server: hello handler ended")
select {
case <-time.After(10 * time.Second):
.Fprintf(w, "hello\n") //请求10s后才会客户端内容
fmtcase <-ctx.Done():
//在10s内http请求取消了
:= ctx.Err()
err .Println("server:", err)
fmt:= http.StatusInternalServerError
internalError .Error(w, err.Error(), internalError)
http}
}
func main() {
.HandleFunc("/", hello)
http.ListenAndServe(":8080", nil)
http}
/*
server: hello handler started
server: context canceled
server: hello handler ended
*/
其他例子
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http, cancel := context.WithTimeout(context.Background(), 3*time.Second)
ctxdefer cancel() // 确保在函数退出时取消上下文
// 使用通道来传递结果
:= make(chan string)
resultChan
go func() {
// 模拟长时间运行的任务
.Sleep(5 * time.Second)
time<- "Task completed successfully"
resultChan }()
select {
case <-ctx.Done():
// 上下文已取消,返回错误信息
.Error(w, "Request canceled or timed out", http.StatusRequestTimeout)
httpreturn
case result := <-resultChan:
// 长时间运行的任务已完成
.Fprintln(w, result)
fmt}
})
.ListenAndServe(":8080", nil)
http}
有时,我们的 Go 程序需要生成其他的、非 Go 的进程。
package main
import (
"fmt"
"io"
"os/exec"
)
func main() {
:= exec.Command("cmd", "/C", "dir") //运行date
dateCmd , err := dateCmd.Output()
dateOutif err != nil {
panic(err)
}
.Println(string(dateOut))
fmt
//使用生成进程的标准输入与输出
:= exec.Command("cmd", "/C", "dir")
myCmd , _ := myCmd.StdinPipe()
In, _ := myCmd.StdoutPipe()
Out.Start()
myCmd.Write([]byte(""))
In.Close()
Out, _ := io.ReadAll(Out)
resBytes.Wait()
myCmd.Println(string(resBytes))
fmt
/*
在生成命令时,我们需要提供一个明确描述命令和参数的数组,而不能只传递一个命令行字符串。
如果你想使用一个字符串生成一个完整的命令,那么你可以使用 bash 命令的 -c 选项:
*/
:= exec.Command("bash", "-c", "ls -a -l -h")
lsCmd , err := lsCmd.Output()
lsOutif err != nil {
panic(err)
}
.Println("> ls -a -l -h")
fmt.Println(string(lsOut))
fmt}
只想用其它(也许是非 Go)的进程,来完全替代当前的 Go 进程。 这时,我们可以使用经典的 exec 函数的 Go 的实现。
Go 没有提供 Unix 经典的 fork 函数。一般来说,这没有问题,因为启动协程、生成进程和执行进程, 已经涵盖了 fork 的大多数使用场景。
package main
import (
"os"
"os/exec"
"syscall"
)
func main() {
, lookErr := exec.LookPath("ls") //应该是 /bin/ls
binaryif lookErr != nil {
panic(lookErr)
}
:= []string{"ls", "-a", "-l", "-h"}
args
:= os.Environ()
env
//如果这个调用成功,那么我们的进程将在这里结束,并被 /bin/ls -a -l -h 进程代替。 如果存在错误,那么我们将会得到一个返回值。
:= syscall.Exec(binary, args, env)
execErr if execErr != nil {
panic(execErr)
}
}
有时候,我们希望 Go 可以智能的处理 Unix 信号。 例如,我们希望当服务器接收到一个 SIGTERM 信号时,能够优雅退出, 或者一个命令行工具在接收到一个 SIGINT 信号时停止处理输入信息。 我们这里讲的就是在 Go 中如何使用通道来处理信号。
确实比 Linux C++处理信号优雅太多了,加上协程和通道的加持太棒了。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
:= make(chan os.Signal, 1)
sigs
.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
signal
:= make(chan bool, 1)
done
go func() {
:= <-sigs
sig .Println()
fmt.Println(sig)
fmt<- true
done }()
.Println("awaiting signal")
fmt<-done
.Println("exiting")
fmt}
使用 os.Exit 可以立即以给定的状态退出程序。C 中也有 exit。
不像例如 C 语言,Go 不使用在 main 中返回一个整数来指明退出状态。 如果你想以非零状态退出,那么你就要使用 os.Exit。
程序中的 ! 永远不会被打印出来。
package main
import (
"fmt"
"os"
)
func main() {
//当使用 os.Exit 时 defer 将不会 被执行, 所以这里的 fmt.Println 将永远不会被调用。
defer fmt.Println("!")
.Exit(3)
os}