Java程序员Go语言入门简介

引用:Java程序员Go语言入门简介

为什么是 Go 语言

  • 类 C 的语法,这意味着 Java、C#、JavaScript 程序员能很快的上手

  • 有自己的垃圾回收机制

  • 跨平台、编译即可执行无需安装依赖环境

  • 支持反射

Go 语言简介

Go 语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是 “兼具 Python 等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。

数据类型

数据类型 说明
bool 布尔
string 字符串
int uint8,uint16,uint32,uint64,int8,int16,int32,int64
float float32,float64
byte byte

参考:https://www.runoob.com/go/go-data-types.html

第一章 ·基本语法

HelloWorld

在线运行示例:https://play.golang.org/p/-4RylAqUV36

package main
​
import "fmt"
​
var name string
​
func init() {
  name = "world"
}
​
func main() {
  fmt.Println("hello " + name)
}

执行一下:

$ go run main.go # main.go 为刚刚创建的那个文件的名称
$ hello world
  1. 命令来运行程序:        go run 命令已经包含了编译和运行。它使用一个临时目录来构建程序,执行完然后清理掉临时目录。你可以执行以下命令来查看临时文件的位置:

go run main.go
// 查看临时文件的位置
go run --work main.go
  1. 明确要编译代码,使用 go build:        这将产生一个可执行文件,名为 main ,你可以执行该文件。如果是在 Linux / OSX 系统中,别忘了使用 ./ 前缀来执行,也就是输入 ./main 。        在开发中,你既可以使用 go run 也可以使用 go build 。但当你正式部署代码的时候,你应该部署通过 go build 产生的二进制文件并且执行它。

go build main.go

变量

变量声明

  1. 初始化一个变量时,请使用:var NAME TYPE;

  2. 给变量申明及赋值时,请使用: NAME := VALUE ;

  3. 给之前已经申明过的变量赋值时,请使用: NAME = VALUE

在线运行示例:https://play.golang.org/p/zPqCkRZgrgp

package main
​
import (
    "fmt"
)
​
func main() {
    var name string   // 声明
    name = "gaoyoubo" // 赋值
    fmt.Println(name)
​
    var age int = 18 // 声明并赋值
    fmt.Println(age)
}

类型推断

在线运行示例:https://play.golang.org/p/0My8veBvtJ8

package main
​
import (
    "fmt"
)
​
func main() {
    name := "gaoyoubo"
    fmt.Println(name)
    //短变量声明运算符 :=
    age := 18
    fmt.Println(age)
}

导入包

Go 有很多内建函数,例如 println,可以在没有引用情况下直接使用。但是,如果不使用 Go 的标准库直接使用第三方库,我们就无法走的更远。import 关键字被用于去声明文件中代码要使用的包。

package main
​
// Go 的两个标准包:fmt 和 os 
import (
  "fmt"
  "os"
)
​
func main() {
  if len(os.Args) != 2 {
    os.Exit(1)
  }
  fmt.Println("It's over", os.Args[1])
}

Go 的标准库文档:

  1. https://golang.org/pkg/fmt/#Println 去看更多关于 PrintLn 函数的信息;

  2. 没有互联网,你可以这样在本地获取文档,使用以下go命令:

godoc -http=:6060
//然后浏览器中访问 http://localhost:6060

函数

  • 函数可以有多个返回值

  • 隐式的指定函数是 private 还是 public,函数首字母大写的为 public、小写的为 private

  • 没有类似 Java 中的try cache、throw,Go 语言是通过将error作为返回值来处理异常。

  • 不支持重载

  • 仅仅关注其中一个返回值。这个情况下,你可以将其他的返回值赋值给空白符_:

下面我们通过一个示例来了解一下,在线运行示例:https://play.golang.org/p/PYy3ueuPFS6

package main

import (
	"errors"
	"fmt"
	"strconv"
)

func main() {
	log1()

	log2("hello world")

	ret1 := add1(1, 1)
	fmt.Println("add1 result:" + strconv.Itoa(ret1))

// 	ret2, _ := Add2(0, 1) ;“ _ ”忽略err内容

	ret2, err := Add2(0, 1)
	if err == nil {
		fmt.Println("Add2 result:" + strconv.Itoa(ret2))
	} else {
		fmt.Println("Add2 error", err)
	}
}

// 私有、无入参、无返回值
func log1() {
	fmt.Println("execute func log1")
}

// 私有、入参、无返回值
func log2(msg string) {
	fmt.Println("execute func log2:" + msg)
}

// 私有、两个入参、一个返回值
func add1(count1, count2 int) int {
	total := count1 + count2
	fmt.Println("execute func add3, result=" + strconv.Itoa(total))
	return total
}

// Public、两个入参、多个返回值
func Add2(count1, count2 int) (int, error) {
	if count1 < 1 || count2 < 1 {
		return 0, errors.New("数量不能小于1")
	}
	total := count1 + count2
	return total, nil
}

该示例输出结果为:

execute func log1
execute func log2:hello world
execute func add3, result=2
add1 result:2
Add2 error 数量不能小于1

但函数有多个返回值的时候,有时你只关注其中一个返回值,这种情况下你可以将其他的返回值赋值给空白符:_,如下:

_, err := Add2(1, 2)
if err != nil {
  fmt.Println(err)
}

空白符特殊在于实际上返回值并没有赋值,所以你可以随意将不同类型的值赋值给他,而不会由于类型不同而报错。

入口函数 Main

在 go 中程序入口必须是 main 函数,并且在 main 包内。

 

第二章 ·结构体

Go 语言不是像 Java 那样的面向对象的语言,他没有对象和继承的概念。也没有class的概念。在 Go 语言中有个概念叫做结构体(struct),结构体和 Java 中的class比较类似。下面我们定义一个结构体:

type User struct {
	Name   string
	Gender string
	Age    int
}

上面我们定义了一个结构体User,并为该结构体分别设置了三个公有属性:Name/Gender/Age,下面我们来创建一个 User 对象。

user := User{
	Name:   "hahaha",
	Gender: "男",
	Age:    18, // 值得一提的是,最后的逗号是必须的,否则编译器会报错,这就是go的设计哲学之一,要求强一致性。
}

结构体的属性可以在结构体内直接声明,那么如何为结构体声明函数(即 Java 中的方法)呢,我们来看下下面的示例:在线运行示例:https://play.golang.org/p/01_cTu0RzdH

package main

import "fmt"

type User struct {
	Name   string
	Gender string
	Age    int
}

// 定义User的成员方法
func (u *User) addAge() {
	u.Age = u.Age + 1
}

func main() {
	user := User{
		Name:   "哈", // 名称
		Gender: "男", // 性别
		Age:    18,  // 值得一提的是,最后的逗号是必须的,否则编译器会报错,这就是go的设计哲学之一,要求强一致性。
	}
	user.addAge()
	fmt.Println(user.Age)
}

指针类型和值类型

Java 中值类型和引用类型都是定死的,int、double、float、long、byte、short、char、boolean 为值类型,其他的都是引用类型,而 Go 语言中却不是这样。

当你写 Go 代码的时候,很自然就会去问自己 应该是值还是指向值的指针呢? 这儿有两个好消息,首先,无论我们讨论下面哪一项,答案都是一样的:使用指针。

  • 局部变量赋值

  • 结构体指针

  • 函数返回值

  • 函数参数

  • 方法接收器

在 Go 语言中: * &表示取地址.        例如你有一个变量a,那么&a就是变量a在内存中的地址,对于 Golang 指针也是有类型的,比如 a 是一个 string 那么 & a 是一个 string 的指针类型,在 Go 里面叫 & string。

  • 表示取值,接上面的例子,假设你定义b := &a 如果你打印b,那么输出的是&a的内存地址,如果要取值,那么需要使用:b

下面我们来看下例子,在线运行:https://play.golang.org/p/jxAKyVMjnoy

package main

import (
	"fmt"
)

func main() {
	a := "123"  //初始化并赋值
	b := &a

	fmt.Println(a)  // 打印变量值
	fmt.Println(b)  //打印变量指针内存地址
	fmt.Println(*b)  // 打印指针内存地址所对应的值
}

输出结果为:
123
0x40c128
123

声明和初始化

当我们第一次看到变量和声明时,我们只看了内置类型,比如整数和字符串。既然现在我们要讨论结构,那么我们需要扩展讨论范围到指针。

创建结构的值的最简单的方式是:

type Saiyan struct {
  Name string
  Power int
}

以下实例化方式,依然有效:

//        示例都是声明变量 goku 并赋值
goku := Saiyan{
  Name: "Goku",
  Power: 9000,
}

// or

goku := Saiyan{}

//else or

goku := Saiyan{Name: "Goku"}
goku.Power = 9000

//else or 

//此外,你可以不写字段名,依赖字段顺序去初始化结构体 (但是为了可读性,你应该把字段名写清楚)
goku := Saiyan{"Goku", 9000}

许多时候,我们并不想让一个变量直接关联到值,而是让它的值为一个指针,通过指针关联到值。一个指针就是内存中的一个地址;指针的值就是实际值的地址。这是间接地获取值的方式。形象地来说,指针和实际值的关系就相当于房子和指向该房子的方向之间的关系。

为什么我们想要一个指针指向值而不是直接包含该值呢?这归结为 Go 中传递参数到函数的方式:就像复制。知道了这个,尝试理解一下下面的代码呢?

func main() {
  goku := Saiyan{"Goku", 9000}
  Super(goku)
  fmt.Println(goku.Power)
}

func Super(s Saiyan) {
  s.Power += 10000
}

上面程序运行的结果是 9000,而不是 19000,。为什么?因为 Super 修改了原始值 goku 的复制版本,而不是它本身,所以,Super 中的修改并不影响上层调用者。现在为了达到你的期望,我们可以传递一个指针到函数中:

func main() {
  goku := &Saiyan{"Goku", 9000}
  Super(goku)
  fmt.Println(goku.Power)
}

func Super(s *Saiyan) {
  s.Power += 10000
}

这一次,我们修改了两处代码。第一个是使用了 ==&== 操作符以获取值的地址(它就是 取地址 操作符)。然后,我们修改了 ==Super== 参数期望的类型。它之前期望一个 ==Saiyan== 类型,但是现在期望一个地址类型 ==Saiyan==,这里 ==X== 意思是 指向类型 ==X== 值的指针 。很显然类型 ==Saiyan== 和 ==*Saiyan== 是有关系的,但是他们是不同的类型。

这里注意到我们仍然传递了一个 ==goku== 的值的副本给 ==Super==,但这时 ==goku== 的值其实是一个地址。所以这个副本值也是一个与原值相等的地址,这就是我们间接传值的方式。想象一下,就像复制一个指向饭店的方向牌。你所拥有的是一个方向牌的副本,但是它仍然指向原来的饭店。

我们可以证实一下这是一个地址的副本,通过修改其指向的值(尽管这可能不是你真正想做的事情):

func main() {
  goku := &Saiyan{"Goku", 9000}
  Super(goku)
  fmt.Println(goku.Power)
}

func Super(s *Saiyan) {
  s = &Saiyan{"Gohan", 1000}
}

上面的代码,又一次地输出 9000。就像许多语言表现的那样,包括 Ruby,Python, Java 和 C#,Go 以及部分的 C#,只是让这个事实变得更明显一些。

同样很明显的是,复制一个指针比复制一个复杂的结构的消耗小多了。在 64 位的机器上面,一个指针占据 64 bit 的空间。如果我们有一个包含很多字段的结构,创建它的副本将会是一个很昂贵的操作。指针的真正价值在于能够分享它所指向的值。我们是想让 ==Super== 修改 ==goku== 的副本还是修改共享的 ==goku== 值本身呢?

所有这些并不是说你总应该使用指针。这章末尾,在我们见识了结构的更多功能以后,我们将重新检视 指针与值这个问题。

结构体上的函数

把一个方法关联在一个结构体上:

type Saiyan struct {
  Name string
  Power int
}

func (s *Saiyan) Super() {
  s.Power += 10000
}

在上面的代码中,可以这么理解,*Saiyan 类型是 Super 方法的 * 接受者 *。然后可以通过下面的代码去调用 Super 方法:

goku := &Saiyan{"Goku", 9001}
goku.Super()
fmt.Println(goku.Power) // 将会打印出 19001

构造器

结构体没有构造器。但是,你可以创建一个返回所期望类型的实例的函数(类似于工厂):

func NewSaiyan(name string, power int) *Saiyan {
  return &Saiyan{
    Name: name,
    Power: power,
  }
}

这种模式以错误的方式惹恼了很多开发人员。一方面,这里有一点轻微的语法变化;另一方面,它确实感觉有点不那么明显。

我们的工厂不必返回一个指针;下面的形式是完全有效的:

func NewSaiyan(name string, power int) Saiyan {
  return Saiyan{
    Name: name,
    Power: power,
  }
}

结构体的字段

到目前为止的例子中,==Saiyan== 有两个字段 ==Name== 和 ==Power==,其类型分别为 ==string== 和 ==int==。字段

任何类型 -- 包括其他结构体类型以及目前我们还没有提及的 ==array==,==maps==,==interfaces== 和 ==functions== 等类型。

例如,我们可以扩展 ==Saiyan== 的定义:

type Saiyan struct {
  Name string
  Power int
  Father *Saiyan
}

然后我们通过下面的方式初始化:

gohan := &Saiyan{
  Name: "Gohan",
  Power: 1000,
  Father: &Saiyan {
    Name: "Goku",
    Power: 9001,
    Father: nil,
  },
}

New

尽管缺少构造器,Go 语言却有一个内置的 new 函数,使用它来分配类型所需要的内存。 new(X) 的结果与 &X{} 相同。

goku := new(Saiyan)
// same as
goku := &Saiyan{}

如何使用取决于你,但是你会发现大多数人更偏爱后一种写法无论是否有字段需要初始化,因为这看起来更具可读性:

goku := new(Saiyan)
goku.name = "goku"
goku.power = 9001

//vs

goku := &Saiyan {
  Name: "goku",
  Power: 9000,
}

无论你选择哪一种,如果你遵循上述的工厂模式,就可以保护剩余的代码而不必知道或担心内存分配细节

组合

Go 支持组合, 这是将一个结构包含进另一个结构的行为。在某些语言中,这种行为叫做 特质 或者 混合。 没有明确的组合机制的语言总是可以做到这一点。在 Java 中, 可以使用 继承 来扩展结构。但是在脚本中并没有这种选项, 混合将会被写成如下形式:

public class Person {
  private String name;

  public String getName() {
    return this.name;
  }
}

public class Saiyan {
  // Saiyan 中包含着 person 对象
  private Person person;

  // 将请求转发到 person 中
  public String getName() {
    return this.person.getName();
  }
  ...
}

这可能会非常繁琐。Person 的每个方法都需要在 Saiyan 中重复。Go 避免了这种复杂性:

type Person struct {
  Name string
}

func (p *Person) Introduce() {
  fmt.Printf("Hi, I'm %s\n", p.Name)
}

type Saiyan struct {
  *Person
  Power int
}

// 使用它
goku := &Saiyan{
  Person: &Person{"Goku"},
  Power: 9001,
}
goku.Introduce()

==Saiyan== 结构体有一个 ==Person== 类型的字段。由于我们没有显示地给它一个字段名,所以我们可以隐式地访问组合类型的字段和函数。然而,Go 编译器确实给了它一个字段,下面这样完全有效:

goku := &Saiyan{
  Person: &Person{"Goku"},
}
fmt.Println(goku.Name)
fmt.Println(goku.Person.Name)

上面两个都打印 「Goku」。

组合比继承更好吗?许多人认为它是一种更好的组织代码的方式。当使用继承的时候,你的类和超类紧密耦合在一起,你最终专注于结构而不是行为。

 

 

并发编程

Go 语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go 语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。 Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。

Java程序员Go语言入门简介_第1张图片

下面我们来看一个例子(在线演示:https://play.golang.org/p/U9U-qjuY0t1)

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个goroutine
	go runing()
	// 创建一个匿名的goroutine
	go func() {
		fmt.Println("喜特:" + time.Now().String())
	}()

	// 这里sleep一下是因为main方法如果执行完了,main该程序创建的所有goroutine都会退出
	time.Sleep(5 * time.Second)
}

func runing() {
	fmt.Println("法克:" + time.Now().String())
	time.Sleep(3 * time.Second)
}

输出:
法克:2009-11-10 23:00:00 +0000 UTC m=+0.000000001
喜特:2009-11-10 23:00:00 +0000 UTC m=+0.000000001

执行结果说明 fuck 函数中的 sleep 三秒并没有影响喜特的输出。 如果说 goroutine 是 Go 语言程序的并发体的话,那么 channel 就是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channel 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。 下面我们利用 goroutine+channel 来实现一个生产消费者模型,示例代码如下:(在线执行:https://play.golang.org/p/lqUBugLdU-I)

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个通道
	channel := make(chan int64)

	// 异步去生产
	go producer(channel)

	// 数据消费
	consumer(channel)
}

// 生产者
func producer(channel chan<- int64) {
	for {
		// 将数据写入通道
		channel <- time.Now().Unix()
		// 睡1秒钟
		time.Sleep(time.Second)
	}
}

// 消费者
func consumer(channel <-chan int64) {
	for {
		timestamp := <-channel
		fmt.Println(timestamp)
	}
}

输出为如下:(每秒钟打印一次)
1257894000
1257894001
1257894002
1257894003

Java 程序员觉得不好用的地方

  • 异常处理

  • 没有泛型

  • 不支持多态、重载

  • 不支持注解(但是他的 struct 中的属性支持tag)

参考

  • https://www.runoob.com/go/go-tutorial.html

  • https://books.studygolang.com/the-little-go-book_ZH_CN/

  • Go 简易教程 https://mlog.club/topic/231#toc_51

 

 

 

 

 

 

 

 

你可能感兴趣的:(go)