Browse Source

Add a Go language example.

This follows the other examples so that it can be used as a tutorial,
such as the ones at:
  https://developers.google.com/protocol-buffers/docs/tutorials

Even though Go generally does not use Makefiles, I added targets for the
Go examples to be consistent with the other languages.

Edit:

Fix Travis run. Change to use $HOME instead of ~. Add protoc to path.
GOPATH entry cannot start with shell metacharacter '~': "~/gocode"

Edit(2):

Fix Go code style to address comments.
Tim Swast 9 years ago
parent
commit
7e31c4d930
8 changed files with 410 additions and 0 deletions
  1. 5 0
      .travis.yml
  2. 21 0
      examples/Makefile
  3. 25 0
      examples/README.txt
  4. 128 0
      examples/add_person.go
  5. 58 0
      examples/add_person_test.go
  6. 59 0
      examples/list_people.go
  7. 96 0
      examples/list_people_test.go
  8. 18 0
      travis.sh

+ 5 - 0
.travis.yml

@@ -15,6 +15,7 @@ env:
   - CONFIG=cpp
   - CONFIG=cpp
   - CONFIG=cpp_distcheck
   - CONFIG=cpp_distcheck
   - CONFIG=csharp
   - CONFIG=csharp
+  - CONFIG=golang
   - CONFIG=java_jdk6
   - CONFIG=java_jdk6
   - CONFIG=java_jdk7
   - CONFIG=java_jdk7
   - CONFIG=java_oracle7
   - CONFIG=java_oracle7
@@ -48,6 +49,10 @@ matrix:
     # which doesn't work on OS X.
     # which doesn't work on OS X.
     - os: osx
     - os: osx
       env: CONFIG=csharp
       env: CONFIG=csharp
+    # Requires installing golang, currently travis.sh is doing that with apt-get
+    # which doesn't work on OS X.
+    - os: osx
+      env: CONFIG=golang
   # Add into the matrix OS X tests of Objective C (needs Xcode, so it won't
   # Add into the matrix OS X tests of Objective C (needs Xcode, so it won't
   # work on other platforms). These are split so it doesn't take as long to run.
   # work on other platforms). These are split so it doesn't take as long to run.
   include:
   include:

+ 21 - 0
examples/Makefile

@@ -5,6 +5,8 @@
 all: cpp java python
 all: cpp java python
 
 
 cpp:    add_person_cpp    list_people_cpp
 cpp:    add_person_cpp    list_people_cpp
+go:     add_person_go     list_people_go
+gotest: add_person_gotest list_people_gotest
 java:   add_person_java   list_people_java
 java:   add_person_java   list_people_java
 python: add_person_python list_people_python
 python: add_person_python list_people_python
 
 
@@ -13,6 +15,8 @@ clean:
 	rm -f javac_middleman AddPerson*.class ListPeople*.class com/example/tutorial/*.class
 	rm -f javac_middleman AddPerson*.class ListPeople*.class com/example/tutorial/*.class
 	rm -f protoc_middleman addressbook.pb.cc addressbook.pb.h addressbook_pb2.py com/example/tutorial/AddressBookProtos.java
 	rm -f protoc_middleman addressbook.pb.cc addressbook.pb.h addressbook_pb2.py com/example/tutorial/AddressBookProtos.java
 	rm -f *.pyc
 	rm -f *.pyc
+	rm -f protoc_middleman_go tutorial/*.pb.go add_person_go list_people_go
+	rmdir tutorial 2>/dev/null || true
 	rmdir com/example/tutorial 2>/dev/null || true
 	rmdir com/example/tutorial 2>/dev/null || true
 	rmdir com/example 2>/dev/null || true
 	rmdir com/example 2>/dev/null || true
 	rmdir com 2>/dev/null || true
 	rmdir com 2>/dev/null || true
@@ -21,6 +25,11 @@ protoc_middleman: addressbook.proto
 	protoc --cpp_out=. --java_out=. --python_out=. addressbook.proto
 	protoc --cpp_out=. --java_out=. --python_out=. addressbook.proto
 	@touch protoc_middleman
 	@touch protoc_middleman
 
 
+protoc_middleman_go: addressbook.proto
+	mkdir tutorial # make directory for go package
+	protoc --go_out=tutorial addressbook.proto
+	@touch protoc_middleman_go
+
 add_person_cpp: add_person.cc protoc_middleman
 add_person_cpp: add_person.cc protoc_middleman
 	pkg-config --cflags protobuf  # fails if protobuf is not installed
 	pkg-config --cflags protobuf  # fails if protobuf is not installed
 	c++ add_person.cc addressbook.pb.cc -o add_person_cpp `pkg-config --cflags --libs protobuf`
 	c++ add_person.cc addressbook.pb.cc -o add_person_cpp `pkg-config --cflags --libs protobuf`
@@ -29,6 +38,18 @@ list_people_cpp: list_people.cc protoc_middleman
 	pkg-config --cflags protobuf  # fails if protobuf is not installed
 	pkg-config --cflags protobuf  # fails if protobuf is not installed
 	c++ list_people.cc addressbook.pb.cc -o list_people_cpp `pkg-config --cflags --libs protobuf`
 	c++ list_people.cc addressbook.pb.cc -o list_people_cpp `pkg-config --cflags --libs protobuf`
 
 
+add_person_go: add_person.go protoc_middleman_go
+	go build -o add_person_go add_person.go
+
+add_person_gotest: add_person_test.go add_person_go
+	go test add_person.go add_person_test.go
+
+list_people_go: list_people.go protoc_middleman_go
+	go build -o list_people_go list_people.go
+
+list_people_gotest: list_people.go list_people_go
+	go test list_people.go list_people_test.go
+
 javac_middleman: AddPerson.java ListPeople.java protoc_middleman
 javac_middleman: AddPerson.java ListPeople.java protoc_middleman
 	javac AddPerson.java ListPeople.java com/example/tutorial/AddressBookProtos.java
 	javac AddPerson.java ListPeople.java com/example/tutorial/AddressBookProtos.java
 	@touch javac_middleman
 	@touch javac_middleman

+ 25 - 0
examples/README.txt

@@ -27,3 +27,28 @@ These examples are part of the Protocol Buffers tutorial, located at:
 * Note that on some platforms you may have to edit the Makefile and remove
 * Note that on some platforms you may have to edit the Makefile and remove
 "-lpthread" from the linker commands (perhaps replacing it with something else).
 "-lpthread" from the linker commands (perhaps replacing it with something else).
 We didn't do this automatically because we wanted to keep the example simple.
 We didn't do this automatically because we wanted to keep the example simple.
+
+## Go ##
+
+The Go example requires a plugin to the protocol buffer compiler, so it is not
+build with all the other examples.  See:
+  https://github.com/golang/protobuf
+for more information about Go protocol buffer support.
+
+First, install the the Protocol Buffers compiler (protoc).
+Then, install the Go Protocol Buffers plugin
+($GOPATH/bin must be in your $PATH for protoc to find it):
+  go get github.com/golang/protobuf/protoc-gen-go
+
+Build the Go samples in this directory with "make go".  This creates the
+following executable files in the current directory:
+  add_person_go      list_people_go
+To run the example:
+  ./add_person_go addressbook.data
+to add a person to the protocol buffer encoded file addressbook.data.  The file
+is created if it does not exist.  To view the data, run:
+  ./list_people_go addressbook.data
+
+Observe that the C++, Python, and Java examples in this directory run in a
+similar way and can view/modify files created by the Go example and vice
+versa.

+ 128 - 0
examples/add_person.go

@@ -0,0 +1,128 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+	pb "github.com/google/protobuf/examples/tutorial"
+)
+
+func promptForAddress(r io.Reader) (*pb.Person, error) {
+	// A protocol buffer can be created like any struct.
+	p := &pb.Person{}
+
+	rd := bufio.NewReader(r)
+	fmt.Print("Enter person ID number: ")
+	// An int32 field in the .proto file is represented as an int32 field
+	// in the generated Go struct.
+	if _, err := fmt.Fscanf(rd, "%d\n", &p.Id); err != nil {
+		return p, err
+	}
+
+	fmt.Print("Enter name: ")
+	name, err := rd.ReadString('\n')
+	if err != nil {
+		return p, err
+	}
+	// A string field in the .proto file results in a string field in Go.
+	// We trim the whitespace because rd.ReadString includes the trailing
+	// newline character in its output.
+	p.Name = strings.TrimSpace(name)
+
+	fmt.Print("Enter email address (blank for none): ")
+	email, err := rd.ReadString('\n')
+	if err != nil {
+		return p, err
+	}
+	p.Email = strings.TrimSpace(email)
+
+	for {
+		fmt.Print("Enter a phone number (or leave blank to finish): ")
+		phone, err := rd.ReadString('\n')
+		if err != nil {
+			return p, err
+		}
+		phone = strings.TrimSpace(phone)
+		if phone == "" {
+			break
+		}
+		// The PhoneNumber message type is nested within the Person
+		// message in the .proto file.  This results in a Go struct
+		// named using the name of the parent prefixed to the name of
+		// the nested message.  Just as with pb.Person, it can be
+		// created like any other struct.
+		pn := &pb.Person_PhoneNumber{
+			Number: phone,
+		}
+
+		fmt.Print("Is this a mobile, home, or work phone? ")
+		ptype, err := rd.ReadString('\n')
+		if err != nil {
+			return p, err
+		}
+		ptype = strings.TrimSpace(ptype)
+
+		// A proto enum results in a Go constant for each enum value.
+		switch ptype {
+		case "mobile":
+			pn.Type = pb.Person_MOBILE
+		case "home":
+			pn.Type = pb.Person_HOME
+		case "work":
+			pn.Type = pb.Person_WORK
+		default:
+			fmt.Printf("Unknown phone type %q.  Using default.\n", ptype)
+		}
+
+		// A repeated proto field maps to a slice field in Go.  We can
+		// append to it like any other slice.
+		p.Phones = append(p.Phones, pn)
+	}
+
+	return p, nil
+}
+
+// Main reads the entire address book from a file, adds one person based on
+// user input, then writes it back out to the same file.
+func main() {
+	if len(os.Args) != 2 {
+		log.Fatalf("Usage:  %s ADDRESS_BOOK_FILE\n", os.Args[0])
+	}
+	fname := os.Args[1]
+
+	// Read the existing address book.
+	in, err := ioutil.ReadFile(fname)
+	if err != nil {
+		if os.IsNotExist(err) {
+			fmt.Printf("%s: File not found.  Creating new file.\n", fname)
+		} else {
+			log.Fatalln("Error reading file:", err)
+		}
+	}
+	book := &pb.AddressBook{}
+	if err := proto.Unmarshal(in, book); err != nil {
+		log.Fatalln("Failed to parse address book:", err)
+	}
+
+	// Add an address.
+	addr, err := promptForAddress(os.Stdin)
+	if err != nil {
+		log.Fatalln("Error with address:", err)
+	}
+	book.People = append(book.People, addr)
+
+	// Write the new address book back to disk.
+	out, err := proto.Marshal(book)
+	if err != nil {
+		log.Fatalln("Failed to encode address book:", err)
+	}
+	if err := ioutil.WriteFile(fname, out, 0644); err != nil {
+		log.Fatalln("Failed to write address book:", err)
+	}
+}

+ 58 - 0
examples/add_person_test.go

@@ -0,0 +1,58 @@
+package main
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	pb "github.com/google/protobuf/examples/tutorial"
+)
+
+func TestPromptForAddressReturnsAddress(t *testing.T) {
+	in := `12345
+Example Name
+name@example.com
+123-456-7890
+home
+222-222-2222
+mobile
+111-111-1111
+work
+777-777-7777
+unknown
+
+`
+	got, err := promptForAddress(strings.NewReader(in))
+	if err != nil {
+		t.Fatalf("promptForAddress(%q) had unexpected error: %s", in, err.Error())
+	}
+	if got.Id != 12345 {
+		t.Errorf("promptForAddress(%q) got %d, want ID %d", in, got.Id, 12345)
+	}
+	if got.Name != "Example Name" {
+		t.Errorf("promptForAddress(%q) => want name %q, got %q", "Example Name", got.Name)
+	}
+	if got.Email != "name@example.com" {
+		t.Errorf("promptForAddress(%q) => want email %q, got %q", "name@example.com", got.Email)
+	}
+
+	want := []*pb.Person_PhoneNumber{
+		{Number: "123-456-7890", Type: pb.Person_HOME},
+		{Number: "222-222-2222", Type: pb.Person_MOBILE},
+		{Number: "111-111-1111", Type: pb.Person_WORK},
+		{Number: "777-777-7777", Type: pb.Person_MOBILE},
+	}
+	if len(got.Phones) != len(want) {
+		t.Errorf("want %d phone numbers, got %d", len(want), len(got.Phones))
+	}
+	phones := len(got.Phones)
+	if phones > len(want) {
+		phones = len(want)
+	}
+	for i := 0; i < phones; i++ {
+		if !proto.Equal(got.Phones[i], want[i]) {
+			t.Errorf("want phone %q, got %q", *want[i], *got.Phones[i])
+		}
+
+	}
+}

+ 59 - 0
examples/list_people.go

@@ -0,0 +1,59 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+
+	"github.com/golang/protobuf/proto"
+	pb "github.com/google/protobuf/examples/tutorial"
+)
+
+func listPeople(w io.Writer, book *pb.AddressBook) {
+	for _, p := range book.People {
+		fmt.Fprintln(w, "Person ID:", p.Id)
+		fmt.Fprintln(w, "  Name:", p.Name)
+		if p.Email != "" {
+			fmt.Fprintln(w, "  E-mail address:", p.Email)
+		}
+
+		for _, pn := range p.Phones {
+			switch pn.Type {
+			case pb.Person_MOBILE:
+				fmt.Fprint(w, "  Mobile phone #: ")
+			case pb.Person_HOME:
+				fmt.Fprint(w, "  Home phone #: ")
+			case pb.Person_WORK:
+				fmt.Fprint(w, "  Work phone #: ")
+			}
+			fmt.Fprintln(w, pn.Number)
+		}
+	}
+}
+
+// Main reads the entire address book from a file and prints all the
+// information inside.
+func main() {
+	if len(os.Args) != 2 {
+		log.Fatalf("Usage:  %s ADDRESS_BOOK_FILE\n", os.Args[0])
+	}
+	fname := os.Args[1]
+
+	// Read the existing address book.
+	in, err := ioutil.ReadFile(fname)
+	if err != nil {
+		if os.IsNotExist(err) {
+			fmt.Printf("%s: File not found.  Creating new file.\n", fname)
+		} else {
+			log.Fatalln("Error reading file:", err)
+		}
+	}
+	book := &pb.AddressBook{}
+	if err := proto.Unmarshal(in, book); err != nil {
+		log.Fatalln("Failed to parse address book:", err)
+	}
+
+	listPeople(os.Stdout, book)
+}

+ 96 - 0
examples/list_people_test.go

@@ -0,0 +1,96 @@
+package main
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	pb "github.com/google/protobuf/examples/tutorial"
+)
+
+func TestListPeopleWritesList(t *testing.T) {
+	buf := new(bytes.Buffer)
+	in := pb.AddressBook{[]*pb.Person{
+		{
+			Name:  "John Doe",
+			Id:    101,
+			Email: "john@example.com",
+		},
+		{
+			Name: "Jane Doe",
+			Id:   102,
+		},
+		{
+			Name:  "Jack Doe",
+			Id:    201,
+			Email: "jack@example.com",
+			Phones: []*pb.Person_PhoneNumber{
+				{Number: "555-555-5555", Type: pb.Person_WORK},
+			},
+		},
+		{
+			Name:  "Jack Buck",
+			Id:    301,
+			Email: "buck@example.com",
+			Phones: []*pb.Person_PhoneNumber{
+				{Number: "555-555-0000", Type: pb.Person_HOME},
+				{Number: "555-555-0001", Type: pb.Person_MOBILE},
+				{Number: "555-555-0002", Type: pb.Person_WORK},
+			},
+		},
+		{
+			Name:  "Janet Doe",
+			Id:    1001,
+			Email: "janet@example.com",
+			Phones: []*pb.Person_PhoneNumber{
+				{Number: "555-777-0000"},
+				{Number: "555-777-0001", Type: pb.Person_HOME},
+			},
+		},
+	}}
+	listPeople(buf, &in)
+	want := strings.Split(`Person ID: 101
+  Name: John Doe
+  E-mail address: john@example.com
+Person ID: 102
+  Name: Jane Doe
+Person ID: 201
+  Name: Jack Doe
+  E-mail address: jack@example.com
+  Work phone #: 555-555-5555
+Person ID: 301
+  Name: Jack Buck
+  E-mail address: buck@example.com
+  Home phone #: 555-555-0000
+  Mobile phone #: 555-555-0001
+  Work phone #: 555-555-0002
+Person ID: 1001
+  Name: Janet Doe
+  E-mail address: janet@example.com
+  Mobile phone #: 555-777-0000
+  Home phone #: 555-777-0001
+`, "\n")
+	got := strings.Split(buf.String(), "\n")
+	if len(got) != len(want) {
+		t.Errorf(
+			"listPeople(%s) =>\n\t%q has %d lines, want %d",
+			in.String(),
+			buf.String(),
+			len(got),
+			len(want))
+	}
+	lines := len(got)
+	if lines > len(want) {
+		lines = len(want)
+	}
+	for i := 0; i < lines; i++ {
+		if got[i] != want[i] {
+			t.Errorf(
+				"listPeople(%s) =>\n\tline %d %q, want %q",
+				in.String(),
+				i,
+				got[i],
+				want[i])
+		}
+	}
+}

+ 18 - 0
travis.sh

@@ -47,6 +47,24 @@ build_csharp() {
   cd conformance && make test_csharp && cd ..
   cd conformance && make test_csharp && cd ..
 }
 }
 
 
+build_golang() {
+  # Go build needs `protoc`.
+  internal_build_cpp
+  # Add protoc to the path so that the examples build finds it.
+  export PATH="`pwd`/src:$PATH"
+
+  # Install Go and the Go protobuf compiler plugin.
+  sudo apt-get update -qq
+  sudo apt-get install -qq golang
+  export GOPATH="$HOME/gocode"
+  mkdir -p "$GOPATH/src/github.com/google"
+  ln -s "`pwd`" "$GOPATH/src/github.com/google/protobuf"
+  export PATH="$GOPATH/bin:$PATH"
+  go get github.com/golang/protobuf/protoc-gen-go
+
+  cd examples && make gotest && cd ..
+}
+
 use_java() {
 use_java() {
   version=$1
   version=$1
   case "$version" in
   case "$version" in