Java程序员的Golang入门指南(下)

4.高级特性

上面介绍的只是Golang的基本语法和特性,尽管像控制语句的条件不用圆括号、函数多返回值、switch-case默认break、函数闭包、集合切片等特性相比Java的确提高了开发效率,但这些在其他语言中也都有,并不是Golang能真正吸引人的地方。不仅是Golang,我们学习任何语言当然都是从基本语法特性着手,但学习时要不断地问自己:使这门语言区别于其他语言的”独到之处“在哪?这种独到之处往往反映了语言的设计思想、出发点、要解决的”痛点“,这才是一门语言或任何技术的立足之本

4.1 goroutine

goroutine使用go关键字来调用函数,也可以使用匿名函数。可以简单的把go关键字调用的函数想像成pthread_create。如果一个goroutine没有被阻塞,那么别的goroutine就不会得到执行。也就是说goroutine阻塞时,Golang会切换到其他goroutine执行,这是非常好的特性!Java对类似goroutine这种的协程没有原生支持,像Akka最害怕的就是阻塞。因为协程不等同于线程,操作系统不会帮我们完成“现场”保存和恢复,所以要实现goroutine这种特性,就要模拟操作系统的行为,保存方法或函数在协程“上下文切换”时的Context,当阻塞结束时才能正确地切换回来。像Kilim等协程库利用字节码生成,能够胜任,而Akka完全是运行时的。

注意:如果你要真正的并发,需要调用runtime.GOMAXPROCS(CPU_NUM)设置。

package main

import "fmt"

func main() {
go f("goroutine") go func(msg string) {
fmt.Println(msg)
}("going") // Block main thread
var input string
fmt.Scanln(&input)
fmt.Println("done")
} func f(msg string) {
fmt.Println(msg)
}

4.2 原子操作

像Java一样,Golang支持很多CAS操作。运行结果是unsaftCnt可能小于200,因为unsafeCnt++在机器指令层面上不是一条指令,而可能是从内存加载数据到寄存器、执行自增运算、保存寄存器中计算结果到内存这三部分,所以不进行保护的话有些更新是会丢失的。

package main

import (
"fmt"
"time"
"sync/atomic"
"runtime"
) func main() {
// IMPORTANT!!!
runtime.GOMAXPROCS(4) // thread-unsafe
var unsafeCnt int32 = 0
for i := 0; i < 10; i++ {
go func() {
for i := 0; i < 20; i++ {
time.Sleep(time.Millisecond)
unsafeCnt++
}
}()
}
time.Sleep(time.Second)
fmt.Println("cnt: ", unsafeCnt) // CAS toolkit
var cnt int32 = 0
for i := 0; i < 10; i++ {
go func() {
for i := 0; i < 20; i++ {
time.Sleep(time.Millisecond)
atomic.AddInt32(&cnt, 1)
}
}()
} time.Sleep(time.Second)
cntFinal := atomic.LoadInt32(&cnt)
fmt.Println("cnt: ", cntFinal)
}

神奇CAS的原理

Golang的AddInt32()类似于Java中AtomicInteger.incrementAndGet(),其伪代码可以表示如下。二者的基本思想是一致的,本质上是 乐观锁:首先,从内存位置M加载要修改的数据到寄存器A中;然后,修改数据并保存到另一寄存器B;最终,利用CPU提供的CAS指令(Java通过JNI调用到)用一条指令完成:1)A值与M处的原值比较;2)若相同则将B值覆盖到M处。

若不相同,则CAS指令会失败,说明从内存加载到执行CAS指令这一小段时间内,发生了上下文切换,执行了其他线程的代码修改了M处的变量值。那么重新执行前面几个步骤再次尝试。

ABA问题:即另一线程修改了M位置的数据,但是从原值改为C,又从C改回原值。这样上下文切换回来,CAS指令发现M处的值“未改变”(实际是改了两次,最后改回来了),所以CAS指令正常执行,不会失败。这种问题在Java中可以用AtomicStampedReference/AtomicMarkableReference解决。

public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}

4.3 Channel管道

通过前面可以看到,尽管goroutine很方便很高效,但如果滥用的话很可能会导致并发安全问题。而Channel就是用来解决这个问题的,它是goroutine之间通信的桥梁,类似Actor模型中每个Actor的mailbox。多个goroutine要修改一个状态时,可以将请求都发送到一个Channel里,然后由一个goroutine负责顺序地修改状态。

Channel默认是阻塞的,也就是说select时如果没有事件,那么当前goroutine会发生读阻塞。同理,Channel是有大小的,当Channel满了时,发送方会发生写阻塞。Channel这种阻塞的特性加上goroutine可以很容易就能实现生产者-消费者模式

用case可以给Channel设置阻塞的超时时间,避免一直阻塞。而default则使select进入无阻塞模式

package main

import (
"fmt"
"time"
) /**
* Output:
* received message: hello
* received message: world
*
* received from channel-1: Hello
* received from channel-2: World
*
* received message: hello
* Time out!
*
* Nothing received!
* received message: hello
* Nothing received!
* Nothing received!
* Nothing received!
* Nothing received!
* Nothing received!
* Nothing received!
* Nothing received!
* Nothing received!
* Nothing received!
* received message: world
* Nothing received!
* Nothing received!
* Nothing received!
*/
func main() {
listenOnChannel()
selectTwoChannels() blockChannelWithTimeout()
unblockChannel()
} func listenOnChannel() {
// Specify channel type and buffer size
channel := make(chan string, 5) go func() {
channel <- "hello"
channel <- "world"
}() for i := 0; i < 2; i++ {
msg := <- channel
fmt.Println("received message: " + msg)
}
} func selectTwoChannels() {
c1 := make(chan string)
c2 := make(chan string) go func() {
time.Sleep(time.Second)
c1 <- "Hello"
}()
go func() {
time.Sleep(time.Second)
c2 <- "World"
}() for i := 0; i < 2; i++ {
select {
case msg1 := <- c1:
fmt.Println("received from channel-1: " + msg1)
case msg2 := <- c2:
fmt.Println("received from channel-2: " + msg2)
}
}
} func blockChannelWithTimeout() {
channel := make(chan string, 5) go func() {
channel <- "hello"
// Sleep 10 sec
time.Sleep(time.Second * 10)
channel <- "world"
}() for i := 0; i < 2; i++ {
select {
case msg := <- channel:
fmt.Println("received message: " + msg)
// Set timeout 5 sec
case <- time.After(time.Second * 5):
fmt.Println("Time out!")
}
}
} func unblockChannel() {
channel := make(chan string, 5) go func() {
channel <- "hello"
time.Sleep(time.Second * 10)
channel <- "world"
}() for i := 0; i < 15; i++ {
select {
case msg := <- channel:
fmt.Println("received message: " + msg)
default:
fmt.Println("Nothing received!")
time.Sleep(time.Second)
}
}
}

4.4 缓冲流

Golang的bufio包提供了方便的缓冲流操作,通过strings或网络IO得到流后,用bufio.NewReader/Writer()包装:

  • 缓冲区:Peek()或Read时,数据会从底层进入到缓冲区。缓冲区默认大小为4096字节。
  • 切片和拷贝:Peek()和ReadSlice()得到的都是切片(缓冲区数据的引用)而不是拷贝,所以更加节约空间。但是当缓冲区数据变化时,切片也会随之变化。而ReadBytes/String()得到的都是数据的拷贝,可以放心使用。
  • Unicode支持:ReadRune()可以直接读取Unicode字符。有意思的是Golang中Unicode字符也要用单引号,这点与Java不同。
  • 分隔符:ReadSlice/Bytes/String()得到的包含分隔符,bufio不会自动去掉。
  • Writer:对应地,Writer提供了WriteBytes/String/Rune。
  • undo方法:可以将读出的字节再放回到缓冲区,就像什么都没发生一样。
package main

import (
"fmt"
"strings"
"bytes"
"bufio"
) /**
* Buffered: 0
* Buffered after peek: 7
* ABCDE
* AxCDE
*
* abcdefghijklmnopqrst 20 <nil>
* uvwxyz1234567890 16 <nil>
* 0 EOF
*
* "ABC "
* "DEF "
* "GHI"
*
* "ABC "
* "DEF "
* "GHI"
*
* read unicode=[你], size=[3]
* read unicode=[好], size=[3]
* read(after undo) unicode=[好], size=[3]
*
* Available: 4096
* Buffered: 0
* Available after write: 4088
* Buffered after write: 8
* Buffer after write: ""
* Available after flush: 4096
* Buffered after flush: 0
* Buffer after flush: "ABCDEFGH"
*
* Hello,世界!
*/
func main() {
testPeek()
testRead()
testReadSlice()
testReadBytes()
testReadUnicode() testWrite()
testWriteByte()
} func testPeek() {
r := strings.NewReader("ABCDEFG")
br := bufio.NewReader(r) fmt.Printf("Buffered: %d\n", br.Buffered()) p, _ := br.Peek(5)
fmt.Printf("Buffered after peek: %d\n", br.Buffered())
fmt.Printf("%s\n", p) p[1] = 'x'
p, _ = br.Peek(5)
fmt.Printf("%s\n", p)
} func testRead() {
r := strings.NewReader("abcdefghijklmnopqrstuvwxyz1234567890")
br := bufio.NewReader(r)
b := make([]byte, 20) n, err := br.Read(b)
fmt.Printf("%-20s %-2v %v\n", b[:n], n, err) n, err = br.Read(b)
fmt.Printf("%-20s %-2v %v\n", b[:n], n, err) n, err = br.Read(b)
fmt.Printf("%-20s %-2v %v\n", b[:n], n, err)
} func testReadSlice() {
r := strings.NewReader("ABC DEF GHI")
br := bufio.NewReader(r) w, _ := br.ReadSlice(' ')
fmt.Printf("%q\n", w) w, _ = br.ReadSlice(' ')
fmt.Printf("%q\n", w) w, _ = br.ReadSlice(' ')
fmt.Printf("%q\n", w)
} func testReadBytes() {
r := strings.NewReader("ABC DEF GHI")
br := bufio.NewReader(r) w, _ := br.ReadBytes(' ')
fmt.Printf("%q\n", w) w, _ = br.ReadSlice(' ')
fmt.Printf("%q\n", w) s, _ := br.ReadString(' ')
fmt.Printf("%q\n", s)
} func testReadUnicode() {
r := strings.NewReader("你好,世界!")
br := bufio.NewReader(r) c, size, _ := br.ReadRune()
fmt.Printf("read unicode=[%c], size=[%v]\n", c, size) c, size, _ = br.ReadRune()
fmt.Printf("read unicode=[%c], size=[%v]\n", c, size) br.UnreadRune()
c, size, _ = br.ReadRune()
fmt.Printf("read(after undo) unicode=[%c], size=[%v]\n", c, size)
} func testWrite() {
b := bytes.NewBuffer(make([]byte, 0))
bw := bufio.NewWriter(b) fmt.Printf("Available: %d\n", bw.Available())
fmt.Printf("Buffered: %d\n", bw.Buffered()) bw.WriteString("ABCDEFGH")
fmt.Printf("Available after write: %d\n", bw.Available())
fmt.Printf("Buffered after write: %d\n", bw.Buffered())
fmt.Printf("Buffer after write: %q\n", b) bw.Flush()
fmt.Printf("Available after flush: %d\n", bw.Available())
fmt.Printf("Buffered after flush: %d\n", bw.Buffered())
fmt.Printf("Buffer after flush: %q\n", b)
} func testWriteByte() {
b := bytes.NewBuffer(make([]byte, 0))
bw := bufio.NewWriter(b) bw.WriteByte('H')
bw.WriteByte('e')
bw.WriteByte('l')
bw.WriteByte('l')
bw.WriteByte('o')
bw.WriteString(",")
bw.WriteRune('世')
bw.WriteRune('界')
bw.WriteRune('!')
bw.Flush() fmt.Println(b)
}

4.5 并发控制

sync包中的WaitGroup是个很有用的类,类似信号量。wg.Add()和Done()能够加减WaitGroup(信号量)的值,而Wait()会挂起当前线程直到信号量变为0。下面的例子用WaitGroup的值表示正在运行的goroutine数量。在goroutine中,用defer Done()确保goroutine正常或异常退出时,WaitGroup都能减一。

package main

import (
"fmt"
"sync"
) /**
* I'm waiting all goroutines on wg done
* I'm done=[0]
* I'm done=[1]
* I'm done=[2]
* I'm done=[3]
* I'm done=[4]
* I'm done=[5]
* I'm done=[6]
* I'm done=[7]
* I'm done=[8]
* I'm done=[9]
*/
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("I'm done=[%d]\n", id)
}(i)
} fmt.Println("I'm waiting all goroutines on wg done")
wg.Wait()
}

4.6 网络编程

Golang的net包的抽象层次还是挺高的,用不了几行代码就能实现一个简单的TCP或HTTP服务端了。

4.6.1 Socket编程

package main

import (
"net"
"fmt"
"io"
) /**
* Starting the server
* Accept the connection: 127.0.0.1:14071
* Warning: End of data EOF
*/
func main() {
listener, err := net.Listen("tcp", "127.0.0.1:12345")
if err != nil {
panic("error listen: " + err.Error())
}
fmt.Println("Starting the server") for {
conn, err := listener.Accept()
if err != nil {
panic("error accept: " + err.Error())
}
fmt.Println("Accept the connection: ", conn.RemoteAddr())
go echoServer(conn)
}
} func echoServer(conn net.Conn) {
buf := make([]byte, 1024)
defer conn.Close() for {
n, err := conn.Read(buf)
switch err {
case nil:
conn.Write(buf[0:n])
case io.EOF:
fmt.Printf("Warning: End of data %s\n", err)
return
default:
fmt.Printf("Error: read data %s\n", err)
return
}
}
}

4.6.2 Http服务器

package main

import (
"fmt"
"log"
"net/http"
) func main() {
http.HandleFunc("/hello", handleHello)
fmt.Println("serving on http://localhost:7777/hello")
log.Fatal(http.ListenAndServe("localhost:7777", nil))
} func handleHello(w http.ResponseWriter, req *http.Request) {
log.Println("serving", req.URL)
fmt.Fprintln(w, "Hello, world!")
}

5.结束语

5.1 Golang初体验

Golang的某些语法的确很简洁,像行尾无分号、条件语句无括号、类型推断、函数多返回值、异常处理、原生协程支持、DuckType继承等,尽管很多并不是Golang首创,但结合到一起写起来还是很舒服的。

当然Golang也有让人“不爽”的地方。像变量和函数中的类型声明写在后面简直是“反人类”!同样是颠覆,switch的case默认会break就很实用。另外,因为Golang主要还是想替代C做系统开发,所以像类啊、包啊还是能看到C的影子,例如类声明只有成员变量而不会包含方法实现等,支持全局函数等,所以有时看到aaa.bbb()还是有点迷糊,不知道aaa是包名还是实例名。

5.2 如何学习一门语言

当我们谈到学习英语时,想到的可能是背单词、学语法、练习听说读写。对于编程语言来说,背单词(关键字)、学语法(语法规则)少不了,可听说读写只剩下了“写”,因为我们说话的对象是“冷冰冰”的计算机。所以唯一的捷径就是“写”,不断地练习!

此外,学的语言多了也能总结出一些规律。首先是基础语法,包括了变量和常量、控制语句、函数、集合、OOP、异常处理、控制台输入输出、包管理等。然后是高级特性就差别比较大了。专注高并发的语言就要看并发方面的特性,专注OOP的语言就要看有哪些抽象层次更高的特性等等。还是那句话,基础语言只能说我们会用,而能够区别一门语言的高级特性才是它的根本和灵魂,也是我们要着重学习和领悟的地方。

Java程序员的Golang入门指南(下)的更多相关文章

  1. Java程序员的Golang入门指南(上)

    Java程序员的Golang入门指南 1.序言 Golang作为一门出身名门望族的编程语言新星,像豆瓣的Redis平台Codis.类Evernote的云笔记leanote等. 1.1 为什么要学习 如 ...

  2. Java程序员的现代RPC指南

    Java程序员的现代RPC指南 1.前言 1.1 RPC框架简介 最早接触RPC还是初学Java时,直接用Socket API传东西好麻烦.于是发现了JDK直接支持的RMI,然后就用得不亦乐乎,各种大 ...

  3. Java程序员的现代RPC指南(Windows版预编译好的Protoc支持C++,Java,Python三种最常用的语言,Thrift则支持几乎主流的各种语言)

    Java程序员的现代RPC指南 1.前言 1.1 RPC框架简介 最早接触RPC还是初学Java时,直接用Socket API传东西好麻烦.于是发现了JDK直接支持的RMI,然后就用得不亦乐乎,各种大 ...

  4. 一名资深架构师规划Java程序员五年职业生涯指南

    每个程序员.或者说每个工作者都应该有自己的职业规划,如果你不是富二代,不是官二代,也没有职业规划,希望你可以思考一下自己的将来.今天我给大家分享的是一篇来自阿里大牛对五年工作经验程序员的职业建议,希望 ...

  5. 2019最新版Java程序员零基础入门视频教程资料(全套)

    为了解决Java学习初学者在网上找视频难的事情,本人整理了一份2019年度最新版的Java学习视频教程.希望看到这份视频的你们都能找到一份称心的工作,技术上都能得到进一步的提升,好东西就要分享给你们, ...

  6. 分享下对JAVA程序员成长之路的总结<转>

    我也搞了几年JAVA了,由于一向懒惰,没有成为大牛,只是一普通程序猿,手痒来给新人分享下从新手成长为老鸟的已见.   首先初识语法的阶段,必须要学会怎么操作对象,操作if和for,操作list set ...

  7. 【转载】分享下多年积累的对JAVA程序员成长之路的总结

    注:该文是从百度贴吧转载过来,之前看到觉得写得还不错,对Java开发学习者来说很有意义的,可以看看. 我也搞了几年JAVA了,由于一向懒惰,没有成为大牛,只是一普通程序猿,不爱玩社交网站,不爱玩微博, ...

  8. Java程序员快速入门Go语言

    这篇文章帮助Java程序员快速入门Go语言. 转载至 开源中国社区. http://www.oschina.net 本文将以一个有代表性的例子为开始,以此让Java程序员对Go语言有个初步认识,随后将 ...

  9. Efficient&Elegant:Java程序员入门Cpp

    最近项目急需C++ 的知识结构,虽说我有过快速学习很多新语言的经验,但对于C++ 老特工我还需保持敬畏(内容太多),本文会从一个Java程序员的角度,制定高效学习路线快速入门C++ . Java是为了 ...

随机推荐

  1. [转]pycharm常用快捷键及设置

    PyCharm3.0默认快捷键(翻译的) PyCharm Default Keymap 1.编辑(Editing) Ctrl + Space    基本的代码完成(类.方法.属性)Ctrl + Alt ...

  2. Frame

    Frame意为框架,是在屏幕上的一个矩形区域. Frame主要作为其他组件的框架基础,或为其他组件提供间距补充. 何时使用Frame组件呢? Frame组件主要用于在复杂的布局中奖其他组件分组,也用于 ...

  3. STL之map排序

    描述 STL的map中存储了字符串以及对应出现的次数,请分别根据字符串顺序从小到大排序和出现次数从小到大排序. 部分代码已经给出,请补充完整,提交时请勿包含已经给出的代码. int main() { ...

  4. 【贪心】Codeforces 349B.Color the Fence题解

    题目链接:http://codeforces.com/problemset/problem/349/B 题目大意 小明要从9个数字(1,2,--,9)去除一些数字拼接成一个数字,是的这个数字最大. 但 ...

  5. CodeForces 914DBash and a Tough Math Puzzle(线段树的骚操作)

    D. Bash and a Tough Math Puzzle time limit per test 2.5 seconds memory limit per test 256 megabytes ...

  6. python 虚拟环境的搭建

    1.python3.6 2.MySQL 3.安装虚拟环境 1.pip install virtualenv  -i  https://pypi.douban.com/simple/ 2.pip ins ...

  7. mv&cp

    mv [选项] [源] [目标] 当目标不存在时,重命名源为目标 当目标存在时,若目标为目录文件,将源移动到目标文件里: 若目标为非目录文件,将源重命名为目标,并强制覆盖目标.   mv -b 1 2 ...

  8. [ZJOI 2007]Hide 捉迷藏

    Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏.他们的家很大且构造很奇特,由N个屋子和N-1条双 ...

  9. 贼有意思[最长上升公共子序列](SAC大佬测试题)

    题目描述Awson 最近越来越蠢了,一天就只知道 zyys.他定义了一个 zyys 数列:这个数列满足:1.是另外两个数列 A,B 的公共子序列;2.数列单调递增.现在他有一个问题,我们假设知道两个长 ...

  10. 洛谷P2480 [SDOI2010]古代猪文

    要求(图是盗来的QAQ) 首先用欧拉定理把幂模一下,直接就是MOD-1了 然后发现MOD-1可以分解为2,3,4679,35617,都是质数,可以直接用Lucas定理 然后用中国剩余定理合并一下即可 ...