前言

本文运用代码片段的办法来解释在go言语开发中常常遇到的小功用点,因为本人主要运用java开发,因而会与其作比较,希望对咱们有所帮助。

1. hello world

新手村的第一课,毋庸置疑。

package main
import "fmt"
func main() {
	fmt.Printf("hello world")
}

2. 隐形初始化

package main
import "fmt"
func main() {
	load()
}
func load() {
	fmt.Printf("初始化..手动%s 不错\n", "1")
}
func init() {
	fmt.Printf("隐形初始化。。\n")
}

在 go 中界说init函数,程序在运行时会自动履行。相似使junit@before注解。

3. 多模块的拜访

javapackage包的概念,go是经过文件夹 +package关键字来界说的。

一般而言,咱们会经过go init来创立项目,生成的go.mod文件坐落根目录。

常见的实践是,创立文件夹而且保持 package 称号与文件夹保持一致。这样import的永远是文件夹,遵循以上规矩则意味着文件夹的称号即为模块名。

同一个package能够创立多个.go文件,尽管分布在不同的文件中。可是他们中的办法称号不能相同。需求留意,这儿与java中不同类中办法能够重名不同。

此外,也没有比方private、protected、public等包拜访权限关键字。只要界说的函数首字母为大写。则能够被外部成功调用。

来看一下示例:

go-tour
└── ch3
    ├── model
    │   └── test
    │   │   ├── testNest.go
    │   └── helper.go
    │   └── helper2.go
    │  
    └── main.go           
    └── go.mod

此处,ch3、model、test均为文件夹,也能够说是packagehelper.go坐落model下,它的代码如下:

package model
import "fmt"
var AppName = "bot"
var appVersion = "1.0.0"
func Say() {
	fmt.Printf("%s", "hello")
}
func init() {
	fmt.Printf("%s,%s", AppName, appVersion)
}

再来看看main.go

package main
import (
	"ch3/model"
	"ch3/model/test"
)
func main() {
	model.Say()
}

明显它的调用是经过packageName.MethodName()来运用的。需求留意的是,一个go.mod下只能有一个main包。

4. 引证外部库

javamaven相似,go几经波折也提供了官方库房。如下,经过go get github.com/satori/go.uuid命令即可装置uuid库,未指定版别,因而下载的为最新版别。

运用时是这样的:

package main
import (
	"fmt"
	uuid "github.com/satori/go.uuid"
)
func main() {
	uuid := uuid.NewV4()
	fmt.Printf("%s", uuid)
}

5. 数组字典和循环

直接看代码便是了。

package main
import "fmt"
var item []int
var m = map[int]int{
	100: 1000,
}
var m2 = make(map[int]int)
func main() {
	for i := 0; i < 10; i++ {
		item = append(item, i)
		m[i] = i
		m2[i] = i
	}
	for i := range item {
		fmt.Printf("item vlaue=%d\n", i)
	}
	for key, value := range m {
		fmt.Printf("m:key=%d,value=%d\n", key, value)
	}
	for _, value := range m2 {
		fmt.Printf("m2:value=%d\n", value)
	}
}
  • := 的办法只能在办法内
  • 全局的只能用 var x=..
  • map输出没有顺序

6. 结构体和JSON

go中经过struct来界说结构体,你能够把它简略理解为目标。一般长这样。

type App struct {
	AppName    string
	AppVersion string `json:"app_version"`
	appAuthor  string "pleuvoir"
	DefaultD   string "default"
}

咱们常常在java程序中运用fastjson来输出JSON字符串go中自带了这样的类库。

package main
import (
	app2 "app/app" //能够界说别名
	"encoding/json"
	"fmt"
)
func main() {
	a := app2.App{}
	fmt.Printf("%s\n", a)
	app := app2.App{AppName: "bot", AppVersion: "1.0.1"}
	json, _ := json.Marshal(app) //转换为字符串
	fmt.Printf("json is %s\n", json)
}
  • 结构体中JSON序列化不会改变大小写,能够指定它输出的key称号经过 json:xxx 的描绘标签。
  • 结构体中的默认值赋值了也不展现

7. 反常处理

作为一个有经验的程序员:),go的反常处理触及的很简略,也往往为人所诟病。比方满屏幕的err运用。

package main
import (
	"fmt"
	"os"
)
func _readFile() (int, error) {
	file, err := os.ReadFile("test.txt")
	if err != nil {
		fmt.Printf("error is = %s\n", err)
		return 0, err
	}
	fmt.Printf("file = %s \n", file)
	return len(file), err
}
func readFile() (int, error) {
	fileLength, err := _readFile()
	if err != nil {
		fmt.Printf("反常,存在过错 %s\n", err)
	}
	return fileLength, err
}
func main() {
	fileLength, _ := readFile()
	fmt.Printf("%d\n", fileLength)
}

java不同,它支持多回来值,为咱们的运用带来了许多便利。假如不需求处理这个反常,能够运用_疏忽。

8. 异步

千呼万唤始出来,令人兴奋的异步。

package main
import (
	"bufio"
	"fmt"
	"os"
)
func worker() {
	for i := 0; i < 10; i++ {
		fmt.Printf("i=%d\n", i)
	}
}
func main() {
	go worker()
	go worker()
	//堵塞 获取控制台的输出
	reader := bufio.NewReader(os.Stdin)
	read, err := reader.ReadBytes('\n') //留意是单引号 回车后完毕控制台输出
	if err != nil {
		fmt.Printf("err is =%s\n", err)
		return
	}
	fmt.Printf("read is %s \n", read)
}

如此的高雅,如此的简略。只需求一个关键字go便能够发动一个协程。咱们在java中常常运用的是线程池,而在go中也存在协程池。据我调查,部分协程池benchmark的功用确实比官方言语关键字高许多。

9. 异步等候

这儿就相似java中运用countdownLatch等关键字空值并发编程中程序的等候问题。

package main
import (
	"fmt"
	"sync"
	"time"
)
func upload(waitGroup *sync.WaitGroup) {
	for i := 0; i < 5; i++ {
		fmt.Printf("正在上传 i=%d \n", i)
	}
	time.Sleep(5 * time.Second)
	waitGroup.Done()
}
func saveToDb() {
	fmt.Printf("保存到数据库中\n")
	time.Sleep(3 * time.Second)
}
func main() {
	begin := time.Now()
	fmt.Printf("程序开端 %s \n", begin.Format(time.RFC850))
	waitGroup := sync.WaitGroup{}
	waitGroup.Add(1)
	go upload(&waitGroup)
	go saveToDb()
	waitGroup.Wait()
	fmt.Printf("程序完毕 耗时 %d ms ", time.Now().UnixMilli()-begin.UnixMilli())
}

sync包相似于J.U.C包,里面能够找到许多并发编程的工具类。sync.WaitGroup便能够简简略单认为是countdownLatch吧。也不能多次调用变为负数,否则会报错。

留意,这儿需求传入指针,因为它不是一个引证类型。一定要经过指针传值,否则进程会进入死锁状态。

10. 管道

package main
import (
	"fmt"
	"sync"
)
var ch = make(chan int)
var sum = 0 //是线程安全的
func consumer(wg *sync.WaitGroup) {
	for {
		select {
		case num, ok := <-ch:
			if !ok {
				wg.Done()
				return
			}
			sum = sum + num
		}
	}
}
func producer() {
	for i := 0; i < 10_0000; i++ {
		ch <- i
	}
	close(ch) //假如不封闭则会死锁
}
func main() {
	wg := sync.WaitGroup{}
	wg.Add(1)
	go producer()
	go consumer(&wg)
	wg.Wait()
	fmt.Printf("sum = %d \n", sum)
}

这儿演示的是什么呢?管道相似一个行列,进行线程间数据的传递。当封闭时消费端也退出,假如没封闭管道,运行时会报死锁。能够看出全局变量在线程间是安全的。

能够衍生出一种固定写法:

//固定写法
func consumer(wg *sync.WaitGroup) {
	for {
		select {
		case num, ok := <-ch:
			if !ok {
				wg.Done()
				return
			}
			sum = sum + num
		}
	}
}

11. 接口

package main
import "fmt"
type Person interface {
	Say()
	SetName(name string)
}
type ZhangSan struct {
	Value string
}
func (z *ZhangSan) Say() {
	fmt.Printf("name=%s", z.Value)
}
func (z *ZhangSan) SetName(name string) {
	z.Value = name + ":hehe"
}
func main() {
	zhangSan := ZhangSan{}
	zhangSan.SetName("pleuvoir")
	zhangSan.Say()
}

如上的程序演示了接口的运用。

  • go的接口没有强依赖
  • 经过结构体 + 办法的办法实现,留意办法传入的能够是引证也能够是值

12. 锁

package main
import (
	"fmt"
	"sync"
)
type Number struct {
	Value int
	mutex sync.Mutex //加锁
}
func (receiver *Number) Add() {
	receiver.mutex.Lock()
	defer receiver.mutex.Unlock() //退出时会履行
	receiver.Value = receiver.Value + 1
	//fmt.Printf("add\n")
}
func (receiver *Number) Get() int {
	receiver.mutex.Lock()
	defer receiver.mutex.Unlock()
	return receiver.Value
}
func main() {
	number := Number{Value: 0}
	wg := sync.WaitGroup{}
	n := 100_0000
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func(wg *sync.WaitGroup) {
			number.Add()
			wg.Done()
		}(&wg)
	}
	wg.Wait()
	fmt.Printf("count=%d", number.Get())
}

这儿是什么?明显就像是显现锁的ReentrantLock的运用,相信咱们都能看懂。这儿呈现了新关键字defer,我暂且是理解为finally。不知道你怎么看?

13. 读写配置文件

这也是一个很常规的功用,看看怎么实现。

package main
import (
	"encoding/json"
	"fmt"
	"os"
)
type Preferences struct {
	Name    string  `json:"name"`
	Version float64 `json:"version"`
}
const configPath = "config.json"
func main() {
	preferences := Preferences{Name: "app", Version: 100.01}
	marshal, err := json.Marshal(preferences)
	err = os.WriteFile(configPath, marshal, 777)
	if err != nil {
		fmt.Printf("写入配置文件过错,%s\n", err)
		return
	}
	//读取配置文件
	file, err := os.ReadFile(configPath)
	if err != nil {
		fmt.Printf("读取文件过错,%s\n", err)
		return
	}
	fmt.Printf("%s\n", file) //{"name":"app","version":100.01}
	//构建一个目标用来序列化
	readConfig := Preferences{}
	//反序列化
	err = json.Unmarshal(file, &readConfig)
	if err != nil {
		fmt.Printf("配置文件转换为JSON过错,%s\n", err)
	}
	fmt.Printf("%v", readConfig) //{app 100.01}

这儿挺没意思的,写入JSON字符串,然后读取回来在加载到内存中。不过,简略的示例也够说明问题了。

14. 宕机处理

这是相似于一种最上层反常捕获的机制,在程序的入口处捕获所有的反常。

package main
import (
	"fmt"
	"time"
)
func worker() {
	//defer func() {  //不能写在主函数,最外层catch没啥用
	//	if err := recover(); err != nil {
	//		fmt.Printf("%s", err)
	//	}
	//}()
	defer recovery()
	panic("严重过错")
}
func recovery() {
	if err := recover(); err != nil {
		fmt.Printf("死机了。%s\n", err)
	}
}
func main() {
	for true {
		worker()
		time.Sleep(1 * time.Second)
	}
}

注释写的很清楚,聪明的你一看就懂。

15. 单元测试

java不同,go主张单元测试文件尽可能的离源代码文件近一些。比方这样:

go-tour
    └── main.go      
    └── main_test.go  

而且它的命名也是这样简略粗犷:

package main
import (
	"testing"
)
func TestInit(t *testing.T) {
	t.Log("heh")
	helper := PersonHelper{}
	helper.init("pleuvoir")
	t.Log(helper.Name)
}

以大写的Test开头,文件称号以_test完毕,很清爽的感觉。

16. 发动传参

这也是一个很常用的知识点。这儿有两种办法:

  • 直接传
  • 运用 flag
package main
import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
)
func main() {
	//第一种办法
	args := os.Args
	for i, arg := range args {
		println(i, arg)
	}
	//第二种办法
	config := struct {
		Debug bool
		Port  int
	}{}
	flag.BoolVar(&config.Debug, "debug", true, "是否敞开debug模式")
	flag.IntVar(&config.Port, "port", 80, "端口")
	flag.Parse()
	json, _ := json.Marshal(config)
	fmt.Printf("json is %s\n", json)
}

我主张运用第二种,更便捷自带类型转换,还能够给默认值,非常好。

17. 高雅退出


package main
import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)
func quit() {
	println("履行一些整理工作。。")
}
//正常的退出
//终端 CTRL+C退出
//反常退出
func main() {
	defer quit()
	println("进来了")
	//读取信号,没有一向会堵塞住
	exitChan := make(chan os.Signal)
	//监听信号
	signals := make(chan os.Signal)
	signal.Notify(signals, syscall.SIGINT, syscall.SIGQUIT)
	go func() {
		//有可能一次接收到多个
		for s := range signals {
			switch s {
			case syscall.SIGINT, syscall.SIGQUIT:
				println("\n监听到操作系统信号。。")
				quit() //假如监听到这个信号没处理,那么程序就不会退出了
				if i, ok := s.(syscall.Signal); ok {
					value := int(i)
					fmt.Printf("是信号类型,准备退出 %d", value)
				} else {
					println("不知道是啥,0退出")
					os.Exit(0)
				}
				//	os.Exit(value)
				exitChan <- s
			}
		}
	}()
	println("\n程序在这儿被堵塞了。")
	<-exitChan
	//panic("heh")
	println("\n堵塞被终止了。")
}

这其实是在监听操作系统的信号,java中也有相似的回调的接口(我忘了名字)。

18. 反射

作为一门高级言语,反射肯定是有的。还是运用reflect包。

package main
import (
	"fmt"
	"reflect"
)
type Person struct {
	Name string `json:"name"`
}
func (p *Person) SetName(name string) {
	p.Name = name
}
func (p *Person) GetName() (string, string) {
	return p.Name, "1.0.1"
}
func worker1() {
	p := Person{}
	p.SetName("pleuvoir")
	name, _ := p.GetName()
	fmt.Printf(name)
}
// 获取办法
func worker2() {
	p := Person{}
	rv := reflect.ValueOf(&p)
	value := []reflect.Value{reflect.ValueOf("peluvoir")}
	rv.MethodByName("SetName").Call(value)
	values := rv.MethodByName("GetName").Call(nil)
	for i, v := range values {
		fmt.Printf("\ni=%d,value=%s\n", i, v)
	}
}
func worker3() {
	s := Person{}
	rt := reflect.TypeOf(s)
	if field, ok := rt.FieldByName("Name"); ok {
		tag := field.Tag.Get("json")
		fmt.Printf("tag is %s \n", tag)
	}
}
func main() {
	//正常获取
	worker1()
	//获取办法
	worker2()
	//获取标签
	worker3()
}

没什么好说的,写代码全靠猜。

19. atomic

相似java中的atomic原子变量。

package main
import (
	"fmt"
	"sync"
	"sync/atomic"
)
func main() {
	workers := 1000
	wg := sync.WaitGroup{}
	wg.Add(workers)
	for i := 0; i < workers; i++ {
		go worker2(&wg)
	}
	wg.Wait()
	fmt.Printf("count = %d", count)
}
var count int64 = 0
func worker1(wg *sync.WaitGroup) {
	count++
	wg.Done()
}
func worker2(wg *sync.WaitGroup) {
	atomic.AddInt64(&count, 1) //特别简略
	wg.Done()
}

真的是特别简略。

20. 线程安全的Map

相似于ConcurrentHashMap,与普通的api有所不同。

var sessions = sync.Map{}
sessions.Store(uuid, uuid)
load, ok := sessions.Load(value.Token)
		if ok {
			// 做你想做的工作
		}

21. return func

这儿便是函数式变成的比方了。函数是一等公民能够作为参数随意传递。java什么时候能支持呢?


package main
import "fmt"
func main() {
	engine := Engine{}
	engine.Function = regular()
	function := engine.Function
	for i := 0; i < 3; i++ {
		s := function("pleuvoir")
		fmt.Printf("s is %s\n", s)
	}
}
type Engine struct {
	Function func(name string) string
}
func regular() (ret func(name string) string) {
	fmt.Printf("初始化一些东西。\n")
	return func(name string) string {
		fmt.Printf("我是worker。name is %s\n", name)
		return "我是匿名函数的回来值"
	}
}

比方这儿,假如要初始化日志什么。最终需求让结构在哪里打印日志,就需求将这个初始化的日志实例传递过去。总而言之,言而总之。会需求让代码各种传递。

这种办法在于第一次调用的时候会履行上面的代码片段,后边只是保存了这个函数的句柄,然后能够一向调用这个匿名函数。

22. context

package main
import (
	"context"
	"fmt"
	"time"
)
func main() {
	worker1()
}
func worker1() {
	//总共2秒超时
	value := context.WithValue(context.Background(), "token", "pleuvoir")
	timeout, cancelFunc := context.WithTimeout(value, 5*time.Second)
	defer cancelFunc()
	//模仿使命
	fmt.Println("开端使命")
	deep := 10
	go handler(timeout, deep)
	fmt.Println("开端堵塞", time.Now())
	//等候主线程超时,堵塞操作
	select {
	case <-timeout.Done():
		fmt.Println("堵塞完毕", timeout.Err(), time.Now())
	}
}
// 模仿使命处理,循环下载图片等
func handler(timeout context.Context, deep int) {
	if deep > 0 {
		fmt.Printf("[begin]token is %s %s deep=%d\n", timeout.Value("token"), time.Now(), deep)
		time.Sleep(1 * time.Second)
		go handler(timeout, deep-1)
	}
	//下面的哪个先回来 先履行哪个
	//假如全体超时 或者 当时办法超越2秒 就完毕
	select {
	//等候超时会回来
	case <-timeout.Done():
		fmt.Println("超时了。", timeout.Err())
		//等候这么久 然后会回来 这个函数可不是比较时刻,这儿其实是在模仿处理使命,固定履行一秒 和歇息一秒效果一样
		//可是歇息一秒的话就不会实时回来了,所以这儿实际应用能够是一个带超时的回调?
	case <-time.After(time.Second):
		fmt.Printf("[ end ]履行完结耗时一秒     %s %d\n", time.Now(), deep)
	}
}

效果:在不同的协程中传递上下文。

  • 传值 相似于threadLocal
  • 能够运用超时机制,无论往下传递了多少协程,只要最上层时刻到了 后边的都不履行
  • 俄罗斯套娃一次一层包装

23. 字符串处理

这是最高频率的操作了,运用任何言语都无法错失。

package main
import (
	"fmt"
	"strings"
)
func main() {
	str := " pleuvoir  "
	trimSpace := strings.TrimSpace(str)
	fmt.Printf("去除空格 %s\n", trimSpace)
	subString := trimSpace[4:len(trimSpace)]
	fmt.Printf("subString after is %s\n", subString)
	prefix := strings.HasPrefix(subString, "vo")
	fmt.Printf("是否有前缀 vo : %v\n", prefix)
	suffix := strings.HasSuffix(subString, "ir")
	fmt.Printf("是否有后缀 ir : %v\n", suffix)
	builder := strings.Builder{}
	builder.WriteString("hello")
	builder.WriteString(" ")
	builder.WriteString("world")
	fmt.Printf("stringBuilder append is %s\n", builder.String())
	eles := []string{"1", "2"}
	join := strings.Join(eles, "@")
	fmt.Printf("join after is %s\n", join)
	//拼接格式化字符串,而且能回来
	sprintf := fmt.Sprintf("%s@%s", "1", "20")
	fmt.Printf("Sprintf after is %s\n", sprintf)
	//打印一个目标 比较明晰的办法
	person := struct {
		Name string
		Age  int
	}{"pleuvoir", 18}
	fmt.Printf("%v", person) // 输出 {Name:pleuvoir Age:18}
}

主要是运用fmt包。

24. 使命投递

假如说运用go最激动人心的是什么?是许多的协程。假如在下载使命中,咱们能够发动许多协程进行分片下载。如下,即展现运用多路复用高速下载。

package main
import (
	"fmt"
	"sync"
	"time"
)
func main() {
	chunks := 10 //文件分成n份
	workers := 5 //个线程处理
	wg := sync.WaitGroup{}
	wg.Add(chunks)
	jobs := make(chan int, chunks) //带缓冲的管道 等于使命数
	for i := 0; i < workers; i++ {
		go handler1(i, jobs, &wg)
	}
	//将使命悉数投递给worker
	scheduler(jobs, chunks)
	wg.Wait()
	fmt.Println("download finished .")
}
// 分成 chunks 份使命 里分发
// 将 n 份下载使命都到管道中去,这儿管道数量等于 使命数量n 管道不会堵塞
func scheduler(jobs chan int, chunks int) {
	for i := 0; i < chunks; i++ {
		//time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
		jobs <- i
	}
}
// 写法2
// 留意这儿的是直接承受管道,这也是一种固定写法,下面的 range jobs 能够认为是堵塞去抢这个使命,多个线程都在抢使命
func handler2(workerId int, jobs <-chan int, wg *sync.WaitGroup) {
	for job := range jobs {
		//	fmt.Printf("workerId[%d] job[%d] start download .\n", workerId, job)
		time.Sleep(1 * time.Second)
		fmt.Printf("workerId[%d] job[%d] download ok.\n", workerId, job)
		wg.Done() //这儿不要break,这样履行完当时的线程就能持续抢了
	}
}
// 写法1,select case 多路复用
func handler1(workerId int, jobs chan int, wg *sync.WaitGroup) {
	for {
		select {
		case job, _ := <-jobs:
			//	fmt.Printf("workerId[%d] job[%d] start download .\n", workerId, job)
			time.Sleep(3 * time.Second)
			fmt.Printf("workerId[%d] job[%d] download ok.\n", workerId, job)
			wg.Done() //这儿不要break,这样履行完当时的线程就能持续抢了
		}
	}
}

后语

以上都是一个新手Gopher的经验总结,文中不免有过错,恳请纠正。

作者:京东零售 付伟

来源:京东与开发者社区