504是一种常见的服务器错误,今天公司的开发机的服务器就出现了504错误,今天就要离职了,一天都没做什么事,正好这件事博哥找我看一下,查一下nginx的日志,发现是upstream timeout,我们知道nginx+php的模式下,nginx作为server把php的请求发到php-fpm进行处理,再把php-fpm处理的结果返回。
这里可以把php-fpm看作nginx日志里说的那个upstream。那么可以推出是php-fpm服务出了问题。

查看php-fpm服务的状态:

sudo service php-fpm status  # centos 6
sudo systemctl status php-fpm # centos 7

发现php-fpm的状态是running,没有问题~那是怎回事呢,不管三七二十一先 sudo service php-fpm restart (centos 7使用systemctl)试一下,发现竟然好了。

看起来问题是解决了,页面响应速度恢复了,但是一登录玉米平台,点开两三个页面有开始504了。。。
看来应该是服务器资源的问题,但是开发机配置很好,top一下可以看到资源是够用的,那就是php-fpm配置的问题了。

sudo vim /path/to/php-fpm.conf

找到 pm.max_children 的部分。可以看一下注释部分:

; The number of child processes to be created when pm is set to ‘static’ and the
; maximum number of child processes when pm is set to ‘dynamic’ or ‘ondemand’.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI. The below defaults are based on a server without much resources. Don’t
; forget to tweak pm.* to fit your needs.
; Note: Used when pm is set to ‘static’, ‘dynamic’ or ‘ondemand’
; Note: This value is mandatory.
pm.max_children = 5

默认的配置值是5,公司开发机配置了一大堆的虚拟机,估计得20来个了,可以预测是child processes不够导致的504,可以根据服务器配置适当地调大这个参数,我把它设置为32了,应该可以适应当前的需求了。

关于504 bad gateway可以理解一下,php-fpm的全称是php fastcgi proceesses manager, cgi则是common gateway interface即通用网关接口,gateway timeout就可以理解为这个cgi超时没有及时返回给web server数据。


刚刚发现这样改了还是会有504 timeout……囧了个囧,勇哥和我机智地分析了一下应该是代码中的问题,他们使用了curl请求另外的资源,我们猜测是curl长时间没有返回而导致php-fpm超时,修改代码设置curl请求的超时时间,再重启php-fpm请求,问题解决了。

一个问题出现可能有很多原因,要一点一点去分析,慢慢才能解决问题~~

之前一片文章分析过PHP中实现自动加载的方法,这几天在看lavaral的代码,发现其自动加载机制和Yii 2 的自动加载是同样的。这里索性把这种常用的自动加载实现做个简单的总结。

关于自动加载和命名空间,是很多PHP开发者绕不过去的领域,虽然很多旧的教程并不会提及这一块,但是可以看到随着PHP的不断发展,命名被用的越来越广泛,PSR-0 和 PSR-4 规范的通过也是业界的一种发展趋势。
关于自动加载和命名空间在项目中的使用,我还和勇哥进行过多次“和颜悦色”、“亲切友好”的交流,我是希望公司都能推广使用更好的自动加载机制(现在使用的自动加载确实有点麻烦),并且希望在项目中使用命名空间,最好符合PSR-0的规范。但是由于公司很多人都没有使用命名空间的习惯,所以讲过多次交流,勇哥没削我,我也没法说服他去推广。。。。。
但是希望所有的php开发者在尽早都使用命名空间。

废话不多说了,下面开始吧。

  1. 自动加载机制
  2. PSR-0和PSR-4规范
  3. laravel中自动流程
阅读全文 »

去年为实验室部署了gitlab用于项目代码和文档的管理,在hixicar项目中用的比较多,也起到不错的作用,可惜其他项目就没怎么用,基本是我自己在用了,不过也极大方便我自己的项目的管理。。
当时安装的最新版本是7.3,作为一个版本帝软件,gitlab的小版本号几乎一直在涨,最近已经涨到了7.14.1了。用的7.3虽然可用,但是有一些小小的问题,当一个项目的文件数特别多,hixi的文件大概到了1G多,后来把文档分离出来后还有500M左右,这样这个项目的Graph画不出来了。虽然没什么太大影响,但是总觉得不爽,一直也没有去解决,最近没什么事就想着升级一下。

官方提供的升级非常方便,可以直接使用软件仓库安装gitlab-ce替换原来安装的gitlab,会保留原来的数据不丢失。简直碉!堡!了!哇!

官方的升级文档: https://about.gitlab.com/upgrade-to-package-repository/

安装repo

curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash

但是我的curl要使用代理,之前使用代理是配置-x参数,但是从网上下载的这个 script.rpm.sh 脚本里面也要用curl去下载文件,这样就会导致运行这个脚本失败。可以设置curl的全局代理,这里有一个小小的trick:使用alias;

alias curl='curl -x proxy_host:port'

这样使用curl就相当于使用curl的别名(alias)会带上代理设置了。另一个方法就是在$HOME/.curlrc 配置文件中添加代理:

vim ~/.curlrc
// 添加
proxy=proxy_host:port

这样就可以了。
其实使用命令安装repo挺麻烦的,尤其在网络不方便的情况下。可以直接使用下面的文本添加repo到/etc/yum.repo.d/

[gitlab_gitlab-ce]
name=gitlab_gitlab-ce
baseurl=http://packages.gitlab.com/gitlab/gitlab-ce/el/6/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packages.gitlab.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt

[gitlab_gitlab-ce-source]
name=gitlab_gitlab-ce-source
baseurl=http://packages.gitlab.com/gitlab/gitlab-ce/el/6/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packages.gitlab.com/gpg.key

上面是对应centos 6的,7的话自己相应修改就可以了。其中的http协议也可以改成https。

安装gitlab-ce

gitlab提供的开源免费版本命名是gitlab-ce了,包名也成了gitlab-ce,安装好repo后,网络比较好的话,可以直接运行安装:

sudo yum install gitlab-ce

仔细看输出会看到gitlan-ce会替换原来的gitlab。因为gitlab的安装路径固定在/opt/gitlab/, 所以替换安装也比较方便吧。反正觉得真的挺方便的,原来的repositories都还在。

但是,gitlab-ce这个包有200多MB,使用yum安装下载数据要300多M。我试着下了好久都没有下载下来,只好在本地windows用浏览器下载了(本机翻墙速度比较好,500-1000Kb/s),再scp到服务器。

rpm下载地址: https://packages.gitlab.com/gitlab/gitlab-ce?filter=rpms

scp到服务器,可以使用git客户端提供就有的scp工具,启动git bash,运行 scp gitlab-ce-7.14.1-ce.0.el6.x86_64.rpm username@host:gitlab-ce-7.14.1-ce.0.el6.x86_64.rpm

在服务器端使用rpm安装

cd
sudo yum localinstall gitlab-ce-7.14.1-ce.0.el6.x86_64.rpm

使用rpm包本地安装也会替换安装原来的gitlab,简直不错。
要注意,虽然git仓库相关的数据不会被删除,但如果你修改多gitlab的rails项目,比如我修改过登陆页面的内容,这些修改会被覆盖掉。如果要保留这些数据要记得做备份。而gitlab的配置文件, /etc/gitlab/gitlab.rb 应该是不会被覆盖的。

安装完后,gitlab会自动 reconfigure 并启动,如果有问题就需要自己看输出内容排查了。比如我们使用学校的smtp服务器发送邮件,修改了gitlab配置的smtp配置,而又在/etc/gitlab/gitlab.rb 中配置了邮件相关的项,导致新的系统 reconfigure 失败,把两处的配置修改到相同就可以正常启动了。参考

新的gitlab添加了很多新的特性,更多的配置项,甚至还有多套主题。。。还不错。

加张图

之前一直使用Yii作为主力的PHP框架,但是也有很多人推荐了解学习一下laravel,虽然很多laravel的粉丝宣传的挺过的,但是也准备了解一下laravel。

官方提供的laravel的安装方式是通过composer进行安装,这是php发展的趋势,也是一个好的方向。但是在国内,这种通过网络安装的方式经常会有一些问题。特别是在校内的一台没有联网的机器就更麻烦了,幸好要安装的机器可以通过代理连接网络,这样问题也许还可以解决。

要安装laravel的最新版本,分以下几步: 1.安装composer, 2. 安装laravel

安装composer

在composer的官网有详细的资料介绍如何安装,一般使用下面的方式:

curl -sS https://getcomposer.org/installer | php -- --filename=composer

这里要为curl设置代理:

curl -sS -x prox_host:port https://getcomposer.org/installer | php -- --filename=composer

这样好像就可以了。其实直接下载一个composer.phar文档也可以,还更加方便呢,在windows 下建议直接下载phar文档,使用官方的exe安装器反而因为网络原因会安装失败。

如果使用命令安装的话可以直接在终端运行composer了,如果下载的是phar归档,则需要添加可执行权限,然后直接运行该归档

chmod +x composer.phar
./composer.phar

安装laravel

laravel的安装方式有两种:

  1. 通过composer安装laravel-installer,再通过installer新建项目
  2. 通过composer create-project 直接新建项目。

laravel installer

我最开始使用第一种方法,先安装laravel-installer:

composer require "laravel/installer=~1.1"

要配置代理需要配置环境变量,也可以指直接在bash设置:

set http_proxy=host:port
set https_proxy=host:port

再运行,可能会报[ErrorException] zlib_decode(): data error 这样的错误,这是openssl验证的问题,可以的通过:

php -r "print_r(openssl_get_cert_locations());"

查看openssl的配置情况并修改配置,但是这样很麻烦,我们可以直接关闭ssl的验证,这环境变量中设置:

HTTP_PROXY_REQUEST_FULLURI=false
HTTPS_PROXY_REQUEST_FULLURI=false

这是再安装就不会有错了。
上面的方法会把laravel installer安装到当前目录下的vendor/laravel。可以把这个目录添加到PATH,也可以直接全目录运行。

laravel new blog

这个命令会通过 Symfony的组件下载文件并新建一个laravel的目录,我在这一步不知道怎么设置代理,第一次网络连接失败,后来一次被我Ctrl-c了。

composer create-project

由于前面把composer配置得可用了,就想应该可以直接使用composer 新建项目了。就使用

composer create-project laravel/laravel --prefer-dist
`

这样就可以新建一个项目了。文件夹名字为laravel,应该可以自己修改了,剩下的配置一个vhost就可以运行laravel了。

最近在为公司的游戏接入多家支付平台,其中多次使用到openssl模块验证数据的签名,之前在做阿里支付的时候也有做支付的回调,但是当时直接套一个sdk代码,没有仔细研究,这里记录一下在php中使用openssl加密数据和验证数据签名的方法。

这里主要包括两个部分,一是直接加密数据,把原来的数据使用密钥加密后传输,在接收端使用密钥解密出数据,这种方法被少数的支付平台使用,比如安智?另一种更常用的是把传输的数据做一个数字签名,数据本身使用明文传输,接收方按照约定的方式使用接受的数据计算一个签名,然后比照接受的签名和计算的签名是否相同。
这两种方法各有优势,按需使用。下面分别介绍。

生成密钥对

首先确保你的php环境开启了openssl模块,这个具体不细说。建议在编译php时静态编译好。
生成密钥对有两种方法,一是使用php提供的方法generate一对密钥,另一种使用openssl命令行生成。

使用php函数生成密钥对

openssl模块提供了很多openssl相关的函数,参考手册 生成密钥对的方法如下:

$privateKey = openssl_pkey_new([
'private_key_bits' => 2048, // private key的大小
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);

openssl_pkey_export_to_file($privateKey, 'php-private.key');
$key = openssl_pkey_get_details($privateKey);
file_put_contents('php-public.key', $key['key']);

openssl_free_key($privateKey); // 释放资源
阅读全文 »

随机数/Rand

在之前的练习中,已经使用过随机数相关的函数了,比如随机休眠N毫秒:

time.Sleep(time.Milliseond * time.Duration(rand.Intn(500) ))

math/rand包提供了很多方法,上面的Intn(n int)方法就是放回n以内整数这是最常用的函数之一。另外常用的方法包括:

rand.Float64()  // 返回0-1之间的一个浮点数
rand.Int31()
rand.Uint32()

和其他平台的随机数生成器一样,rand包提供的随机数序列也是伪随机数,如果不传入一个不同的种子/seed,得到的随机数序列将是确定的。我们常把时间作为随机数种子传入方法:

rs := rand.NewSource(time.Now().UnixNano())
r := rand.New(rs)
// r此时是一个新的随机数生成器
// 正式使用应该这样
r.Intn(100)

strconv/数字与字符串转换

strconv包提供了字符串与数字类型的转换,这是一些很常用的功能,比如从”123”到123的转换,”1.23”转换到1.23;特别是在与客户端通信,使用json,或者处理从数据库查询的数据的时候,经常需要这类转换。

// parseFloat原型
func ParseFloat(s string, bitSize int) (f float64, err error)
f,_ := strconv.ParseFloat("1.23", 64) // 转换到64位Float,不处理转换的错误

// parseInt的原型, base从2-36
func ParseInt(s string, base int, bitSize int) (i int64, err error)

// 更常用的
k,_ := strconv.Atoi("123") // k=123
_,e := strconv.Atoi("wrongFormat") // will return error

其中最常用的是Atoi(s tring)Itoa(i int);strconv包提供的方法大部分会有两个返回值,第一个是转换的结果,第二个是是否转换成功的error。

URL相关

net/url包提供了对URL处理的方法,可以把一个字符转解析成一个url变量,通过提供的属性获取URL的各个部分。
我们自导一个完整的URL的模式是这样的:

scheme://[username:password@]host/path?querystring#fragement_id

其中的host包括domain和port。

url包提供了把一个字符串解析成url,并提供访问上面模式中各个元素的方法:

import "net/url"

urlStr := "https://gobyexample.com/url-parsing?from=goole#url-parsing"
u, err := url.Parse(urlStr)
if err != nil { fmt.Println(err)
}
fmt.Println(u.Scheme,u.Host, u.Path, u.Fragment, u.RawQuery)
host, port, _ := net.SplitHostPort(u.Host)

user := u.User
username := "null"
if user != nil {
username = user.Username() // user.Password()
}
fmt.Println("username", username)
// 解析queryString
query, _ := url.ParseQuery(u.RawQuery)
fmt.Println(query, query["from"][0])

The parsed query param maps are from strings to slices of strings, so index into [0] if you only want the first value.

注意querystring解析出来的map是一个建对应一个slice的。不是一一对应的字符串,这是因为请求时可能有相同的key,一般只取第一个。

Hash/SHA1

Go的crypto包及其子包中包括多种hash函数,我们最常用的MD5和SHA1都包含在里面。各种hash算法实现在各个子包中,比如md5在 crypto/md5, sha1在 crypto/sha1
子包列表和简介:

Name Synopsis
aes Package aes implements AES encryption (formerly Rijndael), as defined in U.S. Federal Information Processing Standards Publication 197.
cipher Package cipher implements standard block cipher modes that can be wrapped around low-level block cipher implementations.
des Package des implements the Data Encryption Standard (DES) and the Triple Data Encryption Algorithm (TDEA) as defined in U.S. Federal Information Processing Standards Publication 46-3.
dsa Package dsa implements the Digital Signature Algorithm, as defined in FIPS 186-3.
ecdsa Package ecdsa implements the Elliptic Curve Digital Signature Algorithm, as defined in FIPS 186-3.
elliptic Package elliptic implements several standard elliptic curves over prime fields.
hmac Package hmac implements the Keyed-Hash Message Authentication Code (HMAC) as defined in U.S. Federal Information Processing Standards Publication 198.
md5 Package md5 implements the MD5 hash algorithm as defined in RFC 1321.
rand Package rand implements a cryptographically secure pseudorandom number generator.
rc4 Package rc4 implements RC4 encryption, as defined in Bruce Schneier’s Applied Cryptography.
rsa Package rsa implements RSA encryption as specified in PKCS#1.
sha1 Package sha1 implements the SHA1 hash algorithm as defined in RFC 3174.
sha256 Package sha256 implements the SHA224 and SHA256 hash algorithms as defined in FIPS 180-4.
sha512 Package sha512 implements the SHA-384, SHA-512, SHA-512/224, and SHA-512/256 hash algorithms as defined in FIPS 180-4.
subtle Package subtle implements functions that are often useful in cryptographic code but require careful thought to use correctly.
tls Package tls partially implements TLS 1.2, as specified in RFC 5246.
x509 Package x509 parses X.509-encoded keys and certificates.
pkix Package pkix contains shared, low level structures used for ASN.1 parsing and serialization of X.509 certificates, CRL and OCSP.

hash用法:

s := "hash this string"
h := sha1.New() // 返回一个hash.Hash变量
// Hash接口: https://golang.org/pkg/hash/#Hash 帮助理解
h.Write([]byte(s))
bs := h.Sum(nil) //Sum(b []byte) appends the current hash to b and returns the resulting slice.
fmt.Println(s)
fmt.Printf("%x\n", bs) //Use the %x format verb to convert a hash results to a hex string.

比php直接调用方法要麻烦一些,理解了倒也还好,比java和C#要方便一些。

Encoding/编码相关

与编码相关的是encoding包和其子包。比如常用的base64的支持在encoding/base64。其用法为:

sEncoded := base64.StdEncoding.EncodeToString([]byte("abcdef123'=-=!="))
fmt.Println(sEncoded)
sDecoded := base64.StdEncoding.DecodeString(sEncoded)
fmt.Println(string(sDecoded))
urlEncoded := base64.URLEncoding.EncodeToString([]byte("你好"))

从上面可以看到encode和decode 是一对互逆的操作。StdEncoding 和 URLEncoding 的编码结果有一点点区别。

待续

后面将是文件操作,系统相关的内容,应该是最后一节了

现在越来越多的server使用nginx做前端,在使用php的项目中越来越多的使用单一入口文件,而且很多时候希望隐藏这个入口文件,生成一个漂亮而简洁的url。以前在apache下是使用一个独立的rewrite模块,或者使用.htaccess文件实现重定向,nginx中需要小小的配置一下。

比如对于一个请求: http://localhost/index.php?r=c/a&p=v 这样,我们希望请求到 http://localhost/c/a?p=v。

比较老的做法是配置一个rewrite,以前的项目一般都是这么做的:

location / {
if ($request_uri !~ "/(index\.php)") {
rewrite ^/(.*)$ /index.php/$1 last;
}
}

但是这样做有可能会导致循环重定向而返回500错误页面。并且有一个问题,就是静态文件也会被转发到index.php。
静态文件转发到php-fpm不仅导致做了无用的工作,而且对于允许用户上传图片/文件的应用可能导致安全问题。这是非常不推荐的用法。

为了解决上面的问题,我试过一个稍微麻烦但是能正常工作的方案:

if ($request_uri !~ "/static(.*)" ) {
set $test A;
}
if ( !-e $request_filename) {
set $test "${test}B";
}
if ($test = AB) {
rewrite ^/(.*) /index.php/$1 last;
}

这里假设静态文件都存放在/static目录下。这样可以让静态资源文件不被转发。但是这个方案啊,总觉得不那么优雅。

今天在解决一个问题的时候正好又查了一下,发现现在更多的使用的try_files配置来做重写。try_files的语法如下:

try_files file ... uri
try_files file ... = code

其作用域是server 和location,并且不能放在if条件里面。最常用的用法是:

try_files $uri $uri/ index.php

下面简单解释一下,try_files顾名思义就是尝试读取文件,正是对于请求的脚本不存在的情况,给nginx一个尝试读取脚本的策略。第一个是$uri就是读取uri指定的文件,如果不存在就把请求的看作目录,查找目录下有没有默认index文件(一般配置为index.html, index.htm, index.php);如果有则读取这个文件。对于try_files的最后一个参数,会作一个 内部重定向/fallback,这个内部重定向可以看作一个内部子请求,会重新被nginx配置match一遍。注意,只有最后一个参数会发起子请求

在我们的配置里面最后一个参数是index.php这样,会发起一轮新的match会被nginx配置里面的 location ~ .*\.(php|php5).*$ {} catch然后进行转发到php-fpm解析。
当请求是静态文件时,因为能直接match的$uri,所以直接就返回静态文件的内容了,对于页面的请求就会转发到index.php.
这样就解决了index转发和静态文件不转发的问题,键值优雅得多了。但是有一个问题,那就是参数。

对一般的web框架来说,其请求的url一都是这样样式的:

http://host.com/index.php?r=controller/action&param1=value1&...
// 要使用下面的url访问
http://host.com/controller/action?param1=value1&...

需要注意的是,nginx在匹配try_files的最后一项时不会自动把args(也就是querystring)转发出去,需要手动加上,所以对于上面的需求,可以使用下面的配置:

try_files $uri $uri/ /index.php?r=$uri&$args

这样,对于http://host.com/user/login?username=wuxu&passwd=123,$uri匹配到user/login,$args匹配到username=wuxu&password=123,经过try_files配置之后就是http://host.com/index.php?r=user/login&username=wuxu&password=123了。(当然这里只是用login做示例,实际中的login可不能用GET传递参数哦)。

对于我们自己实现的框架可能有不同的路由方法,相应的修改try_files的策略就可以了。

当然try_files的最后一个参数还可以更复杂,具体可以看nginx的参考文档。

完。

参考:

排序包/sort

go内置的sort包提供了内置类型和用户自定义类型的排序功能。需要注意的sort包的方法对传入的参数排序后直接修改参数,而不是返回一个新的数组。可以这么理解:一般传入排序的是一个数组,数组一般作为引用传递,而对于引用传递的修改直接反映在原数组上。
sort包提供对数组排序的函数,函数名为待排序数组元素类型加s,参数为待排序数组,比如

strs := []string{"bcd", "abk", "opq", "hij"}
sort.Strings(strs)
// strs has been sorted
sort.StringsAreSorted(strs) // true

同样对于int数组的排序方法为sort.Ints(ints) sort包还提供了一个判断数组是否排序的系列方法,其方法名为参数类型加s加AreSorted(); 比如 IntsAreSorted(ints), StringsAreSorted(strs)

给自定义类型排序

类似于java与其他面向对象语言可以为实现了sortable接口的对象数组进行排序,go也提供了为自定义类型数组进行排序的方法。sort.Sort(myArray)中myArray是一个自定义的类型,它是某种类型的数组类型。比如:

type myType []string

要实现排序,也需要为这种类型绑定一些方法,就像实现sortable接口的方法一样。实际上绑定这些方法也是实现go内置定义的 sort.Interface, 要实现Len() int, Swap和Less() bool三个方法。

阅读全文 »

openresty是章亦春(agentzh)维护的项目,早期由淘宝网赞助,后来作者加入cloudflare公司后,也就由该公司支持了。这是一个扩展nginx的项目,在nginx的基础上添加很多作者开发的模块。该项目目前有很多公司在使用。

openresty项目主页 http://openresty.org/cn/GITHUB

我们要安装一个openresty作为webserver同时把它配置成与nginx使用方式相同。

阅读全文 »

原子性计数/atomic

在过线程场景下,进行全局的技术是很麻烦的,在java中我们需要使用加锁去实现一段互斥代码,go提供的sync包中有一些专门用来计数的封装。

var counter uint64 = 0;
for ci:=0; ci<50; ci++ {
go func() {
for {
atomic.AddUint64(&counter, 1)
runtime.Gosched()
}
}()
}
time.Sleep(time.Second * 1)
cResult := atomic.LoadUint64(&counter)
fmt.Println("counter result is ", cResult)

注意sync包的方法都需要传入指针作为实参。还有goroutine中的runtime.Gosched()是明确指定go调度器切换上下文(使得其他goroutine也能运行);在无线有无限循环的goroutine中,应该在合适的地方添加这一调用。可以在 stackoverflow 了解更多关于Gosched()的知识。

互斥器/mutex

mutex在操作系统中是很重要的一部分,在资源调度/分配中,经常需要使用mutex防止资源同时被多个线程修改导致异常。在go中因为可以有多个goroutine执行,所以也存在同步互斥的问题。

https://gobyexample.com/mutexes

待更新

goroutine的状态/stateful goroutine

我们可以通过除斥锁来在多个goroutine共享状态,另外也可以通过go语言goroutine和channel的内置特性来实现。这种基于channel的实现方式,和goroutine通过通信来共享内存来确保一块数据只被唯一的一个goroutine所有的思路是一样的。

This channel-based approach aligns with Go’s ideas of sharing memory by communicating and having each piece of data owned by exactly 1 goroutine

实际上并不是goroutine本身拥有某种状态,我们这里讲述的也是一种编程的模式/模型,通过goroutine和channel的结合实现一种互斥访问的机制。
它的思路是这样的,有两个所有goroutine共享的channel,就叫它们reads 和 writes,共享的内存区域/变量由一个goroutine所有,我们把这个共享的变量叫做state,它是一个map;这个goroutine负责从state里面读取或者写入数据,当reads或者writes channel有新的任务到来时(任务由其他goroutine添加)。
其他的goroutine需要读取一个状态的时候,就往reads channel中传入一个值,然后等待reads操作返回。要写入的话也同理。
这里要设计reads和writes的数据结构了。也就是这两个channel的类型,reads是要读取,需要传入一个索引,假设是int型的索引,读取的结果希望也存在一个channel中来实现同步;writes则需要传入索引,值和写入数据以及完成的channel。设计如下:

type read struct {
key int
req chan int
}
type write struct {
key int
value int
req chan bool
}

这里的key和chan的类型是有共享的map决定的,我们假设共享的map是map[int]int的如果是其他的map则要相应的修改。

这两个结构就是我们通信用的数据。下面新建两个所有goroutine共享的channel:

reads := make(chan *read) // 注意是指针类型
writes := make(chan *write)

创建管理共享变量的goroutine:

// state manage gr
go func() {
state := make(map[int]int) // 共享的变量
for {
select {
case r:= <-reads: // 如果reads有新的任务
r.req <- state[r.key] // 把值写入channel
case w:= <-writes: // 如果writes有新的任务
state[w.key] = w.value // 把值写入共享变量
w.req <- true // 通知返回
}
}
}()

然后就可以访问共享变量了,读取示例:

for ri := 0; ri<20; ri++ {
go func() {
for {
r := &read { // 注意取地址符
key: rand.Intn(10),
req: make(chan int),
}
reads <- r
res := <-r.req
_ = res // 暂时不用
atomic.AddInt64(&ops, 1)
}
}()
}

写入示例:

for wi:=0; wi<10; wi++ {
go func() {
for {
w := &write{
key: rand.Intn(10),
value: rand.Intn(100),
req: make(chan bool),
}
writes <- w
res := <- w.req
_ = res // 暂时不用
atomic.AddInt64(&ops, 1)
}
}()
}

time.Sleep(time.Second * 1)
opsFinal := atomic.LoadInt64(&ops)
fmt.Println("total ops:", opsFinal)

关于使用:

It might be useful in certain cases though, for example where you have other channels involved or when managing multiple such mutexes would be error-prone. You should use whichever approach feels most natural, especially with respect to understanding the correctness of your program.