1小时快速了解Go语言(写给打算转职golang的程序员)

本文章也有对应的视频讲解:1小时快速了解Go语言

开发环境搭建

Go语言下载地址:https://go.dev/dl,Windows、Mac、Linux都支持,Windows和Mac下载后直接双击安装即可,Linux下载后解压到任意目录都可以,Linux需要手动设置环境变量GOROOT/GOPATH/PATH。
IDE使用Vscode即可,下载地址:https://code.visualstudio.com/Download,在vscode里安装一个扩展,名叫“Go”,出自Go Team at Google。
写第一段Go代码:

package main

import "fmt"

func main() {
	var a float64
	fmt.Printf("the value of a is %f\n", a)
}

保存上述go代码,文件名必须以.go结尾,运行go代码:

go run xxx.go

main()函数是程序的唯一入口,且main()函数必须位于package main中,一个目录下只能存在一个main()函数。
import里指明需要引用的其他package。
Printf()是格式化输出函数,其位于fmt这个package下,%f是浮点数的占位符。
在Go语言里左大括号{统一置于行尾,不能另起一行书写。

基础数据类型

变量声明:

var a int   // 此时a=0

任何时候,Go语言总是变量名在前,类型在后。其他常用的基础数据类型还有int32、int64、float32、float64、string、bool。
变量初始化,即第一次给变量赋值:

var a int = 1
var a = 1   // 根据值推断类型
a := 1      // 省略var,用:代替

修改变量的值:

a = 2

强制类型转换:

var a float64
var b int64
var c int
c = int(b)          // int64不会自动转为int
a = float64(c)      // int不会自动转为float64

指针:

a := 3
var b *int = &a     // &是取址符号,*int表示指向int的指针类型
fmt.Printf("a的地址是%p\n", b)      // 格式化输出地址使用%p
var c int = *b + 7      // 指针前面加个*,把指针类型转为原始的数据类型
fmt.Printf("a+7=%d\n", c)

函数

func swap(a int, b int) (c int, d int){
    c, d = b, a
    return
}

func关键字表示后面要定义一个函数。
函数可以有0个或多个参数,这里指a和b。
函数可以有0个或多个返回值,这里指c和d。

package main

import (
	"fmt"
	"time"
)

func work() {
	begin := time.Now() //当前时间
	defer func() {      //defer后面跟匿名函数
		diff := time.Now().Sub(begin) //计算时间差
		fmt.Printf("用时%d毫秒\n", diff.Milliseconds())
	}()     //小括号里用于传递参数值,匿名函数参数为空,所以这里没有传值
	time.Sleep(2 * time.Second) //休眠2秒钟
}

defer后面跟的代码或匿名函数,在主函数(这里指work())临退出之前才执行。

结构体

定义结构体:

type User struct{
    name string
    age int
}

创建结构体实例:

u1 := User{name: "大乔乔", age: 18}
u2 := User{age: 18}     // name为空字符串
u3 := User{}            // name为空字符串,age为0

读写结构体的成员变量:

u3.age = 28
var a int = u1.age + u2.age

结构体可以拥有0个或多个成员方法:

func (u User) GetAge() int {
    return u.age
}
func (u *User) SetAge(a int) {
    u.age = a
}

成员方法其实等价于普通函数:

// 函数参数传递的是User的拷贝
func GetAge(u User) int {
    return u.age
}
// 函数参数传递的是User的指针
func SetAge(u *User, a int) {
    u.age = a
}

调用结构体的成员方法:

u3.SetAge(u2.GetAge())

Go语言里不存在静态成员,所以成员变量和成员方法只能通过结构体的实例来访问。
Go语言不存在继承,但是可以把一个结构体作为另一个结构体的成员变量的类型。

type Leader struct{
    TeamSize int
    human User
}
var l Leader    // 声明结构体变量后,其成员变量均为空值
l.human = User{name: "大乔乔", age: 18}
l.human.SetAge(l.human.GetAge()+10)

接口

接口是一组方法的集合。

type Animal interface{
    GetAge() int
    SetAge(int)
}

凡是具备GetAge和SetAge这两个方法的结构体都实现了Animal接口,比如上文的User结构体,即可以认为User是一种Animal。

var a Animal
a = u1

也就是说GetAge和SetAge是成为Animal的准入条件,那如果一个接口里没有任何方法(空接口),就可以认为任意类型都实现了这个接口。Go语言自带了一个空接口类型--any,任何数据类型都属于一种any。

type any interface{}
var b any
b = u1
b = false 
b = 9
b = "golang"

结构体实现接口,不需要在定义结构体时显式使用implements等关键字。
函数参数、结构体的成员变量都可以是接口类型。

func getAge(a Animal) int{
    return a.GetAge() + 10
}

type Arm struct{
    Member Animal
    country string
}

func (arm Arm) GetAge () int {
    return arm.Member.GetAge() + 10
}

引用数据类型

切片

切片的定义

type slice struct { 
    array unsafe.Pointer    // 底层数组的指针
    len int 
    cap int 
}

由于切片持有一个数组的指针,我们称切片“引用”了一个数组,所以切片是一种引用数据类型。

arr := make([]int, 3, 5)       // 切片指向一个int数组,长度为3,容量为5
arr[0], arr[1], arr[2] = 2, 9, 7    // 给切片的3个元素赋值
brr := arr      // 切片的赋值拷贝

注意brr:=arr,go语言里的所有等号赋值都会发生拷贝,由于切片是一个包含3个成员变量的结构体,所以切片的拷贝实际上拷贝的是那3个成员变量,也就是说并没有拷贝底层数组,而只是拷贝了底层数组的指针。
brr[0]=3把底层数组的首元素修改成了3,此时arr[0]也变成了3。
切片的长度可以增加:

arr = append(arr, 5)    // arr的长度为4,brr的长度还是3,即brr[3]会发生数组越界,而arr[3]没问题

新元素5追加到7后面,即放到预分配内存里。注意append函数并不会修改切片的长度,只是它会返回一个新切片,新切片的长度增加了。
通过多次append如果预分配内存填满了,则go会自动申请一块更大的底层数组,把老数组的元素逐一拷贝到新数组里去。

arr = append(arr, 5)    // arr的长度为5,brr的长度还是3。注意此时预分配内存已填满
arr = append(arr, 5)    // arr的长度为6,容量为10。arr指向新的更大的数组,brr还指向老数组

切片的遍历有两种方式:

var sum int   // 此时sum的值是0
for i:=0;i<len(arr);i++{
    sum += arr[i]
}

sum = 0
for i,ele := range arr{
    _ = i   // 变量i声明了就必须使用,这里把它赋给_,_相当于占位符,_无法被读取
    sum += ele
}

fmt.Println(sum)

map

map也是一种引用数据类型,它引用的是一个哈希表HashTable。
map的声明和赋值:

var m map[int]string        //声明map变量,key是int类型,value是string类型
m := map[int]string{3:"abc", 7:"mmj"}   //声明并初始化
m[5]="hsi"      //往map里添加一对key-value,也可能是更新key对应的value
delete(m, 3)    //从map里删除key及其对于的value

map的遍历:

for key, value := range m{
    fmt.Printf("key=%d, value=%s\n", key, value)
}

channel

channel也是一种引用数据类型,它引用的是一个环形队列,队列意味着先进先出。
声明并初始化channel变量:

ch := make(chan int, 10)   //队列里存在int类型的元素,队列容量为10

读写channel:

ch <- 1     //向队列尾部添加元素1。如果队列已满,则此操作会阻塞
a := <-ch   //从队列首部取走一个元素,并赋给变量a。如果队列已空,则此操作会阻塞

遍历channel:

close(ch)
for ele := range ch{
    fmt.Println(ele)
}

注意遍历channel之前一定要先close它,表示不允许再向队列里写入元素,否则遍历channel的for循环可能一直无法退出甚至发生死锁。

并发

Go语言的并发性能非常优异,Go语言使用协程而非线程并发执行任务,协程比线程占用的内存空间更小,协程的创建、销毁、切换成本也更低。
go语言启动一个协程非常方便,应该是所有语言里面最方便的:

// 没有返回值的函数
func work(arg string){
    // do something
}

go work("abc")  //启动一个协程,异步地去执行work函数

// 或者直接在go关键字后面跟匿名函数
go func(arg string){
    // do something
}("abc")

其它语言通过类似于join的函数等待线程结束,go语言里等待一个协程运行结束会比较麻烦。

package main

import (
	"fmt"
	"time"
)

func main() {
	var result int
	ch := make(chan bool)
	go func() { //启动子协程
		time.Sleep(2 * time.Second)
		result = 5 //go语言不能获得子协程的返回值,只能通过一个公共变量来传递结果
		ch <- true
	}()
	<-ch //channel为空,读操作阻塞。子协程结束后写channel,此时读channel才解除阻塞,从而实现了main协程等子协程结束
	fmt.Println(result)
}

文件读写

// 读取一个文件的全部内容,写入另外一个文件
func CopyFile() {
	fin, err := os.Open("a.go") //打开文件,准备读它
	if err != nil {
		log.Fatalf("打开文件a.go失败: %v\n", err)
	}
	defer fin.Close() //用完之后,记得关闭文件

	fout, err := os.OpenFile("b.go", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) //打开文件,准备读它
	if err != nil {
		log.Fatalf("打开文件b.go失败: %v\n", err)
	}
	defer fout.Close() //用完之后,记得关闭文件

	buffer := make([]byte, 1024) //创建长度为容量均为1024的byte切片,作为数据中转站
	for {                        //相当于while true,go语言里没的while关键字
		n, err := fin.Read(buffer) //从文件中读取1024个字节,放入buffer,n是实际读出的字节数,仅最后一次Read会发生n<1024的情况
		if err != nil {
			if err == io.EOF { //EOF(End Of File)表示来到文件末尾
				if n > 0 {
					fout.Write(buffer[0:n]) //[0:n]表示截取切片的前n个元素
				}
			} else {
				fmt.Printf("读文件异常: %v\n", err)
			}
			break
		} else {
			fout.Write(buffer[0:n]) //把中转站里的内容写到一个文件里去
		}
	}
}

依赖管理

包package是go语言里面组织代码的最小单元,注意不是go文件,通常一个目录下的所有go文件的package必须一致,除非另一个package名以_test结尾。一个包下的所有go文件直接合并到一个go文件里(仅删除第一行的package xxx)不会有任何语法错误,这也意味着一个包内不能定义同名的全局变量、结构体、接口、函数(init()函数除外)。
若干个包组成一个模块module,module文件在go.mod文件里指定。go.mod里同时指定了go的版本号,以及本module依赖的所有其他module。
以大写开头的全局变量(即函数外的变量)、函数、结构体、结构体的成员变量、结构体的成员方法、接口可以在其他包中使用,称为“可导出”,类似于其他语言里public的概念。
/--project_root_path
  |
  /--dir1
  |  |
  |  /--a.go
  |
  /--dir2
  |  |
  |  /--b.go
  |
  /--go.mod

go.mod

module dqq/1h

go 1.22.0

通过go mod init dqq/1h命令可以自动生成上述文件。
a.go

package util 

// 可导出函数
func GetAge() int {
	return 1
}

b.go

package main 

import "dqq/1h/dir1"   // {module}/{path}

func main(){
	util.GetAge()    // 调用函数时需要在前面加个包名。如果是访问本包内的函数、变量不加包名
}

如果import的内容不是go语言标准库里的包(即不在$GOROOT/src目录下),且module名称也不是当前module,则会去$GOPATH/pkg/mod目录下去搜寻相应的module。通过go get $module_name命令会去互联网上下载对应module的源代码,放到本地的$GOPATH/pkg/mod目录下,并添加到go.mod文件的require条目里。
一个包下可以有多个init()函数,当import这个包时会去执行该包下的所有init()函数,执行main()函数之前会执行执行package main里的所有init()函数。

数据库编程

go语言官方只写了读写数据库的接口,并没有写任何具体的实现。所以针对各种数据库的读写只能依赖第三方库,而很多第三方库并没有按照go官方的接口来实现。
github.com/go-sql-driver/mysql按照go官方接口的约定,实现了针对mysql的读写。
先下载这个库:

go get github.com/go-sql-driver/mysql

package main

import (
	"database/sql"
	"fmt"
	"os"
	"time"

	_ "github.com/go-sql-driver/mysql"     // 注册接口实现
)

func CheckError(err error) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "fatal error: %s\n", err.Error()) //stdout是行缓冲的,他的输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕。而stderr是无缓冲的,会直接输出
		os.Exit(1)
	}
}

func main() {
	//DSN(data source name)格式:[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
	db, err := sql.Open("mysql", "tester:123456@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai")
	CheckError(err)
	defer db.Close()
	rows, err := db.Query("select id,name,city,score,enrollment from student where enrollment>=20220703 limit 5") //查询得分大于2的记录
	CheckError(err)
	defer rows.Close()
	for rows.Next() { //没有数据或发生error时返回false
		var id int
		var score float32
		var name, city string
		var enrollment time.Time
		err = rows.Scan(&id, &name, &city, &score, &enrollment) //通过scan把db里的数据赋给go变量
		CheckError(err)
		fmt.Printf("id=%d, score=%.2f, name=%s, city=%s, enrollment=%s \n", id, score, name, city, enrollment.Format("2006-01-02 15:04:05"))
	}
}

http编程

package main

import (
	"net/http"
)

func home(w http.ResponseWriter, request *http.Request) {
	// 从request里可以取得http请求体和请求头
	w.Header().Set("key", "value") //先设置响应头
	w.Write([]byte("Welcome"))     //后设置响应体

}

func main() {
	http.HandleFunc("/", home)                                         //路由,请求要目录时去执行HelloHandler
	if err := http.ListenAndServe("127.0.0.1:5678", nil); err != nil { //ListenAndServe如果不发生error会一直阻塞。为每一个请求单独创建一个协程去处理
		panic(err) //打印错误信息和调用堆栈,以状态2结束进程
	}
}

打开浏览器,在地址栏输入http://localhost:5678/,查看响应体和响应头。

grpc编程

首先需要编写proto文件。

echo.proto

syntax="proto3";

option go_package = "./idl;idl";  //分号前是go文件的输出目录,分号后是go文件的package名

message Content {
    string data = 1;
}

service EchoService {
    rpc Echo(Content) returns (Content);
}

通过以下命令将.proto文件转为go文件。

protoc --go_out=. --go-grpc_out=. --proto_path=. echo.proto

服务端代码:

package main

import (
	"context"
	"dqq/1h/idl"
	"net"

	"google.golang.org/grpc"
)

type MyEcho struct {
	idl.UnimplementedEchoServiceServer //EchoServiceServer接口要求所有的实现必须内嵌一个UnimplementedEchoServiceServer,用于将来的扩展
}

func (s MyEcho) Echo(ctx context.Context, request *idl.Content) (*idl.Content, error) {
	if request == nil {
		return nil, nil
	}
	return &idl.Content{Data: request.Data}, nil //返回结构体指针,加个&符号。error是个接口,可以用nil赋值
}

func main() {
	// 监听本地的5678端口
	lis, err := net.Listen("tcp", "127.0.0.1:5678")
	if err != nil {
		panic(err)
	}
	//创建服务
	server := grpc.NewServer()
	// 注册服务的具体实现
	idl.RegisterEchoServiceServer(server, &MyEcho{})
	// 启动服务
	err = server.Serve(lis)
	if err != nil {
		panic(err)
	}
}

客户端代码:

package main

import (
	"context"
	"dqq/1h/idl"
	"fmt"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	//连接到服务端
	conn, err := grpc.NewClient(
		"127.0.0.1:5678",
		grpc.WithTransportCredentials(insecure.NewCredentials()), //身份认证Credential即使为空,也必须设置
	)
	if err != nil {
		fmt.Printf("dial failed: %s", err)
		return
	}
	//创建client
	client := idl.NewEchoServiceClient(conn)
	request := &idl.Content{Data: "大乔乔"}                    //构造请求
	resp, err := client.Echo(context.Background(), request) //发起grpc调用
	if err != nil {
		fmt.Printf("rpc failed: %v\n", err)
	} else {
		fmt.Println(resp.Data) //打印调用结果
	}
}

进阶内容

enjoy your golang travels! 如需快速、高效、深入的学习Go语言,欢迎试听我录制的golang视频课程《golang从入门到通天》(电脑端按Ctrl++放大页面观看)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值